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, component und 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

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 | null gekennzeichnet
  • 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-secondary
  • text-white, text-white/70
  • border-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

  1. layouts/app-manager/appRegistry.ts - App registrieren
  2. layouts/components/Dock.vue - Dock-Item hinzufügen
  3. router/index.ts (optional) - Route hinzufügen
  4. 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:

  1. Module eigenständig sind mit eigenen Pages, Components, Services, Types
  2. Composables lokalen Zustand verwalten (Navigation, Datenabruf)
  3. Services API-Aufrufe handhaben (keine HTTP-Logik in Komponenten)
  4. Types alles typsicher halten
  5. Fenstermanager schwebende Fenster mit Drag/Resize ermöglicht
  6. Design-Tokens + Tailwind konsistentes Styling bieten
  7. 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.