diff --git a/CONTACTS.md b/CONTACTS.md new file mode 100644 index 0000000..aa8fd92 --- /dev/null +++ b/CONTACTS.md @@ -0,0 +1,219 @@ +# Kontaktmodul + +## Übersicht +Das Kontaktmodul verwaltet Firmenkontakte (Debitoren/Kreditoren) mit bis zu 2 Ansprechpartnern pro Firma. + +## Entities + +### Contact +Hauptentity für Firmenkontakte mit folgenden Feldern: + +**Firmendaten:** +- `companyName` (Pflicht): Firmenname +- `companyNumber`: Kundennummer +- `street`: Straße +- `zipCode`: PLZ +- `city`: Ort +- `country`: Land +- `phone`: Telefon +- `fax`: Fax +- `email`: E-Mail +- `website`: Website +- `taxNumber`: Steuernummer +- `vatNumber`: USt-IdNr. + +**Klassifizierung:** +- `isDebtor`: Ist Debitor (boolean) +- `isCreditor`: Ist Kreditor (boolean) +- `isActive`: Ist aktiv (boolean) + +**Sonstiges:** +- `notes`: Notizen (Text) +- `contactPersons`: Collection von Ansprechpartnern (OneToMany) +- `createdAt`: Erstellungsdatum +- `updatedAt`: Änderungsdatum + +### ContactPerson +Ansprechpartner-Entity mit folgenden Feldern: + +- `salutation`: Anrede (Herr/Frau/Divers) +- `title`: Titel (z.B. Dr., Prof.) +- `firstName` (Pflicht): Vorname +- `lastName` (Pflicht): Nachname +- `position`: Position +- `department`: Abteilung +- `phone`: Telefon +- `mobile`: Mobil +- `email`: E-Mail +- `isPrimary`: Hauptansprechpartner (boolean) + +## API Endpoints + +### Kontakte +- `GET /api/contacts` - Alle Kontakte auflisten +- `GET /api/contacts/{id}` - Einzelnen Kontakt abrufen +- `POST /api/contacts` - Neuen Kontakt erstellen (erfordert ROLE_USER) +- `PUT /api/contacts/{id}` - Kontakt bearbeiten (erfordert ROLE_USER) +- `DELETE /api/contacts/{id}` - Kontakt löschen (erfordert ROLE_ADMIN) + +### Request/Response Format + +**Kontakt erstellen/bearbeiten:** +```json +{ + "companyName": "Beispiel GmbH", + "companyNumber": "K-12345", + "street": "Musterstraße 123", + "zipCode": "12345", + "city": "Berlin", + "country": "Deutschland", + "phone": "+49 30 12345678", + "fax": "+49 30 12345679", + "email": "info@beispiel.de", + "website": "https://www.beispiel.de", + "taxNumber": "123/456/78901", + "vatNumber": "DE123456789", + "isDebtor": true, + "isCreditor": false, + "isActive": true, + "notes": "Wichtiger Kunde", + "contactPersons": [ + { + "salutation": "Herr", + "title": "Dr.", + "firstName": "Max", + "lastName": "Mustermann", + "position": "Geschäftsführer", + "department": "Management", + "phone": "+49 30 12345680", + "mobile": "+49 170 1234567", + "email": "max.mustermann@beispiel.de", + "isPrimary": true + } + ] +} +``` + +## Repository-Methoden + +### ContactRepository + +**findByType(?bool $isDebtor, ?bool $isCreditor): array** +- Filtert Kontakte nach Typ (Debitor/Kreditor) +- Gibt nur aktive Kontakte zurück +- Sortiert nach Firmennamen + +**search(string $searchTerm): array** +- Durchsucht Kontakte nach: + - Firmenname + - E-Mail + - Stadt + - Ansprechpartner (Vor-/Nachname, E-Mail) + +## Vue.js Komponente + +### ContactManagement.vue + +**Features:** +- DataTable mit Pagination (10/25/50 Einträge) +- Globale Suche über alle Felder +- Filter nach Typ (Alle/Debitoren/Kreditoren) +- Sortierung nach Spalten +- CRUD-Operationen über Dialoge +- Bis zu 2 Ansprechpartner pro Kontakt +- Validierung (Pflichtfelder, E-Mail-Format, URL-Format) +- Responsive Design (mobile + desktop) +- Toast-Benachrichtigungen für Erfolg/Fehler +- Bestätigungsdialog beim Löschen + +**Verwendete PrimeVue Komponenten:** +- DataTable, Column +- Dialog +- Card +- Button +- InputText, Textarea +- Checkbox, Dropdown +- Tag +- Divider +- IconField, InputIcon + +## Berechtigungen + +- **ROLE_USER**: Kann Kontakte ansehen, erstellen und bearbeiten +- **ROLE_ADMIN**: Kann zusätzlich Kontakte löschen + +## Navigation + +Das Kontaktmodul ist über den Menüpunkt "Kontakte" erreichbar (Route: `/contacts`). + +## Datenbank-Tabellen + +### contacts +```sql +CREATE TABLE contacts ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_name VARCHAR(255) NOT NULL, + company_number VARCHAR(50), + street TEXT, + zip_code VARCHAR(20), + city VARCHAR(100), + country VARCHAR(100), + phone VARCHAR(50), + fax VARCHAR(50), + email VARCHAR(180), + website VARCHAR(255), + tax_number VARCHAR(50), + vat_number VARCHAR(50), + is_debtor TINYINT(1) DEFAULT 0, + is_creditor TINYINT(1) DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + notes TEXT, + created_at DATETIME NOT NULL, + updated_at DATETIME, + INDEX idx_company_name (company_name), + INDEX idx_city (city), + INDEX idx_is_debtor (is_debtor), + INDEX idx_is_creditor (is_creditor), + INDEX idx_is_active (is_active) +); +``` + +### contact_persons +```sql +CREATE TABLE contact_persons ( + id INT AUTO_INCREMENT PRIMARY KEY, + contact_id INT NOT NULL, + salutation VARCHAR(20), + title VARCHAR(100), + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + position VARCHAR(100), + department VARCHAR(100), + phone VARCHAR(50), + mobile VARCHAR(50), + email VARCHAR(180), + is_primary TINYINT(1) DEFAULT 0, + FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE, + INDEX idx_contact_id (contact_id), + INDEX idx_is_primary (is_primary) +); +``` + +## Best Practices + +1. **Ansprechpartner-Limit**: Maximal 2 Ansprechpartner pro Kontakt (UI-Limitierung) +2. **Cascade Delete**: Beim Löschen eines Kontakts werden auch alle Ansprechpartner gelöscht +3. **Timestamps**: Automatische Verwaltung von `createdAt` und `updatedAt` +4. **Validierung**: Alle Validierungen erfolgen sowohl im Backend (Symfony Validator) als auch im Frontend (Vue.js) +5. **Session-basierte API**: Alle API-Aufrufe verwenden `credentials: 'include'` für Session-Authentifizierung + +## Erweiterungsmöglichkeiten + +- Aktivitäten-Tracking (Anrufe, E-Mails, Meetings) +- Dokumente-Verwaltung +- Deals/Opportunities +- Import/Export (CSV, Excel) +- Duplikatserkennung +- Kontakt-Historie +- Tags/Labels +- Mehr als 2 Ansprechpartner (aktuell UI-limitiert) diff --git a/README.md b/README.md index 5f9c043..250ad56 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,16 @@ Eine moderne, modulare CRM-Anwendung basierend auf Symfony 7.1 LTS, Vue.js 3 und - **Symfony 7.1 LTS** - Stabile PHP-Backend-Framework - **Vue.js 3** - Modernes, reaktives Frontend mit Composition API -- **PrimeVue** - Professionelle UI-Komponenten (DataTable, Charts, Forms) +- **PrimeVue 4** - Professionelle UI-Komponenten (DataTable, Charts, Forms) +- **Sakai Template** - Professional Admin Layout mit Dark Mode +- **Tailwind CSS v4** - Utility-First CSS Framework - **API Platform** - RESTful API mit OpenAPI-Dokumentation - **MariaDB** - Zuverlässige relationale Datenbank - **Webpack Encore** - Asset-Management und Hot Module Replacement - **Modulares Berechtigungssystem** - Flexible Rollen mit Modul-basierter Rechteverwaltung - **User CRUD** - Vollständige Benutzerverwaltung via API Platform - **Login-System** - Form-basierte Authentifizierung mit Remember Me +- **Kontaktmodul** - Firmenstammdaten mit Ansprechpersonen und Debitor/Kreditor-Klassifizierung ## 📋 Voraussetzungen @@ -101,18 +104,23 @@ npm run watch /assets /js /components - Wiederverwendbare Vue-Komponenten - /views - Page-Level Vue-Komponenten + /views - Page-Level Vue-Komponenten (ContactManagement, UserManagement, etc.) + /layout - Sakai Layout-Komponenten (AppLayout, AppTopbar, AppSidebar, AppMenu) /composables - Vue Composition API Functions /stores - Pinia State Management /api - API Client Wrapper - /styles - SCSS/CSS Styles + /styles + /layout - Sakai SCSS (Topbar, Sidebar, Menu, Footer, Responsive, Animations) + tailwind.css - Tailwind CSS v4 mit PrimeUI Plugin + sakai.scss - Sakai Layout Imports /config - Symfony-Konfiguration /src /Controller - HTTP Controllers - /Entity - Doctrine Entities + /Entity - Doctrine Entities (User, Role, Contact, ContactPerson) /Repository - Database Queries /Service - Business Logic /Security/Voter - Permission Logic + /DataFixtures - Test-Daten (200 Kontakte mit realistischen deutschen Daten) /templates - Twig Templates /public - Public Assets & Entry Point /migrations - Doctrine Migrations @@ -131,7 +139,9 @@ npm run watch - **Vue.js 3** - Progressive JavaScript Framework - **Vue Router** - SPA Navigation - **Pinia** - State Management -- **PrimeVue** - UI Component Library +- **PrimeVue 4** - UI Component Library mit Aura Theme +- **Sakai Template** - Professional Admin Layout (von PrimeFaces) +- **Tailwind CSS v4** - Utility-First CSS Framework mit PrimeUI Plugin - **Webpack Encore** - Asset Bundler ### Database @@ -140,11 +150,18 @@ npm run watch ## 📱 Module - **Dashboard** - Übersicht und KPIs -- **Kontakte** - Kontaktverwaltung mit Status-Tracking (in Entwicklung) -- **Unternehmen** - Firmendatenbank (in Entwicklung) +- **Kontakte** - Firmenstammdatenverwaltung mit Ansprechpersonen (✅ implementiert) + - Vollständige CRUD-Operationen via API Platform + - Server-seitige Filterung (Debitoren/Kreditoren/Status) + - 200 Test-Fixtures mit realistischen deutschen Firmendaten + - Strukturiertes Formular in 6 Kategorien (Basisdaten, Adresse, Kontaktdaten, Steuerdaten, Ansprechpartner, Notizen) + - Bis zu 2 Ansprechpersonen pro Firma + - Debitor/Kreditor-Klassifizierung (beide möglich) + - Aktiv/Inaktiv-Status - **Deals** - Sales-Pipeline Management (in Entwicklung) - **Aktivitäten** - Interaktions-Historie (in Entwicklung) - **Benutzerverwaltung** - CRUD für User (✅ implementiert) +- **Rollenverwaltung** - Modulare Berechtigungen (✅ implementiert) ## 🔐 Sicherheit @@ -197,20 +214,31 @@ Dein Team --- -**Status:** ✅ Grundsystem implementiert - Ready for CRM-Module! +**Status:** ✅ Grundsystem + Kontaktmodul implementiert - Ready for weitere CRM-Module! **Implementiert:** -- ✅ Projekt-Setup (Symfony 7.1 + Vue.js 3 + PrimeVue) +- ✅ Projekt-Setup (Symfony 7.1 + Vue.js 3 + PrimeVue 4) +- ✅ Sakai Template Integration (Professional Admin Layout) +- ✅ Tailwind CSS v4 mit PrimeUI Plugin - ✅ Modulares Berechtigungssystem (User, Role, Module, RolePermission) - ✅ Login-System mit Remember Me - ✅ User-CRUD mit API Platform +- ✅ **Kontaktmodul mit Firmenstammdaten** + - Contact & ContactPerson Entities + - API Platform REST Endpoints mit Filtern + - Vue.js ContactManagement Component + - Strukturiertes Formular (6 Kategorien) + - 200 Test-Fixtures mit deutschen Daten + - Server-seitige Filterung & Pagination - ✅ Vue.js Frontend mit PrimeVue DataTable, Dialogs, Forms - ✅ Password-Hashing via State Processor -- ✅ Admin-Navigation und Schutz +- ✅ Dark Mode Support +- ✅ Responsive Design mit Sakai Layout **Next Steps:** -1. Contact-Entity erstellen: `php bin/console make:entity Contact` -2. Company-Entity erstellen: `php bin/console make:entity Company` -3. Deal-Entity mit Pipeline-Stages erstellen -4. Activity-Entity für Interaktionshistorie -5. Vue.js-Komponenten für Contact/Company/Deal-Management +1. Deal-Entity mit Pipeline-Stages erstellen +2. Activity-Entity für Interaktionshistorie +3. Dashboard mit KPIs und Charts +4. Reporting-Modul +5. E-Mail-Integration +6. Kalender/Termine diff --git a/assets/app.js b/assets/app.js index 7d3b5d9..e206373 100644 --- a/assets/app.js +++ b/assets/app.js @@ -5,14 +5,20 @@ import './bootstrap.js'; * This file will be included onto the page via the importmap() Twig function, * which should already be in your base.html.twig. */ -import './styles/app.scss'; +import './styles/tailwind.css'; +import './styles/sakai.scss'; import { createApp } from 'vue'; import { createPinia } from 'pinia'; import PrimeVue from 'primevue/config'; import Aura from '@primevue/themes/aura'; +import Lara from '@primevue/themes/lara'; +import Nora from '@primevue/themes/nora'; +import { $t, updatePreset, updateSurfacePalette } from '@primeuix/themes'; import ToastService from 'primevue/toastservice'; +import Toast from 'primevue/toast'; import Tooltip from 'primevue/tooltip'; +import StyleClass from 'primevue/styleclass'; import router from './js/router'; import App from './js/App.vue'; import { useAuthStore } from './js/stores/auth'; @@ -25,22 +31,115 @@ console.log('This log comes from assets/app.js - welcome to myCRM!'); const app = createApp(App); const pinia = createPinia(); +// Load saved theme preferences +const savedPreset = localStorage.getItem('preset') || 'Aura'; +const savedPrimary = localStorage.getItem('primaryColor') || 'emerald'; +const savedSurface = localStorage.getItem('surfaceColor'); + +const presets = { Aura, Lara, Nora }; +const primaryColors = { + noir: {}, + emerald: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' }, + green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' }, + lime: { 50: '#f7fee7', 100: '#ecfccb', 200: '#d9f99d', 300: '#bef264', 400: '#a3e635', 500: '#84cc16', 600: '#65a30d', 700: '#4d7c0f', 800: '#3f6212', 900: '#365314', 950: '#1a2e05' }, + orange: { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' }, + amber: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03' }, + yellow: { 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047', 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207', 800: '#854d0e', 900: '#713f12', 950: '#422006' }, + teal: { 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' }, + cyan: { 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9', 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490', 800: '#155e75', 900: '#164e63', 950: '#083344' }, + sky: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49' }, + blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' }, + indigo: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b' }, + violet: { 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd', 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9', 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065' }, + purple: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' }, + fuchsia: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' }, + pink: { 50: '#fdf2f8', 100: '#fce7f3', 200: '#fbcfe8', 300: '#f9a8d4', 400: '#f472b6', 500: '#ec4899', 600: '#db2777', 700: '#be185d', 800: '#9d174d', 900: '#831843', 950: '#500724' }, + rose: { 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' } +}; + +const surfaces = { + slate: { 0: '#ffffff', 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' }, + gray: { 0: '#ffffff', 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' }, + zinc: { 0: '#ffffff', 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' }, + neutral: { 0: '#ffffff', 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' }, + stone: { 0: '#ffffff', 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' }, + soho: { 0: '#ffffff', 50: '#f4f4f4', 100: '#e8e9e9', 200: '#d2d2d4', 300: '#bbbcbe', 400: '#a5a5a9', 500: '#8e8f93', 600: '#77787d', 700: '#616268', 800: '#4a4b52', 900: '#34343d', 950: '#1d1e27' }, + viva: { 0: '#ffffff', 50: '#f3f3f3', 100: '#e7e7e8', 200: '#cfd0d0', 300: '#b7b8b9', 400: '#9fa1a1', 500: '#87898a', 600: '#6e7173', 700: '#565a5b', 800: '#3e4244', 900: '#262b2c', 950: '#0e1315' }, + ocean: { 0: '#ffffff', 50: '#fbfcfc', 100: '#F7F9F8', 200: '#EFF3F2', 300: '#DADEDD', 400: '#B1B7B6', 500: '#828787', 600: '#5F7274', 700: '#415B61', 800: '#29444E', 900: '#183240', 950: '#0c1920' } +}; + +function getPrimaryPreset(primaryName) { + const palette = primaryColors[primaryName]; + + if (primaryName === 'noir') { + return { + semantic: { + primary: { + 50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', 300: '{surface.300}', + 400: '{surface.400}', 500: '{surface.500}', 600: '{surface.600}', 700: '{surface.700}', + 800: '{surface.800}', 900: '{surface.900}', 950: '{surface.950}' + }, + colorScheme: { + light: { + primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' }, + highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' } + }, + dark: { + primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' }, + highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' } + } + } + } + }; + } + + return { + semantic: { + primary: palette, + colorScheme: { + light: { + primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' }, + highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' } + }, + dark: { + primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' }, + highlight: { background: 'color-mix(in srgb, {primary.400}, transparent 84%)', focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)', color: 'rgba(255,255,255,.87)', focusColor: 'rgba(255,255,255,.87)' } + } + } + } + }; +} + app.use(pinia); app.use(router); app.use(PrimeVue, { theme: { - preset: Aura, + preset: presets[savedPreset], options: { - darkModeSelector: false, + darkModeSelector: '.app-dark', cssLayer: false } } }); app.use(ToastService); app.directive('tooltip', Tooltip); +app.directive('styleclass', StyleClass); +app.component('Toast', Toast); app.mount('#app'); +// Apply saved theme colors after mount +if (savedPrimary || savedSurface) { + setTimeout(() => { + if (savedPrimary) { + updatePreset(getPrimaryPreset(savedPrimary)); + } + if (savedSurface && surfaces[savedSurface]) { + updateSurfacePalette(surfaces[savedSurface]); + } + }, 0); +} + // Initialize auth store with user data from backend const authStore = useAuthStore(); authStore.initializeFromElement(); diff --git a/assets/js/App.vue b/assets/js/App.vue index 909cedd..0b30b61 100644 --- a/assets/js/App.vue +++ b/assets/js/App.vue @@ -1,100 +1,16 @@ - - - - - - myCRM - - - - - - - - - - - Dashboard - - - Kontakte - - - Unternehmen - - - Deals - - - - - - Admin - - - - - Benutzerverwaltung - - - Rollenverwaltung - - - Einstellungen - - - - - - - {{ authStore.fullName }} - - Logout - - - - - - - - - - - + + diff --git a/assets/js/layout/AppMenuItem.vue b/assets/js/layout/AppMenuItem.vue new file mode 100644 index 0000000..65ae9a0 --- /dev/null +++ b/assets/js/layout/AppMenuItem.vue @@ -0,0 +1,92 @@ + + + + + {{ item.label }} + + + {{ item.label }} + + + + + {{ item.label }} + + + + + + + + + + + diff --git a/assets/js/layout/AppSidebar.vue b/assets/js/layout/AppSidebar.vue new file mode 100644 index 0000000..0c10c84 --- /dev/null +++ b/assets/js/layout/AppSidebar.vue @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/assets/js/layout/AppTopbar.vue b/assets/js/layout/AppTopbar.vue new file mode 100644 index 0000000..05a04e6 --- /dev/null +++ b/assets/js/layout/AppTopbar.vue @@ -0,0 +1,96 @@ + + + + + + + + + + myCRM + + + + + + + + + + + + + + + + + + + + + + + + + {{ authStore.user?.email?.split('@')[0] }} + + + + + {{ (authStore.user?.fullName || authStore.user?.email || 'U').charAt(0).toUpperCase() }} + + + + {{ authStore.user?.fullName || authStore.user?.email?.split('@')[0] }} + + + {{ authStore.user?.email }} + + + + + + + + {{ authStore.user?.roles?.join(', ') || 'Keine Rolle' }} + + + + + + Mitglied seit {{ new Date(authStore.user.createdAt).toLocaleDateString('de-DE') }} + + + + + + + + Logout + + + + + diff --git a/assets/js/layout/composables/layout.js b/assets/js/layout/composables/layout.js new file mode 100644 index 0000000..6daae71 --- /dev/null +++ b/assets/js/layout/composables/layout.js @@ -0,0 +1,87 @@ +import { computed, reactive } from 'vue'; + +// Load preferences from localStorage +const savedDarkMode = localStorage.getItem('darkMode') === 'true'; +const savedPrimary = localStorage.getItem('primaryColor') || 'emerald'; +const savedSurface = localStorage.getItem('surfaceColor') || null; +const savedPreset = localStorage.getItem('preset') || 'Aura'; +const savedMenuMode = localStorage.getItem('menuMode') || 'static'; + +const layoutConfig = reactive({ + preset: savedPreset, + primary: savedPrimary, + surface: savedSurface, + darkTheme: savedDarkMode, + menuMode: savedMenuMode +}); + +// Apply dark mode on initial load +if (savedDarkMode) { + document.documentElement.classList.add('app-dark'); +} + +const layoutState = reactive({ + staticMenuDesktopInactive: false, + overlayMenuActive: false, + profileSidebarVisible: false, + configSidebarVisible: false, + staticMenuMobileActive: false, + menuHoverActive: false, + activeMenuItem: null +}); + +export function useLayout() { + const setActiveMenuItem = (item) => { + layoutState.activeMenuItem = item.value || item; + }; + + const toggleDarkMode = () => { + if (!document.startViewTransition) { + executeDarkModeToggle(); + + return; + } + + document.startViewTransition(() => executeDarkModeToggle(event)); + }; + + const executeDarkModeToggle = () => { + layoutConfig.darkTheme = !layoutConfig.darkTheme; + document.documentElement.classList.toggle('app-dark'); + + // Save preference to localStorage + localStorage.setItem('darkMode', layoutConfig.darkTheme.toString()); + }; + + const toggleMenu = () => { + if (layoutConfig.menuMode === 'overlay') { + layoutState.overlayMenuActive = !layoutState.overlayMenuActive; + } + + if (window.innerWidth > 991) { + layoutState.staticMenuDesktopInactive = !layoutState.staticMenuDesktopInactive; + } else { + layoutState.staticMenuMobileActive = !layoutState.staticMenuMobileActive; + } + }; + + const isSidebarActive = computed(() => layoutState.overlayMenuActive || layoutState.staticMenuMobileActive); + + const isDarkTheme = computed(() => layoutConfig.darkTheme); + + const getPrimary = computed(() => layoutConfig.primary); + + const getSurface = computed(() => layoutConfig.surface); + + return { + layoutConfig, + layoutState, + toggleMenu, + isSidebarActive, + isDarkTheme, + getPrimary, + getSurface, + setActiveMenuItem, + toggleDarkMode + }; +} diff --git a/assets/js/router.js b/assets/js/router.js index 9720a25..8702bf4 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -1,17 +1,13 @@ import { createRouter, createWebHistory } from 'vue-router'; import Dashboard from './views/Dashboard.vue'; -import ContactList from './views/ContactList.vue'; -import CompanyList from './views/CompanyList.vue'; -import DealList from './views/DealList.vue'; +import ContactManagement from './views/ContactManagement.vue'; import UserManagement from './views/UserManagement.vue'; import RoleManagement from './views/RoleManagement.vue'; import SettingsManagement from './views/SettingsManagement.vue'; const routes = [ { path: '/', name: 'dashboard', component: Dashboard }, - { path: '/contacts', name: 'contacts', component: ContactList }, - { path: '/companies', name: 'companies', component: CompanyList }, - { path: '/deals', name: 'deals', component: DealList }, + { path: '/contacts', name: 'contacts', component: ContactManagement }, { path: '/users', name: 'users', component: UserManagement, meta: { requiresAdmin: true } }, { path: '/roles', name: 'roles', component: RoleManagement, meta: { requiresAdmin: true } }, { path: '/settings', name: 'settings', component: SettingsManagement, meta: { requiresAdmin: true } }, diff --git a/assets/js/views/ContactManagement.vue b/assets/js/views/ContactManagement.vue new file mode 100644 index 0000000..5c0ee6e --- /dev/null +++ b/assets/js/views/ContactManagement.vue @@ -0,0 +1,789 @@ + + + + Kontakte + + + + + + + + + + + + + + + + + + + + + + + + + {{ data.companyName }} + + Nr: {{ data.companyNumber }} + + + + + + + + {{ data.zipCode }} {{ data.city }} + + + {{ data.country }} + + + + + + + + + + {{ person.firstName }} {{ person.lastName }} + + + + {{ person.email }} + + + + Keine Ansprechpartner + + + + + + + + + + + + + + + + + + + + + + + {{ data.phone }} + + + {{ data.email }} + + + + {{ data.website }} + + + + + + + + + + + + + + + Keine Kontakte gefunden. + + + + + + + + + + + + + Basisdaten + + + + + + + Firmenname * + + + + Firmenname ist erforderlich + + + + + Kundennummer + + + + + + + + Debitor + + + + Kreditor + + + + Aktiv + + + + + + + + + + + + + Adresse + + + + + + Straße + + + + + PLZ + + + + + Ort + + + + + Land + + + + + + + + + + + + Kontaktdaten + + + + + + Telefon + + + + + Fax + + + + + E-Mail + + + + + Website + + + + + + + + + + + + Steuerdaten + + + + + + Steuernummer + + + + + USt-IdNr. + + + + + + + + + + + + + Ansprechpartner + + + + + + + + Noch keine Ansprechpartner hinzugefügt + + + + + + + + + Ansprechpartner {{ index + 1 }} + + + + + + + + Anrede + + + + + + Titel + + + + + + Vorname * + + + + + + + Nachname * + + + + + + + Position + + + + + + + Abteilung + + + + + + + Telefon + + + + + + Mobil + + + + + + E-Mail + + + + + + + + + Hauptansprechpartner + + + + + + + + + + + + + + Notizen + + + + + + + + + + + + + + + + + + + + Möchten Sie den Kontakt {{ editingContact.companyName }} wirklich löschen? + + + + + + + + + + + + + diff --git a/assets/styles/app.css b/assets/styles/app.css deleted file mode 100644 index dd6181a..0000000 --- a/assets/styles/app.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - background-color: skyblue; -} diff --git a/assets/styles/app.scss b/assets/styles/app.scss deleted file mode 100644 index 7c5f505..0000000 --- a/assets/styles/app.scss +++ /dev/null @@ -1,145 +0,0 @@ -/* Global styles for myCRM */ - -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background: #f9fafb; - color: #1f2937; - overflow-x: hidden; -} - -#app { - width: 100%; - min-height: 100vh; -} - -/* PrimeVue Component Styling Fixes */ -.p-card { - background: white; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - - .p-card-title { - color: #1f2937; - font-weight: 600; - font-size: 1rem; - - @media (min-width: 768px) { - font-size: 1.125rem; - } - } - - .p-card-content { - color: #4b5563; - font-size: 0.875rem; - - @media (min-width: 768px) { - font-size: 1rem; - } - } -} - -.p-datatable { - .p-datatable-header { - background: white; - border: 1px solid #e5e7eb; - } - - .p-datatable-thead > tr > th { - background: #f9fafb; - color: #374151; - border: 1px solid #e5e7eb; - font-weight: 600; - } - - .p-datatable-tbody > tr { - background: white; - color: #1f2937; - - &:hover { - background: #f9fafb; - } - - > td { - border: 1px solid #e5e7eb; - } - } -} - -.p-button { - &.p-button-info { - background: #3b82f6; - border-color: #3b82f6; - - &:hover { - background: #2563eb; - border-color: #2563eb; - } - } - - &.p-button-danger { - background: #ef4444; - border-color: #ef4444; - - &:hover { - background: #dc2626; - border-color: #dc2626; - } - } -} - -.p-dialog { - .p-dialog-header { - background: white; - color: #1f2937; - } - - .p-dialog-content { - background: white; - color: #1f2937; - } -} - -.p-inputtext { - background: white; - color: #1f2937; - border: 1px solid #d1d5db; - - &:enabled:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25); - } -} - -.p-checkbox { - .p-checkbox-box { - background: white; - border: 1px solid #d1d5db; - - &.p-highlight { - background: #3b82f6; - border-color: #3b82f6; - } - } -} - -.p-tag { - &.p-tag-success { - background: #10b981; - } - - &.p-tag-danger { - background: #ef4444; - } - - &.p-tag-info { - background: #93c5fd; - color: #1e3a8a; - } -} diff --git a/assets/styles/layout/_animations.scss b/assets/styles/layout/_animations.scss new file mode 100644 index 0000000..01bb16a --- /dev/null +++ b/assets/styles/layout/_animations.scss @@ -0,0 +1,29 @@ +// Animation keyframes for StyleClass directive +@keyframes scalein { + 0% { + opacity: 0; + transform: scaleY(0.8); + transition: transform 0.12s cubic-bezier(0, 0, 0.2, 1), opacity 0.12s cubic-bezier(0, 0, 0.2, 1); + } + 100% { + opacity: 1; + transform: scaleY(1); + } +} + +@keyframes fadeout { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.animate-scalein { + animation: scalein 0.15s linear; +} + +.animate-fadeout { + animation: fadeout 0.15s linear; +} diff --git a/assets/styles/layout/_core.scss b/assets/styles/layout/_core.scss new file mode 100644 index 0000000..672a6bf --- /dev/null +++ b/assets/styles/layout/_core.scss @@ -0,0 +1,24 @@ +html { + height: 100%; + font-size: 14px; + line-height: 1.2; +} + +body { + font-family: 'Lato', sans-serif; + color: var(--text-color); + background-color: var(--surface-ground); + margin: 0; + padding: 0; + min-height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + text-decoration: none; +} + +.layout-wrapper { + min-height: 100vh; +} diff --git a/assets/styles/layout/_footer.scss b/assets/styles/layout/_footer.scss new file mode 100644 index 0000000..27bcbf0 --- /dev/null +++ b/assets/styles/layout/_footer.scss @@ -0,0 +1,8 @@ +.layout-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 0 1rem 0; + gap: 0.5rem; + border-top: 1px solid var(--surface-border); +} diff --git a/assets/styles/layout/_main.scss b/assets/styles/layout/_main.scss new file mode 100644 index 0000000..a223ac1 --- /dev/null +++ b/assets/styles/layout/_main.scss @@ -0,0 +1,13 @@ +.layout-main-container { + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; + padding: 6rem 2rem 0 2rem; + transition: margin-left var(--layout-section-transition-duration); +} + +.layout-main { + flex: 1 1 auto; + padding-bottom: 2rem; +} diff --git a/assets/styles/layout/_menu.scss b/assets/styles/layout/_menu.scss new file mode 100644 index 0000000..3d22fb5 --- /dev/null +++ b/assets/styles/layout/_menu.scss @@ -0,0 +1,160 @@ +@use 'mixins' as *; + +.layout-sidebar { + position: fixed; + width: 20rem; + height: calc(100vh - 8rem); + z-index: 999; + overflow-y: auto; + user-select: none; + top: 6rem; + left: 2rem; + transition: + transform var(--layout-section-transition-duration), + left var(--layout-section-transition-duration); + background-color: var(--surface-overlay); + border-radius: var(--content-border-radius); + padding: 0.5rem 1.5rem; +} + +.layout-menu { + margin: 0; + padding: 0; + list-style-type: none; + + .layout-root-menuitem { + > .layout-menuitem-root-text { + font-size: 0.857rem; + text-transform: uppercase; + font-weight: 700; + color: var(--text-color); + margin: 0.75rem 0; + } + + > a { + display: none; + } + } + + a { + user-select: none; + + &.active-menuitem { + > .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + li.active-menuitem { + > a { + .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + ul { + margin: 0; + padding: 0; + list-style-type: none; + + a { + display: flex; + align-items: center; + position: relative; + outline: 0 none; + color: var(--text-color); + cursor: pointer; + padding: 0.75rem 1rem; + border-radius: var(--content-border-radius); + transition: + background-color var(--element-transition-duration), + box-shadow var(--element-transition-duration); + + .layout-menuitem-icon { + margin-right: 0.5rem; + } + + .layout-submenu-toggler { + font-size: 75%; + margin-left: auto; + transition: transform var(--element-transition-duration); + } + + &.active-route { + font-weight: 700; + color: var(--primary-color); + } + + &:hover { + background-color: var(--surface-hover); + } + + &:focus { + @include focused-inset(); + } + } + + ul { + overflow: hidden; + border-radius: var(--content-border-radius); + + li { + a { + margin-left: 1rem; + } + + li { + a { + margin-left: 2rem; + } + + li { + a { + margin-left: 2.5rem; + } + + li { + a { + margin-left: 3rem; + } + + li { + a { + margin-left: 3.5rem; + } + + li { + a { + margin-left: 4rem; + } + } + } + } + } + } + } + } + } +} + +.layout-submenu-enter-from, +.layout-submenu-leave-to { + max-height: 0; +} + +.layout-submenu-enter-to, +.layout-submenu-leave-from { + max-height: 1000px; +} + +.layout-submenu-leave-active { + overflow: hidden; + transition: max-height 0.45s cubic-bezier(0, 1, 0, 1); +} + +.layout-submenu-enter-active { + overflow: hidden; + transition: max-height 1s ease-in-out; +} diff --git a/assets/styles/layout/_mixins.scss b/assets/styles/layout/_mixins.scss new file mode 100644 index 0000000..ad330b1 --- /dev/null +++ b/assets/styles/layout/_mixins.scss @@ -0,0 +1,15 @@ +@mixin focused() { + outline-width: var(--focus-ring-width); + outline-style: var(--focus-ring-style); + outline-color: var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + box-shadow: var(--focus-ring-shadow); + transition: + box-shadow var(--transition-duration), + outline-color var(--transition-duration); +} + +@mixin focused-inset() { + outline-offset: -1px; + box-shadow: inset var(--focus-ring-shadow); +} diff --git a/assets/styles/layout/_preloading.scss b/assets/styles/layout/_preloading.scss new file mode 100644 index 0000000..a814104 --- /dev/null +++ b/assets/styles/layout/_preloading.scss @@ -0,0 +1,47 @@ +.preloader { + position: fixed; + z-index: 999999; + background: #edf1f5; + width: 100%; + height: 100%; +} +.preloader-content { + border: 0 solid transparent; + border-radius: 50%; + width: 150px; + height: 150px; + position: absolute; + top: calc(50vh - 75px); + left: calc(50vw - 75px); +} + +.preloader-content:before, .preloader-content:after{ + content: ''; + border: 1em solid var(--primary-color); + border-radius: 50%; + width: inherit; + height: inherit; + position: absolute; + top: 0; + left: 0; + animation: loader 2s linear infinite; + opacity: 0; +} + +.preloader-content:before{ + animation-delay: 0.5s; +} + +@keyframes loader{ + 0%{ + transform: scale(0); + opacity: 0; + } + 50%{ + opacity: 1; + } + 100%{ + transform: scale(1); + opacity: 0; + } +} diff --git a/assets/styles/layout/_responsive.scss b/assets/styles/layout/_responsive.scss new file mode 100644 index 0000000..561d5f1 --- /dev/null +++ b/assets/styles/layout/_responsive.scss @@ -0,0 +1,110 @@ +@media screen and (min-width: 1960px) { + .layout-main, + .landing-wrapper { + width: 1504px; + margin-left: auto !important; + margin-right: auto !important; + } +} + +@media (min-width: 992px) { + .layout-wrapper { + &.layout-overlay { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-right: 1px solid var(--surface-border); + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + } + + &.layout-overlay-active { + .layout-sidebar { + transform: translateX(0); + } + } + } + + &.layout-static { + .layout-main-container { + margin-left: 22rem; + } + + &.layout-static-inactive { + .layout-sidebar { + transform: translateX(-100%); + left: 0; + } + + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + } + } + + .layout-mask { + display: none; + } + } +} + +@media (max-width: 991px) { + .blocked-scroll { + overflow: hidden; + } + + .layout-wrapper { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + } + + .layout-mask { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 998; + width: 100%; + height: 100%; + background-color: var(--maskbg); + } + + &.layout-mobile-active { + .layout-sidebar { + transform: translateX(0); + } + + .layout-mask { + display: block; + } + } + } +} diff --git a/assets/styles/layout/_topbar.scss b/assets/styles/layout/_topbar.scss new file mode 100644 index 0000000..b8e34b3 --- /dev/null +++ b/assets/styles/layout/_topbar.scss @@ -0,0 +1,201 @@ +@use 'mixins' as *; + +.layout-topbar { + position: fixed; + height: 4rem; + z-index: 997; + left: 0; + top: 0; + width: 100%; + padding: 0 2rem; + background-color: var(--surface-card); + transition: left var(--layout-section-transition-duration); + display: flex; + align-items: center; + + .layout-topbar-logo-container { + width: 20rem; + display: flex; + align-items: center; + } + + .layout-topbar-logo { + display: inline-flex; + align-items: center; + font-size: 1.5rem; + border-radius: var(--content-border-radius); + color: var(--text-color); + font-weight: 500; + gap: 0.5rem; + + svg { + width: 3rem; + } + + &:focus-visible { + @include focused(); + } + } + + .layout-topbar-action { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + color: var(--text-color); + transition: background-color var(--element-transition-duration); + cursor: pointer; + + &:hover { + background-color: var(--surface-hover); + } + + &:focus-visible { + @include focused(); + } + + i { + font-size: 1.25rem; + } + + span { + font-size: 1rem; + display: none; + } + + &.layout-topbar-action-highlight { + background-color: var(--primary-color); + color: var(--primary-contrast-color); + } + } + + .layout-menu-button { + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: none; + } + + .layout-topbar-actions { + margin-left: auto; + display: flex; + gap: 1rem; + } + + .layout-topbar-menu-content { + display: flex; + gap: 1rem; + } + + .layout-config-menu { + display: flex; + gap: 1rem; + } +} + +@media (max-width: 991px) { + .layout-topbar { + padding: 0 2rem; + + .layout-topbar-logo-container { + width: auto; + } + + .layout-menu-button { + margin-left: 0; + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: inline-flex; + } + + .layout-topbar-menu { + position: absolute; + background-color: var(--surface-overlay); + transform-origin: top; + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + border-radius: var(--content-border-radius); + padding: 1rem; + right: 2rem; + top: 4rem; + min-width: 15rem; + border: 1px solid var(--surface-border); + + .layout-topbar-menu-content { + gap: 0.5rem; + } + + .layout-topbar-action { + display: flex; + width: 100%; + height: auto; + justify-content: flex-start; + border-radius: var(--content-border-radius); + padding: 0.5rem 1rem; + + i { + font-size: 1rem; + margin-right: 0.5rem; + } + + span { + font-weight: medium; + display: block; + } + } + } + + .layout-topbar-menu-content { + flex-direction: column; + } + } +} + +.config-panel { + .config-panel-label { + font-size: 0.875rem; + color: var(--text-secondary-color); + font-weight: 600; + line-height: 1; + } + + .config-panel-colors { + > div { + padding-top: 0.5rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: space-between; + + button { + border: none; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + padding: 0; + cursor: pointer; + outline-color: transparent; + outline-width: 2px; + outline-style: solid; + outline-offset: 1px; + + &.active-color { + outline-color: var(--primary-color); + } + } + } + } + + .config-panel-settings { + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/assets/styles/layout/_typography.scss b/assets/styles/layout/_typography.scss new file mode 100644 index 0000000..b17bbc2 --- /dev/null +++ b/assets/styles/layout/_typography.scss @@ -0,0 +1,68 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.5rem 0 1rem 0; + font-family: inherit; + font-weight: 700; + line-height: 1.5; + color: var(--text-color); + + &:first-child { + margin-top: 0; + } +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +mark { + background: #fff8e1; + padding: 0.25rem 0.4rem; + border-radius: var(--content-border-radius); + font-family: monospace; +} + +blockquote { + margin: 1rem 0; + padding: 0 2rem; + border-left: 4px solid #90a4ae; +} + +hr { + border-top: solid var(--surface-border); + border-width: 1px 0 0 0; + margin: 1rem 0; +} + +p { + margin: 0 0 1rem 0; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/assets/styles/layout/_utils.scss b/assets/styles/layout/_utils.scss new file mode 100644 index 0000000..6ccec88 --- /dev/null +++ b/assets/styles/layout/_utils.scss @@ -0,0 +1,25 @@ +/* Utils */ +.clearfix:after { + content: ' '; + display: block; + clear: both; +} + +.card { + background: var(--surface-card); + padding: 2rem; + margin-bottom: 2rem; + border-radius: var(--content-border-radius); + + &:last-child { + margin-bottom: 0; + } +} + +.p-toast { + &.p-toast-top-right, + &.p-toast-top-left, + &.p-toast-top-center { + top: 100px; + } +} diff --git a/assets/styles/layout/layout.scss b/assets/styles/layout/layout.scss new file mode 100644 index 0000000..70a04ef --- /dev/null +++ b/assets/styles/layout/layout.scss @@ -0,0 +1,14 @@ +@use './variables/_common'; +@use './variables/_light'; +@use './variables/_dark'; +@use './_mixins'; +@use './_animations'; +@use './_preloading'; +@use './_core'; +@use './_main'; +@use './_topbar'; +@use './_menu'; +@use './_footer'; +@use './_responsive'; +@use './_utils'; +@use './_typography'; diff --git a/assets/styles/layout/variables/_common.scss b/assets/styles/layout/variables/_common.scss new file mode 100644 index 0000000..2a040c2 --- /dev/null +++ b/assets/styles/layout/variables/_common.scss @@ -0,0 +1,20 @@ +:root { + --primary-color: var(--p-primary-color); + --primary-contrast-color: var(--p-primary-contrast-color); + --text-color: var(--p-text-color); + --text-color-secondary: var(--p-text-muted-color); + --surface-border: var(--p-content-border-color); + --surface-card: var(--p-content-background); + --surface-hover: var(--p-content-hover-background); + --surface-overlay: var(--p-overlay-popover-background); + --transition-duration: var(--p-transition-duration); + --maskbg: var(--p-mask-background); + --content-border-radius: var(--p-content-border-radius); + --layout-section-transition-duration: 0.2s; + --element-transition-duration: var(--p-transition-duration); + --focus-ring-width: var(--p-focus-ring-width); + --focus-ring-style: var(--p-focus-ring-style); + --focus-ring-color: var(--p-focus-ring-color); + --focus-ring-offset: var(--p-focus-ring-offset); + --focus-ring-shadow: var(--p-focus-ring-shadow); +} diff --git a/assets/styles/layout/variables/_dark.scss b/assets/styles/layout/variables/_dark.scss new file mode 100644 index 0000000..bb91605 --- /dev/null +++ b/assets/styles/layout/variables/_dark.scss @@ -0,0 +1,5 @@ +:root[class*='app-dark'] { + --surface-ground: var(--p-surface-950); + --code-background: var(--p-surface-800); + --code-color: var(--p-surface-100); +} diff --git a/assets/styles/layout/variables/_light.scss b/assets/styles/layout/variables/_light.scss new file mode 100644 index 0000000..aa3403c --- /dev/null +++ b/assets/styles/layout/variables/_light.scss @@ -0,0 +1,5 @@ +:root { + --surface-ground: var(--p-surface-100); + --code-background: var(--p-surface-900); + --code-color: var(--p-surface-200); +} diff --git a/assets/styles/sakai.scss b/assets/styles/sakai.scss new file mode 100644 index 0000000..c015b7e --- /dev/null +++ b/assets/styles/sakai.scss @@ -0,0 +1,3 @@ +/* Sakai Layout Styles */ +@import 'primeicons/primeicons.css'; +@import './layout/layout.scss'; diff --git a/assets/styles/tailwind.css b/assets/styles/tailwind.css new file mode 100644 index 0000000..8340fce --- /dev/null +++ b/assets/styles/tailwind.css @@ -0,0 +1,32 @@ +@import 'tailwindcss'; + +@plugin 'tailwindcss-primeui'; + +@custom-variant dark (&:where([class*="app-dark"], [class*="app-dark"] *)); + +@theme { + --breakpoint-*: initial; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --breakpoint-2xl: 1920px; +} + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} diff --git a/composer.json b/composer.json index cddec76..da19a41 100644 --- a/composer.json +++ b/composer.json @@ -103,7 +103,7 @@ } }, "require-dev": { - "doctrine/doctrine-fixtures-bundle": "^4.3", + "doctrine/doctrine-fixtures-bundle": "*", "phpunit/phpunit": "^12.4", "symfony/browser-kit": "7.1.*", "symfony/css-selector": "7.1.*", diff --git a/composer.lock b/composer.lock index 8ea500f..e288264 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "10b55ae5547ef383b9e8a94a7ca7402c", + "content-hash": "af57c523401fba0e523501b76e0629f0", "packages": [ { "name": "api-platform/core", diff --git a/migrations/Version20251108175514.php b/migrations/Version20251108175514.php new file mode 100644 index 0000000..0ea45b8 --- /dev/null +++ b/migrations/Version20251108175514.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE contact_persons (id INT AUTO_INCREMENT NOT NULL, contact_id INT NOT NULL, salutation VARCHAR(20) DEFAULT NULL, title VARCHAR(100) DEFAULT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, position VARCHAR(100) DEFAULT NULL, department VARCHAR(100) DEFAULT NULL, phone VARCHAR(50) DEFAULT NULL, mobile VARCHAR(50) DEFAULT NULL, email VARCHAR(180) DEFAULT NULL, is_primary TINYINT(1) NOT NULL, INDEX IDX_3873E652E7A1254A (contact_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE contacts (id INT AUTO_INCREMENT NOT NULL, company_name VARCHAR(255) NOT NULL, company_number VARCHAR(50) DEFAULT NULL, street LONGTEXT DEFAULT NULL, zip_code VARCHAR(20) DEFAULT NULL, city VARCHAR(100) DEFAULT NULL, country VARCHAR(100) DEFAULT NULL, phone VARCHAR(50) DEFAULT NULL, fax VARCHAR(50) DEFAULT NULL, email VARCHAR(180) DEFAULT NULL, website VARCHAR(255) DEFAULT NULL, tax_number VARCHAR(50) DEFAULT NULL, vat_number VARCHAR(50) DEFAULT NULL, is_debtor TINYINT(1) NOT NULL, is_creditor TINYINT(1) NOT NULL, is_active TINYINT(1) NOT NULL, notes LONGTEXT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE contact_persons ADD CONSTRAINT FK_3873E652E7A1254A FOREIGN KEY (contact_id) REFERENCES contacts (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contact_persons DROP FOREIGN KEY FK_3873E652E7A1254A'); + $this->addSql('DROP TABLE contact_persons'); + $this->addSql('DROP TABLE contacts'); + } +} diff --git a/package-lock.json b/package-lock.json index 914db89..f1234f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "dependencies": { "@primevue/themes": "^4.4.1", "pinia": "^2.2.0", - "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primevue": "^4.3.0", "vue-router": "^4.5.0" @@ -17,18 +16,36 @@ "@babel/core": "^7.17.0", "@babel/preset-env": "^7.16.0", "@symfony/webpack-encore": "^5.1.0", + "@tailwindcss/postcss": "^4.1.17", + "@tailwindcss/vite": "^4.1.17", + "autoprefixer": "^10.4.21", "core-js": "^3.38.0", - "postcss": "^8.4.0", + "postcss": "^8.5.6", "postcss-loader": "^7.0.0", "regenerator-runtime": "^0.13.9", "sass": "^1.70.0", "sass-loader": "^16.0.0", + "tailwindcss": "^4.1.17", + "tailwindcss-primeui": "^0.6.1", "vue": "^3.5.0", "vue-loader": "^17.4.0", "webpack": "^5.74.0", "webpack-cli": "^5.1.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1549,6 +1566,474 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2127,6 +2612,336 @@ "node": ">=12.11.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz", + "integrity": "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz", + "integrity": "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz", + "integrity": "sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz", + "integrity": "sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz", + "integrity": "sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz", + "integrity": "sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz", + "integrity": "sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz", + "integrity": "sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz", + "integrity": "sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz", + "integrity": "sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz", + "integrity": "sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz", + "integrity": "sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz", + "integrity": "sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz", + "integrity": "sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz", + "integrity": "sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz", + "integrity": "sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz", + "integrity": "sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz", + "integrity": "sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz", + "integrity": "sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz", + "integrity": "sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz", + "integrity": "sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz", + "integrity": "sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2297,6 +3112,302 @@ "node": ">=10" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2845,6 +3956,44 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/babel-loader": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", @@ -3702,6 +4851,49 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3870,6 +5062,36 @@ "flat": "cli.js" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4336,6 +5558,277 @@ "node": ">=0.10.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -4568,6 +6061,16 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5399,12 +6902,6 @@ "renderkid": "^3.0.0" } }, - "node_modules/primeflex": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-3.3.1.tgz", - "integrity": "sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ==", - "license": "MIT" - }, "node_modules/primeicons": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", @@ -5638,6 +7135,49 @@ "dev": true, "license": "MIT" }, + "node_modules/rollup": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.1.tgz", + "integrity": "sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.1", + "@rollup/rollup-android-arm64": "4.53.1", + "@rollup/rollup-darwin-arm64": "4.53.1", + "@rollup/rollup-darwin-x64": "4.53.1", + "@rollup/rollup-freebsd-arm64": "4.53.1", + "@rollup/rollup-freebsd-x64": "4.53.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.1", + "@rollup/rollup-linux-arm-musleabihf": "4.53.1", + "@rollup/rollup-linux-arm64-gnu": "4.53.1", + "@rollup/rollup-linux-arm64-musl": "4.53.1", + "@rollup/rollup-linux-loong64-gnu": "4.53.1", + "@rollup/rollup-linux-ppc64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-musl": "4.53.1", + "@rollup/rollup-linux-s390x-gnu": "4.53.1", + "@rollup/rollup-linux-x64-gnu": "4.53.1", + "@rollup/rollup-linux-x64-musl": "4.53.1", + "@rollup/rollup-openharmony-arm64": "4.53.1", + "@rollup/rollup-win32-arm64-msvc": "4.53.1", + "@rollup/rollup-win32-ia32-msvc": "4.53.1", + "@rollup/rollup-win32-x64-gnu": "4.53.1", + "@rollup/rollup-win32-x64-msvc": "4.53.1", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6038,6 +7578,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss-primeui": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/tailwindcss-primeui/-/tailwindcss-primeui-0.6.1.tgz", + "integrity": "sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.1.0" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -6154,6 +7711,57 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -6274,6 +7882,115 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vue": { "version": "3.5.24", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", diff --git a/package.json b/package.json index cb1bd2d..b52295b 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,17 @@ "@babel/core": "^7.17.0", "@babel/preset-env": "^7.16.0", "@symfony/webpack-encore": "^5.1.0", + "@tailwindcss/postcss": "^4.1.17", + "@tailwindcss/vite": "^4.1.17", + "autoprefixer": "^10.4.21", "core-js": "^3.38.0", - "postcss": "^8.4.0", + "postcss": "^8.5.6", "postcss-loader": "^7.0.0", "regenerator-runtime": "^0.13.9", "sass": "^1.70.0", "sass-loader": "^16.0.0", + "tailwindcss": "^4.1.17", + "tailwindcss-primeui": "^0.6.1", "vue": "^3.5.0", "vue-loader": "^17.4.0", "webpack": "^5.74.0", @@ -17,7 +22,6 @@ "dependencies": { "@primevue/themes": "^4.4.1", "pinia": "^2.2.0", - "primeflex": "^3.3.1", "primeicons": "^7.0.0", "primevue": "^4.3.0", "vue-router": "^4.5.0" diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..668a5b9 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} diff --git a/src/DataFixtures/ContactFixtures.php b/src/DataFixtures/ContactFixtures.php new file mode 100644 index 0000000..ad4bad4 --- /dev/null +++ b/src/DataFixtures/ContactFixtures.php @@ -0,0 +1,226 @@ +companyNames[array_rand($this->companyNames)] . ' ' . + $this->industries[array_rand($this->industries)]; + $companyType = $this->companyTypes[array_rand($this->companyTypes)]; + $contact->setCompanyName($companyName . ' ' . $companyType); + + // Kundennummer + $contact->setCompanyNumber('K' . str_pad($i, 5, '0', STR_PAD_LEFT)); + + // Adresse + $city = $this->germanCities[array_rand($this->germanCities)]; + $street = $this->streets[array_rand($this->streets)]; + $contact->setStreet($street . ' ' . rand(1, 150)); + $contact->setZipCode($city[1]); + $contact->setCity($city[0]); + $contact->setCountry('Deutschland'); + + // Kontaktdaten + $contact->setPhone('0' . rand(100, 999) . ' ' . rand(10000, 99999)); + if (rand(0, 3) > 0) { + $contact->setFax('0' . rand(100, 999) . ' ' . rand(10000, 99999)); + } + + $domain = strtolower(str_replace(' ', '', $companyName)); + $domain = preg_replace('/[^a-z0-9]/', '', $domain); + $contact->setEmail('info@' . substr($domain, 0, 20) . '.de'); + $contact->setWebsite('https://www.' . substr($domain, 0, 20) . '.de'); + + // Steuernummern + if (rand(0, 1)) { + $contact->setTaxNumber(rand(10, 99) . '/' . rand(100, 999) . '/' . rand(10000, 99999)); + } + if (rand(0, 1)) { + $contact->setVatNumber('DE' . rand(100000000, 999999999)); + } + + // Typ + $type = rand(1, 10); + if ($type <= 6) { + $contact->setIsDebtor(true); + } elseif ($type <= 8) { + $contact->setIsCreditor(true); + } else { + $contact->setIsDebtor(true); + $contact->setIsCreditor(true); + } + + // Status + $contact->setIsActive(rand(0, 10) > 0); // 90% aktiv + + // Notizen + if (rand(0, 3) === 0) { + $notes = [ + 'Wichtiger Kunde, bevorzugte Behandlung', + 'Zahlt pünktlich', + 'Benötigt ausführliche Beratung', + 'Großkunde mit Sonderkonditionen', + 'Neukunde seit ' . date('Y'), + 'Jahresvertrag läuft noch bis ' . date('m.Y', strtotime('+' . rand(1, 12) . ' months')), + ]; + $contact->setNotes($notes[array_rand($notes)]); + } + + // Ansprechpartner (1-2 pro Firma) + $numPersons = rand(1, 2); + for ($p = 0; $p < $numPersons; $p++) { + $person = new ContactPerson(); + + $gender = rand(0, 1); + $person->setSalutation($gender ? 'Herr' : 'Frau'); + + if (rand(0, 5) === 0) { + $person->setTitle(rand(0, 1) ? 'Dr.' : 'Prof. Dr.'); + } + + $person->setFirstName($this->firstNames[array_rand($this->firstNames)]); + $person->setLastName($this->lastNames[array_rand($this->lastNames)]); + + $person->setPosition($this->positions[array_rand($this->positions)]); + $person->setDepartment($this->departments[array_rand($this->departments)]); + + $person->setPhone('0' . rand(100, 999) . ' ' . rand(10000, 99999) . '-' . rand(10, 99)); + if (rand(0, 2) > 0) { + $person->setMobile('0' . rand(150, 179) . ' ' . rand(1000000, 9999999)); + } + + $firstName = strtolower($person->getFirstName()); + $lastName = strtolower($person->getLastName()); + $person->setEmail($firstName . '.' . $lastName . '@' . substr($domain, 0, 20) . '.de'); + + $person->setIsPrimary($p === 0); + + $contact->addContactPerson($person); + $manager->persist($person); + } + + $manager->persist($contact); + + // Batch-Verarbeitung für bessere Performance + if (($i % 50) === 0) { + $manager->flush(); + $manager->clear(); + } + } + + $manager->flush(); + } +} diff --git a/src/Entity/Contact.php b/src/Entity/Contact.php new file mode 100644 index 0000000..61b44fd --- /dev/null +++ b/src/Entity/Contact.php @@ -0,0 +1,394 @@ + ['contact:read']], + denormalizationContext: ['groups' => ['contact:write']], + order: ['companyName' => 'ASC'] +)] +#[ApiFilter(BooleanFilter::class, properties: ['isDebtor', 'isCreditor', 'isActive'])] +#[ApiFilter(SearchFilter::class, properties: ['companyName' => 'partial', 'city' => 'partial', 'email' => 'partial'])] +class Contact +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['contact:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\NotBlank(message: 'Der Firmenname darf nicht leer sein')] + #[Assert\Length(max: 255)] + private ?string $companyName = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $companyNumber = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + private ?string $street = null; + + #[ORM\Column(length: 20, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 20)] + private ?string $zipCode = null; + + #[ORM\Column(length: 100, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 100)] + private ?string $city = null; + + #[ORM\Column(length: 100, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 100)] + private ?string $country = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $phone = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $fax = null; + + #[ORM\Column(length: 180, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Email(message: 'Bitte geben Sie eine gültige E-Mail-Adresse ein')] + #[Assert\Length(max: 180)] + private ?string $email = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Url(message: 'Bitte geben Sie eine gültige URL ein')] + #[Assert\Length(max: 255)] + private ?string $website = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $taxNumber = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $vatNumber = null; + + #[ORM\Column] + #[Groups(['contact:read', 'contact:write'])] + private bool $isDebtor = false; + + #[ORM\Column] + #[Groups(['contact:read', 'contact:write'])] + private bool $isCreditor = false; + + #[ORM\Column] + #[Groups(['contact:read', 'contact:write'])] + private bool $isActive = true; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + private ?string $notes = null; + + #[ORM\OneToMany(targetEntity: ContactPerson::class, mappedBy: 'contact', cascade: ['persist', 'remove'], orphanRemoval: true)] + #[Groups(['contact:read', 'contact:write'])] + private Collection $contactPersons; + + #[ORM\Column] + #[Groups(['contact:read'])] + private ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column(nullable: true)] + #[Groups(['contact:read'])] + private ?\DateTimeImmutable $updatedAt = null; + + public function __construct() + { + $this->contactPersons = new ArrayCollection(); + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCompanyName(): ?string + { + return $this->companyName; + } + + public function setCompanyName(string $companyName): static + { + $this->companyName = $companyName; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCompanyNumber(): ?string + { + return $this->companyNumber; + } + + public function setCompanyNumber(?string $companyNumber): static + { + $this->companyNumber = $companyNumber; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getStreet(): ?string + { + return $this->street; + } + + public function setStreet(?string $street): static + { + $this->street = $street; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getZipCode(): ?string + { + return $this->zipCode; + } + + public function setZipCode(?string $zipCode): static + { + $this->zipCode = $zipCode; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(?string $city): static + { + $this->city = $city; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCountry(): ?string + { + return $this->country; + } + + public function setCountry(?string $country): static + { + $this->country = $country; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(?string $phone): static + { + $this->phone = $phone; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getFax(): ?string + { + return $this->fax; + } + + public function setFax(?string $fax): static + { + $this->fax = $fax; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(?string $email): static + { + $this->email = $email; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getWebsite(): ?string + { + return $this->website; + } + + public function setWebsite(?string $website): static + { + $this->website = $website; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getTaxNumber(): ?string + { + return $this->taxNumber; + } + + public function setTaxNumber(?string $taxNumber): static + { + $this->taxNumber = $taxNumber; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getVatNumber(): ?string + { + return $this->vatNumber; + } + + public function setVatNumber(?string $vatNumber): static + { + $this->vatNumber = $vatNumber; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function isDebtor(): bool + { + return $this->isDebtor; + } + + public function setIsDebtor(bool $isDebtor): static + { + $this->isDebtor = $isDebtor; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function isCreditor(): bool + { + return $this->isCreditor; + } + + public function setIsCreditor(bool $isCreditor): static + { + $this->isCreditor = $isCreditor; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getIsActive(): bool + { + return $this->isActive; + } + + public function setIsActive(bool $isActive): static + { + $this->isActive = $isActive; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getNotes(): ?string + { + return $this->notes; + } + + public function setNotes(?string $notes): static + { + $this->notes = $notes; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + /** + * @return Collection + */ + public function getContactPersons(): Collection + { + return $this->contactPersons; + } + + public function addContactPerson(ContactPerson $contactPerson): static + { + if (!$this->contactPersons->contains($contactPerson)) { + $this->contactPersons->add($contactPerson); + $contactPerson->setContact($this); + } + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function removeContactPerson(ContactPerson $contactPerson): static + { + if ($this->contactPersons->removeElement($contactPerson)) { + if ($contactPerson->getContact() === $this) { + $contactPerson->setContact(null); + } + } + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } + + public function __toString(): string + { + return $this->companyName ?? ''; + } +} diff --git a/src/Entity/ContactPerson.php b/src/Entity/ContactPerson.php new file mode 100644 index 0000000..a00c224 --- /dev/null +++ b/src/Entity/ContactPerson.php @@ -0,0 +1,216 @@ + ['contact:read']], + denormalizationContext: ['groups' => ['contact:write']] +)] +class ContactPerson +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['contact:read'])] + private ?int $id = null; + + #[ORM\ManyToOne(targetEntity: Contact::class, inversedBy: 'contactPersons')] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + private ?Contact $contact = null; + + #[ORM\Column(length: 20, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 20)] + private ?string $salutation = null; + + #[ORM\Column(length: 100, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 100)] + private ?string $title = null; + + #[ORM\Column(length: 100)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\NotBlank(message: 'Der Vorname darf nicht leer sein')] + #[Assert\Length(max: 100)] + private ?string $firstName = null; + + #[ORM\Column(length: 100)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\NotBlank(message: 'Der Nachname darf nicht leer sein')] + #[Assert\Length(max: 100)] + private ?string $lastName = null; + + #[ORM\Column(length: 100, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 100)] + private ?string $position = null; + + #[ORM\Column(length: 100, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 100)] + private ?string $department = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $phone = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Length(max: 50)] + private ?string $mobile = null; + + #[ORM\Column(length: 180, nullable: true)] + #[Groups(['contact:read', 'contact:write'])] + #[Assert\Email(message: 'Bitte geben Sie eine gültige E-Mail-Adresse ein')] + #[Assert\Length(max: 180)] + private ?string $email = null; + + #[ORM\Column] + #[Groups(['contact:read', 'contact:write'])] + private bool $isPrimary = false; + + public function getId(): ?int + { + return $this->id; + } + + public function getContact(): ?Contact + { + return $this->contact; + } + + public function setContact(?Contact $contact): static + { + $this->contact = $contact; + return $this; + } + + public function getSalutation(): ?string + { + return $this->salutation; + } + + public function setSalutation(?string $salutation): static + { + $this->salutation = $salutation; + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): static + { + $this->title = $title; + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + return $this; + } + + public function getPosition(): ?string + { + return $this->position; + } + + public function setPosition(?string $position): static + { + $this->position = $position; + return $this; + } + + public function getDepartment(): ?string + { + return $this->department; + } + + public function setDepartment(?string $department): static + { + $this->department = $department; + return $this; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(?string $phone): static + { + $this->phone = $phone; + return $this; + } + + public function getMobile(): ?string + { + return $this->mobile; + } + + public function setMobile(?string $mobile): static + { + $this->mobile = $mobile; + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(?string $email): static + { + $this->email = $email; + return $this; + } + + public function getIsPrimary(): bool + { + return $this->isPrimary; + } + + public function setIsPrimary(bool $isPrimary): static + { + $this->isPrimary = $isPrimary; + return $this; + } + + public function getFullName(): string + { + return trim(($this->title ? $this->title . ' ' : '') . $this->firstName . ' ' . $this->lastName); + } + + public function __toString(): string + { + return $this->getFullName(); + } +} diff --git a/src/Repository/ContactRepository.php b/src/Repository/ContactRepository.php new file mode 100644 index 0000000..8b25d98 --- /dev/null +++ b/src/Repository/ContactRepository.php @@ -0,0 +1,60 @@ + + */ +class ContactRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Contact::class); + } + + /** + * Find contacts by type (debtor, creditor, or both) + */ + public function findByType(?bool $isDebtor = null, ?bool $isCreditor = null): array + { + $qb = $this->createQueryBuilder('c') + ->where('c.isActive = :active') + ->setParameter('active', true) + ->orderBy('c.companyName', 'ASC'); + + if ($isDebtor !== null) { + $qb->andWhere('c.isDebtor = :isDebtor') + ->setParameter('isDebtor', $isDebtor); + } + + if ($isCreditor !== null) { + $qb->andWhere('c.isCreditor = :isCreditor') + ->setParameter('isCreditor', $isCreditor); + } + + return $qb->getQuery()->getResult(); + } + + /** + * Search contacts by company name, contact person, or email + */ + public function search(string $searchTerm): array + { + return $this->createQueryBuilder('c') + ->leftJoin('c.contactPersons', 'cp') + ->where('c.companyName LIKE :search') + ->orWhere('c.email LIKE :search') + ->orWhere('c.city LIKE :search') + ->orWhere('cp.firstName LIKE :search') + ->orWhere('cp.lastName LIKE :search') + ->orWhere('cp.email LIKE :search') + ->setParameter('search', '%' . $searchTerm . '%') + ->orderBy('c.companyName', 'ASC') + ->getQuery() + ->getResult(); + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..c6265df --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,14 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./assets/**/*.{js,vue}", + "./templates/**/*.html.twig", + ], + darkMode: 'class', + theme: { + extend: {}, + }, + plugins: [ + require('tailwindcss-primeui') + ], +}
Noch keine Ansprechpartner hinzugefügt