WorkmateOS Frontend/UI Architektur-Leitfaden
Überblick
Das WorkmateOS Frontend ist eine Vue 3 + TypeScript + Vite-basierte Anwendung, die eine modulare Architektur mit einem einzigartigen desktop-ähnlichen Fensterverwaltungssystem verwendet. Die Anwendung ist in unabhängige Module (CRM, Dashboard, etc.) organisiert, die als schwebende Fenster innerhalb der Hauptanwendung geöffnet werden können.
1. Gesamte Verzeichnisstruktur
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
ui/src/
├── main.ts # Einstiegspunkt
├── App.vue # Root-Komponente
├── style.css # Globale Styles
│
├── router/
│ └── index.ts # Vue Router Konfiguration
│
├── layouts/
│ ├── AppLayout.vue # Haupt-Layout (Topbar + Dock + Window Host)
│ ├── app-manager/
│ │ ├── appRegistry.ts # Zentrale App-Registrierung
│ │ ├── useAppManager.ts # Fensterverwaltungslogik
│ │ ├── WindowHost.vue # Container für alle Fenster
│ │ ├── WindowFrame.vue # Individueller Fenster-Wrapper
│ │ └── index.ts # Exports
│ └── components/
│ ├── Topbar.vue # Obere Navigationsleiste
│ ├── Dock.vue # Unteres App-Dock
│ └── index.ts # Exports
│
├── modules/
│ ├── crm/ # CRM-Modul
│ │ ├── CrmApp.vue # Modul-Einstiegskomponente
│ │ ├── pages/ # Seiten-Komponenten
│ │ │ ├── dashboard/
│ │ │ │ └── CrmDashboardPage.vue
│ │ │ ├── customer/
│ │ │ │ ├── CustomersListPage.vue
│ │ │ │ └── CustomerDetailPage.vue
│ │ │ ├── contacts/
│ │ │ │ ├── ContactsListPage.vue
│ │ │ │ └── ContactDetailPage.vue
│ │ │ └── index.ts # Seiten-Exports
│ │ ├── components/
│ │ │ ├── customer/
│ │ │ │ ├── CustomerCard.vue
│ │ │ │ └── CustomerForm.vue
│ │ │ ├── contacts/
│ │ │ │ ├── ContactCard.vue
│ │ │ │ └── ContactForm.vue
│ │ │ ├── widgets/
│ │ │ │ ├── CrmKpiCustomers.vue
│ │ │ │ ├── CrmRecentActivity.vue
│ │ │ │ ├── CrmShortcuts.vue
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── composables/
│ │ │ ├── useCrmNavigation.ts # Navigationszustandsverwaltung
│ │ │ ├── useCrmStats.ts # Statistik-Logik
│ │ │ └── useCrmActivity.ts # Aktivitätsverwaltung
│ │ ├── services/
│ │ │ └── crm.service.ts # API-Aufrufe
│ │ └── types/
│ │ ├── customer.ts
│ │ ├── contact.ts
│ │ ├── activity.ts
│ │ └── stats.ts
│ │
│ └── dashboard/
│ ├── pages/
│ │ └── DashboardPage.vue
│ ├── components/
│ │ ├── WidgetRenderer.vue
│ │ └── widgets/
│ │ ├── StatsWidget.vue
│ │ ├── RemindersWidget.vue
│ │ ├── ShortcutsWidget.vue
│ │ ├── ActivityFeedWidget.vue
│ │ ├── NotificationsWidget.vue
│ │ ├── CalendarWidget.vue
│ │ ├── WeatherWidget.vue
│ │ ├── ChartWidget.vue
│ │ ├── SystemMonitorWidget.vue
│ │ └── index.ts
│ ├── services/
│ │ └── widgetRegistry.ts # Widget-Registrierung
│ └── types/
│ └── widgetTypes.ts # Typ-Definitionen
│
├── services/
│ ├── api/
│ │ └── client.ts # Axios API Client
│ └── assets.ts # Asset-Pfade
│
├── composables/
│ ├── useDashboard.ts # Globale Dashboard-Logik
│ ├── useEmployees.ts
│ ├── useProjects.ts
│ └── useInvocies.ts
│
├── styles/
│ ├── tokens.css # Design-Tokens (Farben, Abstände, etc.)
│ ├── base.css # Globale Styles
│ └── components/
│ ├── button.css
│ └── kit-components.css
│
├── pages/ # Globale Seiten
│ ├── UnderConstruction.vue
│ └── Linktree.vue
│
└── assets/
└── [Bilder, Schriften, etc.]
2. Anwendungs-Bootstrap-Ablauf
main.ts
1
2
3
4
5
6
7
8
9
10
11
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { router } from "./router/index.ts";
const pinia = createPinia();
createApp(App)
.use(pinia)
.use(router)
.mount("#app");
Wichtige Punkte:
- Pinia wird für State Management initialisiert (aktuell minimal genutzt)
- Vue Router verwaltet globales Routing
- App.vue ist die Root-Komponente
App.vue (Root-Komponente)
1
2
3
4
5
6
7
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
Einfache Root-Komponente, die an den Router delegiert.
3. Router-Konfiguration (router/index.ts)
Der Router verwendet zwei Hauptrouten:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const routes = [
// Standard-Weiterleitung
{ path: "/", redirect: "/under-construction" },
// Haupt-App-Layout mit Modulen
{
path: "/app",
component: AppLayout,
children: [
{
path: "dashboard",
name: "dashboard",
component: () => import("@/modules/dashboard/pages/DashboardPage.vue"),
},
{
path: "crm",
name: "crm",
component: CrmApp, // Modul-Einstiegspunkt
}
],
},
// Öffentliche Routen
{
path: "/under-construction",
component: () => import("@/pages/UnderConstruction.vue"),
},
{
path: "/linktree",
component: () => import("@/pages/Linktree.vue"),
},
];
Muster: Child-Routen werden lazy-loaded, außer CrmApp, die direkt importiert wird.
4. Modul-System-Architektur
Modul-Registrierung (appRegistry.ts)
Zentrale Registrierung für alle Module:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { markRaw } from "vue";
import { icons } from "lucide-vue-next";
import CrmApp from "@/modules/crm/CrmApp.vue";
export const apps = [
{
id: "crm",
title: "CRM",
icons: markRaw(icons.Users),
component: markRaw(CrmApp),
window: {
width: 1100,
height: 700
}
},
// Weitere Apps hier...
];
Wichtige Punkte:
- Verwendet
markRaw(), um Vue-Reaktivitäts-Overhead zu vermeiden - Jede App hat eine
id,title,icon,componentund Standard-Fenstergröße - Wird vom Fenstermanager zum Öffnen von Apps verwendet
Fenstermanager (useAppManager.ts)
Reaktive Zustandsverwaltung für Fenster:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export interface WindowApp {
id: string; // Eindeutige Fensterinstanz-ID
appId: string; // Referenz zur App-Registrierung
title: string;
component: Component;
props?: Record<string, any>;
x: number; y: number; // Position
width: number;
height: number;
z: number; // Z-Index für Layering
}
export const appManager = {
windows, // Reaktives Array geöffneter Fenster
activeWindow, // Aktuell fokussiertes Fenster
openWindow(appId), // App öffnen
closeWindow(id), // Fenster schließen
focusWindow(id), // Fenster fokussieren (nach vorne bringen)
startDragFor(id, e), // Drag-Handler
startResizeFor(id, e) // Resize-Handler
};
Hauptfunktionen:
- Mehrere Instanzen derselben App können geöffnet sein
- Fenster sind verschiebbar und in der Größe veränderbar
- Z-Index wird für richtiges Layering verwaltet
- Position/Größe auf Viewport begrenzt
5. Modul-Strukturmuster - CRM-Modul-Beispiel
Modul-Einstiegspunkt (CrmApp.vue)
Fungiert als Router/Container für das Modul:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<script setup lang="ts">
import {
CrmDashboardPage,
CustomersListPage,
CustomerDetailPage,
ContactListPage,
ContactDetailPage,
} from "./pages";
import { useCrmNavigation } from "./composables/useCrmNavigation";
const {
view,
activeCustomerId,
activeContactId,
goDashboard,
goCustomers,
goCustomerDetail,
goContacts,
goContactDetail,
openCreateContact,
openCreateCustomer
} = useCrmNavigation();
</script>
<template>
<div class="crm-app h-full">
<!-- Bedingte Darstellung basierend auf Ansichtszustand -->
<CrmDashboardPage
v-if="view === 'dashboard'"
@openCustomers="goCustomers"
@create-customer="openCreateCustomer"
@create-contact="openCreateContact"
/>
<CustomersListPage
v-if="view === 'customers'"
@openCustomer="goCustomerDetail"
@openDashboard="goDashboard"
/>
<!-- Weitere Ansichten... -->
</div>
</template>
Muster:
- Modul hat internes Routing über Composable-basierten State
- Keine Vue Router Child-Routen (in sich geschlossen)
- Ansichtswechsel über bedingte Darstellung
Navigations-Composable (useCrmNavigation.ts)
Interne Zustandsverwaltung für Modulansichten:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export type CrmView =
| "dashboard"
| "customers"
| "customer-detail"
| "contacts"
| "contact-detail"
| "customer-create"
| "contact-create";
export function useCrmNavigation() {
const view = ref<CrmView>("dashboard");
const activeCustomerId = ref<string | null>(null);
const activeContactId = ref<string | null>(null);
function goDashboard() {
view.value = "dashboard";
activeCustomerId.value = null;
activeContactId.value = null;
}
function goCustomers() {
view.value = "customers";
activeCustomerId.value = null;
}
// ... weitere Navigationsfunktionen
return {
view,
activeCustomerId,
activeContactId,
goDashboard,
goCustomers,
// ... weitere Exports
};
}
Muster:
- Pures Composable ohne Abhängigkeiten vom Vue Router
- Referenzen über Composable übergeben, nicht über Route-Parameter
- Im gesamten Modul für Navigation verwendet
API-Service-Schicht (crm.service.ts)
Zentralisierte API-Aufrufe:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import api from "@/services/api/client";
export const crmService = {
// Kunden
async getCustomers(): Promise<Customer[]> {
const { data } = await api.get("/api/backoffice/crm/customers");
return data;
},
async getCustomer(id: string): Promise<Customer> {
const { data } = await api.get(`/api/backoffice/crm/customers/${id}`);
return data;
},
async createCustomer(payload: Partial<Customer>) {
return api.post("/api/backoffice/crm/customers", payload);
},
// Kontakte
async getContacts(): Promise<Contact[]> {
const { data } = await api.get("/api/backoffice/crm/contacts");
return data;
},
// Aktivitäten
async getLatestActivities(limit: number): Promise<CrmActivity[]> {
const { data } = await api.get(`/api/backoffice/crm/activities/latest?limit=${limit}`);
return data;
},
// Statistiken
async getCrmStats(): Promise<CrmStats> {
const { data } = await api.get("/api/backoffice/crm/stats");
return data;
}
};
Muster:
- Service-Objekt mit statischen Methoden
- Verwendet gemeinsamen Axios-Client
- Gibt Promise
für richtige Typisierung zurück - Handhabt URL-Konstruktion und Parameter
Datenabruf-Composable (useCrmStats.ts)
Ladezustand + Service-Integration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export function useCrmStats() {
const stats = ref<CrmStats | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
async function fetchStats() {
loading.value = true;
error.value = null;
try {
const data = await crmService.getCrmStats();
stats.value = data;
} catch (e: any) {
error.value = e.message ?? "Fehler beim Laden der Statistiken";
} finally {
loading.value = false;
}
}
return {
stats,
loading,
error,
fetchStats,
};
}
Muster:
- Wrapping von Service-Aufrufen
- Verwaltet Lade-/Fehlerzustände
- Gibt reaktive Ref + Fetch-Funktion zurück
- In Komponenten verwendet via
const { stats, loading } = useCrmStats()
Seiten-Komponenten (CustomersListPage.vue)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<template>
<div class="h-full flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-semibold text-white">Kundenliste</h1>
<button class="kit-btn-primary" @click="openCreateModal">
+ Neuer Kunde
</button>
</div>
<!-- Suche/Filter -->
<input v-model="search" type="text" placeholder="Kunde suchen…" />
<!-- Grid -->
<div class="grid grid-cols-4 gap-2">
<div
v-for="c in pagedCustomers"
:key="c.id"
@click="openCustomer(c.id)"
>
</div>
</div>
<!-- Pagination -->
<div class="flex items-center justify-between">
<button :disabled="page === 1" @click="page--">← Zurück</button>
<span>{"layout"=>"default", "title"=>"Architektur", "parent"=>"Frontend", "grand_parent"=>"Wiki", "nav_order"=>1, "content"=>"# WorkmateOS Frontend/UI Architektur-Leitfaden\n\n## Überblick\n\nDas WorkmateOS Frontend ist eine Vue 3 + TypeScript + Vite-basierte Anwendung, die eine **modulare Architektur** mit einem einzigartigen desktop-ähnlichen Fensterverwaltungssystem verwendet. Die Anwendung ist in unabhängige Module (CRM, Dashboard, etc.) organisiert, die als schwebende Fenster innerhalb der Hauptanwendung geöffnet werden können.\n\n---\n\n## 1. Gesamte Verzeichnisstruktur\n\n```\nui/src/\n├── main.ts # Einstiegspunkt\n├── App.vue # Root-Komponente\n├── style.css # Globale Styles\n│\n├── router/\n│ └── index.ts # Vue Router Konfiguration\n│\n├── layouts/\n│ ├── AppLayout.vue # Haupt-Layout (Topbar + Dock + Window Host)\n│ ├── app-manager/\n│ │ ├── appRegistry.ts # Zentrale App-Registrierung\n│ │ ├── useAppManager.ts # Fensterverwaltungslogik\n│ │ ├── WindowHost.vue # Container für alle Fenster\n│ │ ├── WindowFrame.vue # Individueller Fenster-Wrapper\n│ │ └── index.ts # Exports\n│ └── components/\n│ ├── Topbar.vue # Obere Navigationsleiste\n│ ├── Dock.vue # Unteres App-Dock\n│ └── index.ts # Exports\n│\n├── modules/\n│ ├── crm/ # CRM-Modul\n│ │ ├── CrmApp.vue # Modul-Einstiegskomponente\n│ │ ├── pages/ # Seiten-Komponenten\n│ │ │ ├── dashboard/\n│ │ │ │ └── CrmDashboardPage.vue\n│ │ │ ├── customer/\n│ │ │ │ ├── CustomersListPage.vue\n│ │ │ │ └── CustomerDetailPage.vue\n│ │ │ ├── contacts/\n│ │ │ │ ├── ContactsListPage.vue\n│ │ │ │ └── ContactDetailPage.vue\n│ │ │ └── index.ts # Seiten-Exports\n│ │ ├── components/\n│ │ │ ├── customer/\n│ │ │ │ ├── CustomerCard.vue\n│ │ │ │ └── CustomerForm.vue\n│ │ │ ├── contacts/\n│ │ │ │ ├── ContactCard.vue\n│ │ │ │ └── ContactForm.vue\n│ │ │ ├── widgets/\n│ │ │ │ ├── CrmKpiCustomers.vue\n│ │ │ │ ├── CrmRecentActivity.vue\n│ │ │ │ ├── CrmShortcuts.vue\n│ │ │ │ └── index.ts\n│ │ │ └── index.ts\n│ │ ├── composables/\n│ │ │ ├── useCrmNavigation.ts # Navigationszustandsverwaltung\n│ │ │ ├── useCrmStats.ts # Statistik-Logik\n│ │ │ └── useCrmActivity.ts # Aktivitätsverwaltung\n│ │ ├── services/\n│ │ │ └── crm.service.ts # API-Aufrufe\n│ │ └── types/\n│ │ ├── customer.ts\n│ │ ├── contact.ts\n│ │ ├── activity.ts\n│ │ └── stats.ts\n│ │\n│ └── dashboard/\n│ ├── pages/\n│ │ └── DashboardPage.vue\n│ ├── components/\n│ │ ├── WidgetRenderer.vue\n│ │ └── widgets/\n│ │ ├── StatsWidget.vue\n│ │ ├── RemindersWidget.vue\n│ │ ├── ShortcutsWidget.vue\n│ │ ├── ActivityFeedWidget.vue\n│ │ ├── NotificationsWidget.vue\n│ │ ├── CalendarWidget.vue\n│ │ ├── WeatherWidget.vue\n│ │ ├── ChartWidget.vue\n│ │ ├── SystemMonitorWidget.vue\n│ │ └── index.ts\n│ ├── services/\n│ │ └── widgetRegistry.ts # Widget-Registrierung\n│ └── types/\n│ └── widgetTypes.ts # Typ-Definitionen\n│\n├── services/\n│ ├── api/\n│ │ └── client.ts # Axios API Client\n│ └── assets.ts # Asset-Pfade\n│\n├── composables/\n│ ├── useDashboard.ts # Globale Dashboard-Logik\n│ ├── useEmployees.ts\n│ ├── useProjects.ts\n│ └── useInvocies.ts\n│\n├── styles/\n│ ├── tokens.css # Design-Tokens (Farben, Abstände, etc.)\n│ ├── base.css # Globale Styles\n│ └── components/\n│ ├── button.css\n│ └── kit-components.css\n│\n├── pages/ # Globale Seiten\n│ ├── UnderConstruction.vue\n│ └── Linktree.vue\n│\n└── assets/\n └── [Bilder, Schriften, etc.]\n```\n\n---\n\n## 2. Anwendungs-Bootstrap-Ablauf\n\n### main.ts\n```typescript\nimport { createApp } from \"vue\";\nimport { createPinia } from \"pinia\";\nimport App from \"./App.vue\";\nimport { router } from \"./router/index.ts\";\n\nconst pinia = createPinia();\n\ncreateApp(App)\n .use(pinia)\n .use(router)\n .mount(\"#app\");\n```\n\n**Wichtige Punkte:**\n- Pinia wird für State Management initialisiert (aktuell minimal genutzt)\n- Vue Router verwaltet globales Routing\n- App.vue ist die Root-Komponente\n\n### App.vue (Root-Komponente)\n```vue\n<script setup lang=\"ts\">\nimport { RouterView } from 'vue-router'\n</script>\n\n<template>\n <RouterView />\n</template>\n```\n\nEinfache Root-Komponente, die an den Router delegiert.\n\n---\n\n## 3. Router-Konfiguration (router/index.ts)\n\nDer Router verwendet zwei Hauptrouten:\n\n```typescript\nconst routes = [\n // Standard-Weiterleitung\n { path: \"/\", redirect: \"/under-construction\" },\n\n // Haupt-App-Layout mit Modulen\n {\n path: \"/app\",\n component: AppLayout,\n children: [\n {\n path: \"dashboard\",\n name: \"dashboard\",\n component: () => import(\"@/modules/dashboard/pages/DashboardPage.vue\"),\n },\n {\n path: \"crm\",\n name: \"crm\",\n component: CrmApp, // Modul-Einstiegspunkt\n }\n ],\n },\n\n // Öffentliche Routen\n {\n path: \"/under-construction\",\n component: () => import(\"@/pages/UnderConstruction.vue\"),\n },\n {\n path: \"/linktree\",\n component: () => import(\"@/pages/Linktree.vue\"),\n },\n];\n```\n\n**Muster:** Child-Routen werden lazy-loaded, außer CrmApp, die direkt importiert wird.\n\n---\n\n## 4. Modul-System-Architektur\n\n### Modul-Registrierung (appRegistry.ts)\n\nZentrale Registrierung für alle Module:\n\n```typescript\nimport { markRaw } from \"vue\";\nimport { icons } from \"lucide-vue-next\";\nimport CrmApp from \"@/modules/crm/CrmApp.vue\";\n\nexport const apps = [\n {\n id: \"crm\",\n title: \"CRM\",\n icons: markRaw(icons.Users),\n component: markRaw(CrmApp),\n window: {\n width: 1100,\n height: 700\n }\n },\n // Weitere Apps hier...\n];\n```\n\n**Wichtige Punkte:**\n- Verwendet `markRaw()`, um Vue-Reaktivitäts-Overhead zu vermeiden\n- Jede App hat eine `id`, `title`, `icon`, `component` und Standard-Fenstergröße\n- Wird vom Fenstermanager zum Öffnen von Apps verwendet\n\n### Fenstermanager (useAppManager.ts)\n\nReaktive Zustandsverwaltung für Fenster:\n\n```typescript\nexport interface WindowApp {\n id: string; // Eindeutige Fensterinstanz-ID\n appId: string; // Referenz zur App-Registrierung\n title: string;\n component: Component;\n props?: Record<string, any>;\n x: number; y: number; // Position\n width: number;\n height: number;\n z: number; // Z-Index für Layering\n}\n\nexport const appManager = {\n windows, // Reaktives Array geöffneter Fenster\n activeWindow, // Aktuell fokussiertes Fenster\n\n openWindow(appId), // App öffnen\n closeWindow(id), // Fenster schließen\n focusWindow(id), // Fenster fokussieren (nach vorne bringen)\n startDragFor(id, e), // Drag-Handler\n startResizeFor(id, e) // Resize-Handler\n};\n```\n\n**Hauptfunktionen:**\n- Mehrere Instanzen derselben App können geöffnet sein\n- Fenster sind verschiebbar und in der Größe veränderbar\n- Z-Index wird für richtiges Layering verwaltet\n- Position/Größe auf Viewport begrenzt\n\n---\n\n## 5. Modul-Strukturmuster - CRM-Modul-Beispiel\n\n### Modul-Einstiegspunkt (CrmApp.vue)\n\nFungiert als Router/Container für das Modul:\n\n```vue\n<script setup lang=\"ts\">\nimport {\n CrmDashboardPage,\n CustomersListPage,\n CustomerDetailPage,\n ContactListPage,\n ContactDetailPage,\n} from \"./pages\";\nimport { useCrmNavigation } from \"./composables/useCrmNavigation\";\n\nconst {\n view,\n activeCustomerId,\n activeContactId,\n goDashboard,\n goCustomers,\n goCustomerDetail,\n goContacts,\n goContactDetail,\n openCreateContact,\n openCreateCustomer\n} = useCrmNavigation();\n</script>\n\n<template>\n <div class=\"crm-app h-full\">\n <!-- Bedingte Darstellung basierend auf Ansichtszustand -->\n <CrmDashboardPage\n v-if=\"view === 'dashboard'\"\n @openCustomers=\"goCustomers\"\n @create-customer=\"openCreateCustomer\"\n @create-contact=\"openCreateContact\"\n />\n\n <CustomersListPage\n v-if=\"view === 'customers'\"\n @openCustomer=\"goCustomerDetail\"\n @openDashboard=\"goDashboard\"\n />\n\n <!-- Weitere Ansichten... -->\n </div>\n</template>\n```\n\n**Muster:**\n- Modul hat internes Routing über Composable-basierten State\n- Keine Vue Router Child-Routen (in sich geschlossen)\n- Ansichtswechsel über bedingte Darstellung\n\n### Navigations-Composable (useCrmNavigation.ts)\n\nInterne Zustandsverwaltung für Modulansichten:\n\n```typescript\nexport type CrmView =\n | \"dashboard\"\n | \"customers\"\n | \"customer-detail\"\n | \"contacts\"\n | \"contact-detail\"\n | \"customer-create\"\n | \"contact-create\";\n\nexport function useCrmNavigation() {\n const view = ref<CrmView>(\"dashboard\");\n const activeCustomerId = ref<string | null>(null);\n const activeContactId = ref<string | null>(null);\n\n function goDashboard() {\n view.value = \"dashboard\";\n activeCustomerId.value = null;\n activeContactId.value = null;\n }\n\n function goCustomers() {\n view.value = \"customers\";\n activeCustomerId.value = null;\n }\n\n // ... weitere Navigationsfunktionen\n\n return {\n view,\n activeCustomerId,\n activeContactId,\n goDashboard,\n goCustomers,\n // ... weitere Exports\n };\n}\n```\n\n**Muster:**\n- Pures Composable ohne Abhängigkeiten vom Vue Router\n- Referenzen über Composable übergeben, nicht über Route-Parameter\n- Im gesamten Modul für Navigation verwendet\n\n### API-Service-Schicht (crm.service.ts)\n\nZentralisierte API-Aufrufe:\n\n```typescript\nimport api from \"@/services/api/client\";\n\nexport const crmService = {\n // Kunden\n async getCustomers(): Promise<Customer[]> {\n const { data } = await api.get(\"/api/backoffice/crm/customers\");\n return data;\n },\n\n async getCustomer(id: string): Promise<Customer> {\n const { data } = await api.get(`/api/backoffice/crm/customers/${id}`);\n return data;\n },\n\n async createCustomer(payload: Partial<Customer>) {\n return api.post(\"/api/backoffice/crm/customers\", payload);\n },\n\n // Kontakte\n async getContacts(): Promise<Contact[]> {\n const { data } = await api.get(\"/api/backoffice/crm/contacts\");\n return data;\n },\n\n // Aktivitäten\n async getLatestActivities(limit: number): Promise<CrmActivity[]> {\n const { data } = await api.get(`/api/backoffice/crm/activities/latest?limit=${limit}`);\n return data;\n },\n\n // Statistiken\n async getCrmStats(): Promise<CrmStats> {\n const { data } = await api.get(\"/api/backoffice/crm/stats\");\n return data;\n }\n};\n```\n\n**Muster:**\n- Service-Objekt mit statischen Methoden\n- Verwendet gemeinsamen Axios-Client\n- Gibt Promise<T> für richtige Typisierung zurück\n- Handhabt URL-Konstruktion und Parameter\n\n### Datenabruf-Composable (useCrmStats.ts)\n\nLadezustand + Service-Integration:\n\n```typescript\nexport function useCrmStats() {\n const stats = ref<CrmStats | null>(null);\n const loading = ref(false);\n const error = ref<string | null>(null);\n\n async function fetchStats() {\n loading.value = true;\n error.value = null;\n\n try {\n const data = await crmService.getCrmStats();\n stats.value = data;\n } catch (e: any) {\n error.value = e.message ?? \"Fehler beim Laden der Statistiken\";\n } finally {\n loading.value = false;\n }\n }\n\n return {\n stats,\n loading,\n error,\n fetchStats,\n };\n}\n```\n\n**Muster:**\n- Wrapping von Service-Aufrufen\n- Verwaltet Lade-/Fehlerzustände\n- Gibt reaktive Ref + Fetch-Funktion zurück\n- In Komponenten verwendet via `const { stats, loading } = useCrmStats()`\n\n### Seiten-Komponenten (CustomersListPage.vue)\n\n```vue\n<template>\n <div class=\"h-full flex flex-col gap-4 p-4\">\n <div class=\"flex items-center justify-between\">\n <h1 class=\"text-2xl font-semibold text-white\">Kundenliste</h1>\n <button class=\"kit-btn-primary\" @click=\"openCreateModal\">\n + Neuer Kunde\n </button>\n </div>\n\n <!-- Suche/Filter -->\n <input v-model=\"search\" type=\"text\" placeholder=\"Kunde suchen…\" />\n\n <!-- Grid -->\n <div class=\"grid grid-cols-4 gap-2\">\n <div\n v-for=\"c in pagedCustomers\"\n :key=\"c.id\"\n @click=\"openCustomer(c.id)\"\n >\n {{ c.name }}\n </div>\n </div>\n\n <!-- Pagination -->\n <div class=\"flex items-center justify-between\">\n <button :disabled=\"page === 1\" @click=\"page--\">← Zurück</button>\n <span>{{ page }} / {{ totalPages }}</span>\n <button :disabled=\"page === totalPages\" @click=\"page++\">Weiter →</button>\n </div>\n\n <CustomerForm v-if=\"showModal\" @close=\"showModal = false\" @saved=\"reload\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from \"vue\";\nimport { crmService } from \"../../services/crm.service\";\n\nconst emit = defineEmits<{\n (e: \"openCustomer\", id: string): void;\n (e: \"openDashboard\"): void;\n}>();\n\nconst customers = ref<Customer[]>([]);\nconst search = ref(\"\");\nconst page = ref(1);\nconst pageSize = 8;\n\nasync function load() {\n customers.value = await crmService.getCustomers();\n}\n\nonMounted(load);\n\nconst filtered = computed(() =>\n customers.value.filter(c =>\n c.name.toLowerCase().includes(search.value.toLowerCase())\n )\n);\n\nconst totalPages = computed(() =>\n Math.max(1, Math.ceil(filtered.value.length / pageSize))\n);\n\nconst pagedCustomers = computed(() => {\n const start = (page.value - 1) * pageSize;\n return filtered.value.slice(start, start + pageSize);\n});\n\nfunction openCustomer(id: string) {\n emit(\"openCustomer\", id);\n}\n</script>\n```\n\n**Muster:**\n- Seite emit Events zur übergeordneten Komponente (CrmApp.vue)\n- Verwendet Composables für Logik\n- Handhabt eigenen lokalen Zustand (Suche, Pagination)\n- Service-Aufrufe in onMounted\n- Computed für Filterung/Pagination\n\n### Typ-Definitionen (types/customer.ts)\n\n```typescript\nexport interface Customer {\n id: string;\n customer_number: string | null;\n name: string;\n email: string | null;\n phone: string | null;\n address: string | null;\n zip: string | null;\n city: string | null;\n country: string | null;\n notes: string | null;\n is_active: boolean;\n created_at: string;\n updated_at: string;\n}\n```\n\n**Muster:**\n- Alle Modul-Typen in dediziertem `types/`-Ordner\n- Exportierte Interfaces, keine Klassen\n- Nullable Felder mit `| null` gekennzeichnet\n- ISO-Datums-Strings für Daten\n\n### Komponenten-Barrel-Exports (components/index.ts)\n\n```typescript\n// Cards\nexport { default as ContactCard } from \"./contacts/ContactCard.vue\";\nexport { default as CustomerCard } from \"./customer/CustomerCard.vue\";\n\n// Forms\nexport { default as ContactForm } from \"./contacts/ContactForm.vue\";\nexport { default as CustomerForm } from \"./customer/CustomerForm.vue\";\n\n// Widgets\nexport * from \"./widgets\";\n```\n\n**Muster:**\n- Alle Komponenten aus Index exportiert\n- Ermöglicht `import { CustomerForm } from \"../../components\"`\n\n---\n\n## 6. API-Client-Setup (services/api/client.ts)\n\nZentralisierte Axios-Instanz:\n\n```typescript\nimport axios, { type AxiosInstance } from 'axios';\n\nconst API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';\n\nexport const apiClient: AxiosInstance = axios.create({\n baseURL: API_BASE_URL,\n headers: {\n 'Content-Type': 'application/json',\n },\n timeout: 30000,\n});\n\n// Request Interceptor (JWT Token würde hier hinzugefügt)\napiClient.interceptors.request.use(\n (config) => {\n // TODO: JWT Token hinzufügen\n return config;\n },\n (error) => Promise.reject(error)\n);\n\n// Response Interceptor (Fehlerbehandlung)\napiClient.interceptors.response.use(\n (response) => response,\n (error) => {\n // Globale Fehlerbehandlung (401, 403, 404, 500, etc)\n return Promise.reject(error);\n }\n);\n\nexport default apiClient;\n```\n\n**Verwendung in Services:**\n```typescript\nconst { data } = await api.get(\"/api/backoffice/crm/customers\");\n```\n\n**Funktionen:**\n- Einzelne Instanz wird app-weit geteilt\n- Base-URL aus Umgebungsvariablen\n- Bereit für JWT-Interceptor\n- Zentralisierte Fehlerbehandlung\n\n---\n\n## 7. Layout-System (AppLayout.vue)\n\nHaupt-Container mit Topbar, Window Host und Dock:\n\n```vue\n<template>\n <div class=\"w-screen h-screen overflow-hidden flex flex-col bg-bg-primary\">\n <!-- Topbar -->\n <KitTopbar />\n\n <!-- Haupt-Fensterbereich -->\n <div class=\"flex flex-1 overflow-hidden\">\n <WindowHost class=\"flex-1\" />\n </div>\n\n <!-- Dock (Unten) -->\n <KitDock />\n </div>\n</template>\n```\n\n### Dock-Komponente (Dock.vue)\n\nUntere Navigationsleiste zum Öffnen von Apps:\n\n```vue\n<script setup lang=\"ts\">\nconst dockItems = [\n { id: \"crm\", label: \"CRM\", icon: markRaw(Users) },\n { id: \"projects\", label: \"Projects\", icon: markRaw(Briefcase) },\n { id: \"time\", label: \"Time\", icon: markRaw(Timer) },\n // Weitere Items...\n];\n\nfunction openApp(appId: string) {\n openWindow(appId);\n}\n\nconst isActive = (appId: string) => {\n const winId = activeWindow.value;\n return windows.some(w => w.id === winId && w.appId === appId);\n};\n</script>\n```\n\n**Funktionen:**\n- Verwendet lucide-vue-next Icons (markRaw für Performance)\n- Integriert mit useAppManager\n- Zeigt aktiven Zustand, wenn App-Fenster geöffnet ist\n- Responsive Design (versteckt auf Mobilgeräten)\n\n### Window Host (WindowHost.vue)\n\nContainer für alle schwebenden Fenster:\n\n```vue\n<template>\n <div class=\"window-host\">\n <WindowFrame\n v-for=\"w in windows\"\n :key=\"w.id\"\n :win=\"w\"\n >\n <!-- App-Komponente wird hier gerendert -->\n <component :is=\"resolveComponent(w.appId)\" v-bind=\"w.props\" />\n </WindowFrame>\n </div>\n</template>\n```\n\n### Window Frame (WindowFrame.vue)\n\nIndividueller Fenster-Wrapper mit Titelleiste, Resize-Handle:\n\n```vue\n<template>\n <div class=\"window-frame\" :style=\"frameStyleString\" @mousedown=\"focus\">\n <!-- Titelleiste (verschiebbar) -->\n <div class=\"window-titlebar\" @mousedown.stop=\"startDrag\">\n <span>{{ win.title }}</span>\n <button @click.stop=\"close\">✕</button>\n </div>\n\n <!-- Inhalt -->\n <div class=\"window-content\">\n <slot />\n </div>\n\n <!-- Resize-Handle -->\n <div class=\"resize-handle\" @mousedown.stop=\"startResize\" />\n </div>\n</template>\n```\n\n**Funktionen:**\n- Absolut positioniert mit dynamischem x, y, width, height\n- Verschiebbare Titelleiste\n- Größenänderbar über Eckengriff\n- Schließen-Button\n- Fokus bei Klick (Z-Index-Verwaltung)\n\n---\n\n## 8. State Management (Pinia)\n\nAktuell ist Pinia eingerichtet, aber **minimal genutzt**. Die App setzt auf:\n- **Composables** für lokalen Zustand (useCrmNavigation, useCrmStats, etc.)\n- **Service-Schicht** für API-Aufrufe\n- **Reaktive Refs** für Komponentenzustand\n\n**Wann einen Pinia Store hinzufügen:**\n- Globaler App-Zustand (User, Auth, Theme)\n- Geteilter Zustand über mehrere Module\n- Komplexe Zustandsübergänge\n\n---\n\n## 9. Styling-System\n\n### Design-Tokens (styles/tokens.css)\n\nCSS Custom Properties für Konsistenz:\n\n```css\n:root {\n /* Farben */\n --color-bg-primary: #232223;\n --color-accent-primary: #ff9100;\n --color-text-primary: #ffffff;\n --color-text-secondary: rgba(255, 255, 255, 0.7);\n --color-border-light: rgba(255, 255, 255, 0.1);\n\n /* Typografie */\n --font-primary: \"Fira Sans\", sans-serif;\n --font-mono: \"JetBrains Mono\", monospace;\n\n /* Abstände */\n --space-xs: 4px;\n --space-sm: 8px;\n --space-md: 16px;\n --space-lg: 24px;\n --space-xl: 32px;\n\n /* Layout */\n --os-dock-height: 80px;\n --os-topbar-height: 48px;\n}\n```\n\n### Tailwind CSS\n\nVerwendet Tailwind v4 mit `@tailwindcss/vite` Plugin:\n\n```vue\n<div class=\"px-6 py-4 bg-white/5 border border-white/10 rounded-lg\">\n <h2 class=\"text-lg font-semibold text-white\">Titel</h2>\n</div>\n```\n\n**Verwendete Klassen:**\n- `bg-bg-primary`, `bg-bg-secondary`\n- `text-white`, `text-white/70`\n- `border-white/10`\n- Grid-Layouts: `grid grid-cols-4 gap-2`\n- Flex: `flex items-center justify-between`\n\n### Komponenten-Styles (styles/components/)\n\n```css\n/* button.css */\n.kit-btn-primary {\n @apply px-4 py-2 bg-accent-primary rounded hover:opacity-80;\n}\n\n.kit-btn-accent {\n @apply px-4 py-2 bg-white/10 rounded hover:bg-white/20;\n}\n\n.kit-input {\n @apply px-3 py-2 bg-white/5 border border-white/10 rounded;\n}\n\n.kit-label {\n @apply block text-sm font-medium text-white/70 mb-1;\n}\n```\n\n---\n\n## 10. Neues Modul erstellen - Schritt für Schritt\n\n### Schritt 1: Modul-Struktur erstellen\n\n```\nui/src/modules/mymodule/\n├── MyModuleApp.vue # Einstiegspunkt\n├── pages/\n│ ├── MyPage.vue\n│ └── index.ts\n├── components/\n│ ├── MyComponent.vue\n│ └── index.ts\n├── composables/\n│ └── useMyModuleNav.ts\n├── services/\n│ └── mymodule.service.ts\n└── types/\n └── mymodule.ts\n```\n\n### Schritt 2: Typ-Definitionen erstellen (types/mymodule.ts)\n\n```typescript\nexport interface MyResource {\n id: string;\n name: string;\n description: string | null;\n created_at: string;\n updated_at: string;\n}\n```\n\n### Schritt 3: Service-Schicht erstellen (services/mymodule.service.ts)\n\n```typescript\nimport api from \"@/services/api/client\";\nimport type { MyResource } from \"../types/mymodule\";\n\nexport const mymoduleService = {\n async getResources(): Promise<MyResource[]> {\n const { data } = await api.get(\"/api/mymodule/resources\");\n return data;\n },\n\n async getResource(id: string): Promise<MyResource> {\n const { data } = await api.get(`/api/mymodule/resources/${id}`);\n return data;\n },\n\n async createResource(payload: Partial<MyResource>) {\n return api.post(\"/api/mymodule/resources\", payload);\n },\n};\n```\n\n### Schritt 4: Navigations-Composable erstellen (composables/useMyModuleNav.ts)\n\n```typescript\nimport { ref } from \"vue\";\n\nexport type MyModuleView = \"list\" | \"detail\" | \"create\";\n\nexport function useMyModuleNav() {\n const view = ref<MyModuleView>(\"list\");\n const activeResourceId = ref<string | null>(null);\n\n function goList() {\n view.value = \"list\";\n activeResourceId.value = null;\n }\n\n function goDetail(resourceId: string) {\n activeResourceId.value = resourceId;\n view.value = \"detail\";\n }\n\n function goCreate() {\n view.value = \"create\";\n activeResourceId.value = null;\n }\n\n return {\n view,\n activeResourceId,\n goList,\n goDetail,\n goCreate,\n };\n}\n```\n\n### Schritt 5: Seiten erstellen\n\n```vue\n<!-- pages/ResourceListPage.vue -->\n<template>\n <div class=\"h-full flex flex-col gap-4 p-4\">\n <h1 class=\"text-2xl font-semibold text-white\">Ressourcen</h1>\n\n <button class=\"kit-btn-primary\" @click=\"openCreate\">\n + Neue Ressource\n </button>\n\n <div class=\"grid grid-cols-4 gap-2\">\n <div\n v-for=\"r in resources\"\n :key=\"r.id\"\n @click=\"openDetail(r.id)\"\n class=\"p-3 bg-white/5 rounded border border-white/10\"\n >\n {{ r.name }}\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted } from \"vue\";\nimport { mymoduleService } from \"../../services/mymodule.service\";\n\nconst emit = defineEmits<{\n (e: \"openDetail\", id: string): void;\n (e: \"openCreate\"): void;\n}>();\n\nconst resources = ref([]);\n\nonMounted(async () => {\n resources.value = await mymoduleService.getResources();\n});\n\nfunction openDetail(id: string) {\n emit(\"openDetail\", id);\n}\n\nfunction openCreate() {\n emit(\"openCreate\");\n}\n</script>\n```\n\n### Schritt 6: Modul-Einstiegspunkt erstellen (MyModuleApp.vue)\n\n```vue\n<script setup lang=\"ts\">\nimport { ResourceListPage } from \"./pages\";\nimport { useMyModuleNav } from \"./composables/useMyModuleNav\";\n\nconst { view, activeResourceId, goList, goDetail, goCreate } = useMyModuleNav();\n</script>\n\n<template>\n <div class=\"mymodule-app h-full\">\n <ResourceListPage\n v-if=\"view === 'list'\"\n @openDetail=\"goDetail\"\n @openCreate=\"goCreate\"\n />\n </div>\n</template>\n```\n\n### Schritt 7: Modul registrieren (layouts/app-manager/appRegistry.ts)\n\n```typescript\nimport MyModuleApp from \"@/modules/mymodule/MyModuleApp.vue\";\n\nexport const apps = [\n {\n id: \"crm\",\n title: \"CRM\",\n icons: markRaw(icons.Users),\n component: markRaw(CrmApp),\n window: { width: 1100, height: 700 }\n },\n {\n id: \"mymodule\",\n title: \"Mein Modul\",\n icons: markRaw(icons.Package),\n component: markRaw(MyModuleApp),\n window: { width: 900, height: 600 }\n },\n];\n```\n\n### Schritt 8: Dock-Item hinzufügen (layouts/components/Dock.vue)\n\n```typescript\nconst dockItems = [\n { id: \"crm\", label: \"CRM\", icon: markRaw(Users) },\n { id: \"mymodule\", label: \"Modul\", icon: markRaw(Package) }, // Hier hinzufügen\n { id: \"projects\", label: \"Projects\", icon: markRaw(Briefcase) },\n];\n```\n\n### Schritt 9 (Optional): Route hinzufügen (router/index.ts)\n\nFür Landing-Page oder eigenständige Route:\n\n```typescript\n{\n path: \"/app\",\n component: AppLayout,\n children: [\n {\n path: \"mymodule\",\n name: \"mymodule\",\n component: () => import(\"@/modules/mymodule/MyModuleApp.vue\"),\n },\n ],\n}\n```\n\n---\n\n## 11. Häufige Muster & Best Practices\n\n### Muster: Service + Composable\n\n**Service handhabt HTTP:**\n```typescript\n// crm.service.ts\nasync getCustomers(): Promise<Customer[]> {\n const { data } = await api.get(\"/api/crm/customers\");\n return data;\n}\n```\n\n**Composable handhabt Zustand + Fehler:**\n```typescript\n// useCrmStats.ts\nexport function useCrmStats() {\n const stats = ref<CrmStats | null>(null);\n const loading = ref(false);\n const error = ref<string | null>(null);\n\n async function fetchStats() {\n loading.value = true;\n try {\n stats.value = await crmService.getCrmStats();\n } catch (e) {\n error.value = e.message;\n } finally {\n loading.value = false;\n }\n }\n\n return { stats, loading, error, fetchStats };\n}\n```\n\n**Komponente verwendet Composable:**\n```vue\n<script setup>\nconst { stats, loading, error, fetchStats } = useCrmStats();\nonMounted(fetchStats);\n</script>\n\n<template>\n <div v-if=\"loading\">Lädt...</div>\n <div v-else-if=\"error\" class=\"text-red\">{{ error }}</div>\n <div v-else>{{ stats }}</div>\n</template>\n```\n\n### Muster: Modul-Navigation\n\n**Modul-Eintrag verwendet lokalen Zustand, nicht Router:**\n```typescript\nconst { view, activeId, goList, goDetail } = useModuleNav();\n```\n\n**Übergeordnete Komponente rendert bedingt:**\n```vue\n<ListPage v-if=\"view === 'list'\" @openDetail=\"goDetail\" />\n<DetailPage v-if=\"view === 'detail'\" :id=\"activeId\" @back=\"goList\" />\n```\n\n**Events propagieren nach oben:**\n```typescript\nconst emit = defineEmits<{\n (e: \"openCustomer\", id: string): void;\n (e: \"back\"): void;\n}>();\n```\n\n### Muster: Reaktive Formulare\n\n**Einfaches v-model Binding:**\n```vue\n<input v-model=\"form.name\" class=\"kit-input\" />\n<input v-model=\"form.email\" type=\"email\" class=\"kit-input\" />\n\n<script setup>\nconst form = ref({ name: \"\", email: \"\" });\n</script>\n```\n\n**Submit über Service:**\n```typescript\nasync function save() {\n loading.value = true;\n try {\n await crmService.createCustomer(form.value);\n emit(\"saved\");\n } finally {\n loading.value = false;\n }\n}\n```\n\n### Muster: Pagination\n\n```typescript\nconst page = ref(1);\nconst pageSize = 10;\n\nconst filtered = computed(() =>\n items.value.filter(i => i.name.includes(search.value))\n);\n\nconst totalPages = computed(() =>\n Math.max(1, Math.ceil(filtered.value.length / pageSize))\n);\n\nconst pagedItems = computed(() => {\n const start = (page.value - 1) * pageSize;\n return filtered.value.slice(start, start + pageSize);\n});\n```\n\n### Muster: Modal/Formular-Modal\n\n```vue\n<div v-if=\"showModal\" class=\"fixed inset-0 bg-black/60 flex items-center justify-center z-50\">\n <div class=\"bg-bg-secondary rounded-xl p-6\">\n <h2>{{ isEdit ? \"Bearbeiten\" : \"Erstellen\" }}</h2>\n\n <form @submit.prevent=\"save\">\n <input v-model=\"form.name\" />\n <button type=\"submit\">Speichern</button>\n <button @click=\"close\">Abbrechen</button>\n </form>\n </div>\n</div>\n```\n\n---\n\n## 12. Wichtige Dateien zum Ändern beim Hinzufügen eines Moduls\n\n1. **layouts/app-manager/appRegistry.ts** - App registrieren\n2. **layouts/components/Dock.vue** - Dock-Item hinzufügen\n3. **router/index.ts** (optional) - Route hinzufügen\n4. Modul-Ordner unter `modules/` erstellen\n\n---\n\n## 13. Umgebungs-Setup\n\n### .env\n```\nVITE_API_BASE_URL=http://localhost:8000\n```\n\n### vite.config.ts\n```typescript\nexport default defineConfig({\n plugins: [tailwindcss(), vue()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"src\"),\n },\n },\n server: {\n host: \"0.0.0.0\",\n port: 5173,\n },\n});\n```\n\n---\n\n## 14. Build & Ausführung\n\n```bash\n# Dependencies installieren\npnpm install\n\n# Entwicklung\npnpm run dev\n\n# Build\npnpm run build\n\n# Vorschau\npnpm run preview\n```\n\n---\n\n## 15. Technologie-Stack\n\n- **Vue 3** - Framework\n- **Vite** - Build-Tool (mit rolldown-vite)\n- **TypeScript** - Typ-Sicherheit\n- **Tailwind CSS 4** - Styling\n- **Vue Router 4** - Globales Routing\n- **Pinia 3** - State Management (eingerichtet, minimal genutzt)\n- **Axios** - HTTP-Client\n- **Lucide Vue Next** - Icons\n\n---\n\n## Zusammenfassung\n\nDas WorkmateOS Frontend verwendet eine **modulare fensterbasierte Architektur**, bei der:\n\n1. **Module eigenständig sind** mit eigenen Pages, Components, Services, Types\n2. **Composables lokalen Zustand verwalten** (Navigation, Datenabruf)\n3. **Services API-Aufrufe handhaben** (keine HTTP-Logik in Komponenten)\n4. **Types alles typsicher halten**\n5. **Fenstermanager** schwebende Fenster mit Drag/Resize ermöglicht\n6. **Design-Tokens + Tailwind** konsistentes Styling bieten\n7. **Kein komplexes Routing** - Module verwenden interne Ansichtswechsel\n\nUm ein neues Modul hinzuzufügen: Ordnerstruktur erstellen → Typen definieren → Service erstellen → Composables erstellen → Seiten erstellen → Modul-Einstiegspunkt erstellen → In appRegistry registrieren → Dock-Item hinzufügen.\n", "dir"=>"/wiki/frontend/", "name"=>"architecture.md", "path"=>"wiki/frontend/architecture.md", "url"=>"/wiki/frontend/architecture.html"} / </span>
<button :disabled="page === totalPages" @click="page++">Weiter →</button>
</div>
<CustomerForm v-if="showModal" @close="showModal = false" @saved="reload" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { crmService } from "../../services/crm.service";
const emit = defineEmits<{
(e: "openCustomer", id: string): void;
(e: "openDashboard"): void;
}>();
const customers = ref<Customer[]>([]);
const search = ref("");
const page = ref(1);
const pageSize = 8;
async function load() {
customers.value = await crmService.getCustomers();
}
onMounted(load);
const filtered = computed(() =>
customers.value.filter(c =>
c.name.toLowerCase().includes(search.value.toLowerCase())
)
);
const totalPages = computed(() =>
Math.max(1, Math.ceil(filtered.value.length / pageSize))
);
const pagedCustomers = computed(() => {
const start = (page.value - 1) * pageSize;
return filtered.value.slice(start, start + pageSize);
});
function openCustomer(id: string) {
emit("openCustomer", id);
}
</script>
Muster:
- Seite emit Events zur übergeordneten Komponente (CrmApp.vue)
- Verwendet Composables für Logik
- Handhabt eigenen lokalen Zustand (Suche, Pagination)
- Service-Aufrufe in onMounted
- Computed für Filterung/Pagination
Typ-Definitionen (types/customer.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export interface Customer {
id: string;
customer_number: string | null;
name: string;
email: string | null;
phone: string | null;
address: string | null;
zip: string | null;
city: string | null;
country: string | null;
notes: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
Muster:
- Alle Modul-Typen in dediziertem
types/-Ordner - Exportierte Interfaces, keine Klassen
- Nullable Felder mit
| nullgekennzeichnet - ISO-Datums-Strings für Daten
Komponenten-Barrel-Exports (components/index.ts)
1
2
3
4
5
6
7
8
9
10
// Cards
export { default as ContactCard } from "./contacts/ContactCard.vue";
export { default as CustomerCard } from "./customer/CustomerCard.vue";
// Forms
export { default as ContactForm } from "./contacts/ContactForm.vue";
export { default as CustomerForm } from "./customer/CustomerForm.vue";
// Widgets
export * from "./widgets";
Muster:
- Alle Komponenten aus Index exportiert
- Ermöglicht
import { CustomerForm } from "../../components"
6. API-Client-Setup (services/api/client.ts)
Zentralisierte Axios-Instanz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import axios, { type AxiosInstance } from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
export const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
timeout: 30000,
});
// Request Interceptor (JWT Token würde hier hinzugefügt)
apiClient.interceptors.request.use(
(config) => {
// TODO: JWT Token hinzufügen
return config;
},
(error) => Promise.reject(error)
);
// Response Interceptor (Fehlerbehandlung)
apiClient.interceptors.response.use(
(response) => response,
(error) => {
// Globale Fehlerbehandlung (401, 403, 404, 500, etc)
return Promise.reject(error);
}
);
export default apiClient;
Verwendung in Services:
1
const { data } = await api.get("/api/backoffice/crm/customers");
Funktionen:
- Einzelne Instanz wird app-weit geteilt
- Base-URL aus Umgebungsvariablen
- Bereit für JWT-Interceptor
- Zentralisierte Fehlerbehandlung
7. Layout-System (AppLayout.vue)
Haupt-Container mit Topbar, Window Host und Dock:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="w-screen h-screen overflow-hidden flex flex-col bg-bg-primary">
<!-- Topbar -->
<KitTopbar />
<!-- Haupt-Fensterbereich -->
<div class="flex flex-1 overflow-hidden">
<WindowHost class="flex-1" />
</div>
<!-- Dock (Unten) -->
<KitDock />
</div>
</template>
Dock-Komponente (Dock.vue)
Untere Navigationsleiste zum Öffnen von Apps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts">
const dockItems = [
{ id: "crm", label: "CRM", icon: markRaw(Users) },
{ id: "projects", label: "Projects", icon: markRaw(Briefcase) },
{ id: "time", label: "Time", icon: markRaw(Timer) },
// Weitere Items...
];
function openApp(appId: string) {
openWindow(appId);
}
const isActive = (appId: string) => {
const winId = activeWindow.value;
return windows.some(w => w.id === winId && w.appId === appId);
};
</script>
Funktionen:
- Verwendet lucide-vue-next Icons (markRaw für Performance)
- Integriert mit useAppManager
- Zeigt aktiven Zustand, wenn App-Fenster geöffnet ist
- Responsive Design (versteckt auf Mobilgeräten)
Window Host (WindowHost.vue)
Container für alle schwebenden Fenster:
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="window-host">
<WindowFrame
v-for="w in windows"
:key="w.id"
:win="w"
>
<!-- App-Komponente wird hier gerendert -->
<component :is="resolveComponent(w.appId)" v-bind="w.props" />
</WindowFrame>
</div>
</template>
Window Frame (WindowFrame.vue)
Individueller Fenster-Wrapper mit Titelleiste, Resize-Handle:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="window-frame" :style="frameStyleString" @mousedown="focus">
<!-- Titelleiste (verschiebbar) -->
<div class="window-titlebar" @mousedown.stop="startDrag">
<span></span>
<button @click.stop="close">✕</button>
</div>
<!-- Inhalt -->
<div class="window-content">
<slot />
</div>
<!-- Resize-Handle -->
<div class="resize-handle" @mousedown.stop="startResize" />
</div>
</template>
Funktionen:
- Absolut positioniert mit dynamischem x, y, width, height
- Verschiebbare Titelleiste
- Größenänderbar über Eckengriff
- Schließen-Button
- Fokus bei Klick (Z-Index-Verwaltung)
8. State Management (Pinia)
Aktuell ist Pinia eingerichtet, aber minimal genutzt. Die App setzt auf:
- Composables für lokalen Zustand (useCrmNavigation, useCrmStats, etc.)
- Service-Schicht für API-Aufrufe
- Reaktive Refs für Komponentenzustand
Wann einen Pinia Store hinzufügen:
- Globaler App-Zustand (User, Auth, Theme)
- Geteilter Zustand über mehrere Module
- Komplexe Zustandsübergänge
9. Styling-System
Design-Tokens (styles/tokens.css)
CSS Custom Properties für Konsistenz:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
:root {
/* Farben */
--color-bg-primary: #232223;
--color-accent-primary: #ff9100;
--color-text-primary: #ffffff;
--color-text-secondary: rgba(255, 255, 255, 0.7);
--color-border-light: rgba(255, 255, 255, 0.1);
/* Typografie */
--font-primary: "Fira Sans", sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* Abstände */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
/* Layout */
--os-dock-height: 80px;
--os-topbar-height: 48px;
}
Tailwind CSS
Verwendet Tailwind v4 mit @tailwindcss/vite Plugin:
1
2
3
<div class="px-6 py-4 bg-white/5 border border-white/10 rounded-lg">
<h2 class="text-lg font-semibold text-white">Titel</h2>
</div>
Verwendete Klassen:
bg-bg-primary,bg-bg-secondarytext-white,text-white/70border-white/10- Grid-Layouts:
grid grid-cols-4 gap-2 - Flex:
flex items-center justify-between
Komponenten-Styles (styles/components/)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* button.css */
.kit-btn-primary {
@apply px-4 py-2 bg-accent-primary rounded hover:opacity-80;
}
.kit-btn-accent {
@apply px-4 py-2 bg-white/10 rounded hover:bg-white/20;
}
.kit-input {
@apply px-3 py-2 bg-white/5 border border-white/10 rounded;
}
.kit-label {
@apply block text-sm font-medium text-white/70 mb-1;
}
10. Neues Modul erstellen - Schritt für Schritt
Schritt 1: Modul-Struktur erstellen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ui/src/modules/mymodule/
├── MyModuleApp.vue # Einstiegspunkt
├── pages/
│ ├── MyPage.vue
│ └── index.ts
├── components/
│ ├── MyComponent.vue
│ └── index.ts
├── composables/
│ └── useMyModuleNav.ts
├── services/
│ └── mymodule.service.ts
└── types/
└── mymodule.ts
Schritt 2: Typ-Definitionen erstellen (types/mymodule.ts)
1
2
3
4
5
6
7
export interface MyResource {
id: string;
name: string;
description: string | null;
created_at: string;
updated_at: string;
}
Schritt 3: Service-Schicht erstellen (services/mymodule.service.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import api from "@/services/api/client";
import type { MyResource } from "../types/mymodule";
export const mymoduleService = {
async getResources(): Promise<MyResource[]> {
const { data } = await api.get("/api/mymodule/resources");
return data;
},
async getResource(id: string): Promise<MyResource> {
const { data } = await api.get(`/api/mymodule/resources/${id}`);
return data;
},
async createResource(payload: Partial<MyResource>) {
return api.post("/api/mymodule/resources", payload);
},
};
Schritt 4: Navigations-Composable erstellen (composables/useMyModuleNav.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { ref } from "vue";
export type MyModuleView = "list" | "detail" | "create";
export function useMyModuleNav() {
const view = ref<MyModuleView>("list");
const activeResourceId = ref<string | null>(null);
function goList() {
view.value = "list";
activeResourceId.value = null;
}
function goDetail(resourceId: string) {
activeResourceId.value = resourceId;
view.value = "detail";
}
function goCreate() {
view.value = "create";
activeResourceId.value = null;
}
return {
view,
activeResourceId,
goList,
goDetail,
goCreate,
};
}
Schritt 5: Seiten erstellen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!-- pages/ResourceListPage.vue -->
<template>
<div class="h-full flex flex-col gap-4 p-4">
<h1 class="text-2xl font-semibold text-white">Ressourcen</h1>
<button class="kit-btn-primary" @click="openCreate">
+ Neue Ressource
</button>
<div class="grid grid-cols-4 gap-2">
<div
v-for="r in resources"
:key="r.id"
@click="openDetail(r.id)"
class="p-3 bg-white/5 rounded border border-white/10"
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { mymoduleService } from "../../services/mymodule.service";
const emit = defineEmits<{
(e: "openDetail", id: string): void;
(e: "openCreate"): void;
}>();
const resources = ref([]);
onMounted(async () => {
resources.value = await mymoduleService.getResources();
});
function openDetail(id: string) {
emit("openDetail", id);
}
function openCreate() {
emit("openCreate");
}
</script>
Schritt 6: Modul-Einstiegspunkt erstellen (MyModuleApp.vue)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts">
import { ResourceListPage } from "./pages";
import { useMyModuleNav } from "./composables/useMyModuleNav";
const { view, activeResourceId, goList, goDetail, goCreate } = useMyModuleNav();
</script>
<template>
<div class="mymodule-app h-full">
<ResourceListPage
v-if="view === 'list'"
@openDetail="goDetail"
@openCreate="goCreate"
/>
</div>
</template>
Schritt 7: Modul registrieren (layouts/app-manager/appRegistry.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import MyModuleApp from "@/modules/mymodule/MyModuleApp.vue";
export const apps = [
{
id: "crm",
title: "CRM",
icons: markRaw(icons.Users),
component: markRaw(CrmApp),
window: { width: 1100, height: 700 }
},
{
id: "mymodule",
title: "Mein Modul",
icons: markRaw(icons.Package),
component: markRaw(MyModuleApp),
window: { width: 900, height: 600 }
},
];
Schritt 8: Dock-Item hinzufügen (layouts/components/Dock.vue)
1
2
3
4
5
const dockItems = [
{ id: "crm", label: "CRM", icon: markRaw(Users) },
{ id: "mymodule", label: "Modul", icon: markRaw(Package) }, // Hier hinzufügen
{ id: "projects", label: "Projects", icon: markRaw(Briefcase) },
];
Schritt 9 (Optional): Route hinzufügen (router/index.ts)
Für Landing-Page oder eigenständige Route:
1
2
3
4
5
6
7
8
9
10
11
{
path: "/app",
component: AppLayout,
children: [
{
path: "mymodule",
name: "mymodule",
component: () => import("@/modules/mymodule/MyModuleApp.vue"),
},
],
}
11. Häufige Muster & Best Practices
Muster: Service + Composable
Service handhabt HTTP:
1
2
3
4
5
// crm.service.ts
async getCustomers(): Promise<Customer[]> {
const { data } = await api.get("/api/crm/customers");
return data;
}
Composable handhabt Zustand + Fehler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// useCrmStats.ts
export function useCrmStats() {
const stats = ref<CrmStats | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
async function fetchStats() {
loading.value = true;
try {
stats.value = await crmService.getCrmStats();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
return { stats, loading, error, fetchStats };
}
Komponente verwendet Composable:
1
2
3
4
5
6
7
8
9
10
<script setup>
const { stats, loading, error, fetchStats } = useCrmStats();
onMounted(fetchStats);
</script>
<template>
<div v-if="loading">Lädt...</div>
<div v-else-if="error" class="text-red"></div>
<div v-else></div>
</template>
Muster: Modul-Navigation
Modul-Eintrag verwendet lokalen Zustand, nicht Router:
1
const { view, activeId, goList, goDetail } = useModuleNav();
Übergeordnete Komponente rendert bedingt:
1
2
<ListPage v-if="view === 'list'" @openDetail="goDetail" />
<DetailPage v-if="view === 'detail'" :id="activeId" @back="goList" />
Events propagieren nach oben:
1
2
3
4
const emit = defineEmits<{
(e: "openCustomer", id: string): void;
(e: "back"): void;
}>();
Muster: Reaktive Formulare
Einfaches v-model Binding:
1
2
3
4
5
6
<input v-model="form.name" class="kit-input" />
<input v-model="form.email" type="email" class="kit-input" />
<script setup>
const form = ref({ name: "", email: "" });
</script>
Submit über Service:
1
2
3
4
5
6
7
8
9
async function save() {
loading.value = true;
try {
await crmService.createCustomer(form.value);
emit("saved");
} finally {
loading.value = false;
}
}
Muster: Pagination
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const page = ref(1);
const pageSize = 10;
const filtered = computed(() =>
items.value.filter(i => i.name.includes(search.value))
);
const totalPages = computed(() =>
Math.max(1, Math.ceil(filtered.value.length / pageSize))
);
const pagedItems = computed(() => {
const start = (page.value - 1) * pageSize;
return filtered.value.slice(start, start + pageSize);
});
Muster: Modal/Formular-Modal
1
2
3
4
5
6
7
8
9
10
11
<div v-if="showModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-bg-secondary rounded-xl p-6">
<h2></h2>
<form @submit.prevent="save">
<input v-model="form.name" />
<button type="submit">Speichern</button>
<button @click="close">Abbrechen</button>
</form>
</div>
</div>
12. Wichtige Dateien zum Ändern beim Hinzufügen eines Moduls
- layouts/app-manager/appRegistry.ts - App registrieren
- layouts/components/Dock.vue - Dock-Item hinzufügen
- router/index.ts (optional) - Route hinzufügen
- Modul-Ordner unter
modules/erstellen
13. Umgebungs-Setup
.env
1
VITE_API_BASE_URL=http://localhost:8000
vite.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
export default defineConfig({
plugins: [tailwindcss(), vue()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
server: {
host: "0.0.0.0",
port: 5173,
},
});
14. Build & Ausführung
1
2
3
4
5
6
7
8
9
10
11
# Dependencies installieren
pnpm install
# Entwicklung
pnpm run dev
# Build
pnpm run build
# Vorschau
pnpm run preview
15. Technologie-Stack
- Vue 3 - Framework
- Vite - Build-Tool (mit rolldown-vite)
- TypeScript - Typ-Sicherheit
- Tailwind CSS 4 - Styling
- Vue Router 4 - Globales Routing
- Pinia 3 - State Management (eingerichtet, minimal genutzt)
- Axios - HTTP-Client
- Lucide Vue Next - Icons
Zusammenfassung
Das WorkmateOS Frontend verwendet eine modulare fensterbasierte Architektur, bei der:
- Module eigenständig sind mit eigenen Pages, Components, Services, Types
- Composables lokalen Zustand verwalten (Navigation, Datenabruf)
- Services API-Aufrufe handhaben (keine HTTP-Logik in Komponenten)
- Types alles typsicher halten
- Fenstermanager schwebende Fenster mit Drag/Resize ermöglicht
- Design-Tokens + Tailwind konsistentes Styling bieten
- Kein komplexes Routing - Module verwenden interne Ansichtswechsel
Um ein neues Modul hinzuzufügen: Ordnerstruktur erstellen → Typen definieren → Service erstellen → Composables erstellen → Seiten erstellen → Modul-Einstiegspunkt erstellen → In appRegistry registrieren → Dock-Item hinzufügen.