mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat(frontend): bullet-editor fuer Projektaufgaben + Popup-Backdrop deaktiviert
- Modal.tsx + Drawer.tsx: onClick={onClose} vom Backdrop entfernt — alle
Popups schliessen sich jetzt nur noch ueber Speichern/Abbrechen-Buttons
- ProjectModal.tsx: Textarea "Aufgaben" durch BulletEditor-Komponente
ersetzt (Toolbar-Button toggled Aufzaehlungspunkt, Enter setzt Bullet
auf naechster Zeile fort, leere Bullet-Zeile + Enter beendet die Liste)
- ExpertProfileTab.module.css: CSS fuer bulletEditor, bulletToolbar,
bulletBtn, bulletEditorTextarea hinzugefuegt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2078a90fba
commit
a4013d4356
5 changed files with 201 additions and 7 deletions
26
Summarize.md
26
Summarize.md
|
|
@ -6,6 +6,32 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Aenderungen 2026-03-13 (13): Experten-Profil – Bullet-Editor fuer Aufgaben + Popup-Backdrop deaktiviert
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- `components/Modal.tsx` — `onClick={onClose}` vom Overlay-div entfernt: Popups schliessen sich nicht mehr durch Klick neben das Popup (betrifft alle Modal-Instanzen im Projekt)
|
||||||
|
- `components/Drawer.tsx` — Gleiches: Drawer schliesst sich nicht mehr durch Klick auf den Backdrop
|
||||||
|
- `profile/sections/ProjectModal.tsx` — Neue lokale `BulletEditor`-Komponente (keine externe Library): Toolbar mit "☰ Liste"-Button (toggled `• ` am Zeilenanfang ein/aus), automatische Aufzaehlungsfortsetzung beim Enter-Druck, leere Bullet-Zeile `• ` + Enter beendet die Liste; ersetzte die einfache `<textarea>` im Feld "Aufgaben"
|
||||||
|
- `profile/ExpertProfileTab.module.css` — Neue CSS-Klassen: `.bulletEditor`, `.bulletToolbar`, `.bulletBtn`, `.bulletEditorTextarea`, `.bulletBtn:hover`; Editor-Container mit `border + :focus-within`-Ring analog zu Input-Feldern; Textarea innerhalb des Editors hat keinen eigenen Rand (verhindert doppelten Rahmen)
|
||||||
|
|
||||||
|
#### TypeScript
|
||||||
|
- `npx tsc --noEmit` in packages/frontend: 0 Fehler
|
||||||
|
|
||||||
|
#### Deployment-Hinweis (Schritt 13)
|
||||||
|
- Rebuild + Restart: nur frontend (kein Backend-Aenderung)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Aenderungen 2026-03-13 (12): Bug-Fix Sprachen im Experten-Profil
|
||||||
|
|
||||||
|
#### Backend (core-service)
|
||||||
|
- `core/expert-profile/dto/create-language.dto.ts` — `@IsIn` Validator um fehlende Werte erweitert: `Verhandlungssicher`, `Fliesend`, `Gut` waren nicht enthalten (Frontend sendete diese Werte → 400 Fehler); alle 10 Werte jetzt korrekt: `['Muttersprache', 'Verhandlungssicher', 'Fliesend', 'Gut', 'C2', 'C1', 'B2', 'B1', 'A2', 'A1']`
|
||||||
|
|
||||||
|
#### Deployment-Hinweis (Schritt 12)
|
||||||
|
- Rebuild + Restart: nur core-service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Aenderungen 2026-03-13 (11): Kontakt-Detailseite – Breite, Outlook-Daten-Sektion, Felder, Outlook-Push
|
### Aenderungen 2026-03-13 (11): Kontakt-Detailseite – Breite, Outlook-Daten-Sektion, Felder, Outlook-Push
|
||||||
|
|
||||||
#### Backend (crm-service)
|
#### Backend (crm-service)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function Drawer({
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={styles.overlay} onClick={onClose}>
|
<div className={styles.overlay}>
|
||||||
<div
|
<div
|
||||||
className={styles.panel}
|
className={styles.panel}
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = '600px' }:
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className={styles.overlay} onClick={onClose}>
|
<div className={styles.overlay}>
|
||||||
<div
|
<div
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
style={{ maxWidth }}
|
style={{ maxWidth }}
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,67 @@
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Bullet Editor === */
|
||||||
|
.bulletEditor {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletEditor:focus-within {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletToolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletBtn {
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
transition: background 0.1s, border-color 0.1s, color 0.1s;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletBtn:hover {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletEditor .bulletEditorTextarea {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
background: transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulletEditor .bulletEditorTextarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.checkboxRow {
|
.checkboxRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, useEffect, useRef, useCallback, type FormEvent } from 'react';
|
||||||
import { Modal } from '../../components/Modal';
|
import { Modal } from '../../components/Modal';
|
||||||
import api from '../../api/client';
|
import api from '../../api/client';
|
||||||
import type { ExpertProject } from '../ExpertProfileTab';
|
import type { ExpertProject } from '../ExpertProfileTab';
|
||||||
|
|
@ -34,6 +34,113 @@ const INDUSTRIES = [
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i);
|
const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i);
|
||||||
|
|
||||||
|
// Lightweight bullet-point editor — no external dependencies
|
||||||
|
function BulletEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
maxLength,
|
||||||
|
rows = 6,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
rows?: number;
|
||||||
|
}) {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const toggleBullet = useCallback(() => {
|
||||||
|
const ta = textareaRef.current;
|
||||||
|
if (!ta) return;
|
||||||
|
const start = ta.selectionStart;
|
||||||
|
const val = ta.value;
|
||||||
|
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||||
|
const lineEnd = val.indexOf('\n', start);
|
||||||
|
const line = val.substring(lineStart, lineEnd === -1 ? val.length : lineEnd);
|
||||||
|
|
||||||
|
let newVal: string;
|
||||||
|
let newCursor: number;
|
||||||
|
|
||||||
|
if (line.startsWith('• ')) {
|
||||||
|
// Remove bullet from current line
|
||||||
|
newVal =
|
||||||
|
val.substring(0, lineStart) +
|
||||||
|
line.substring(2) +
|
||||||
|
val.substring(lineEnd === -1 ? val.length : lineEnd);
|
||||||
|
newCursor = Math.max(lineStart, start - 2);
|
||||||
|
} else {
|
||||||
|
// Add bullet to current line
|
||||||
|
newVal = val.substring(0, lineStart) + '• ' + val.substring(lineStart);
|
||||||
|
newCursor = start + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(newVal);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ta.selectionStart = ta.selectionEnd = newCursor;
|
||||||
|
ta.focus();
|
||||||
|
});
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
const ta = textareaRef.current;
|
||||||
|
if (!ta) return;
|
||||||
|
const start = ta.selectionStart;
|
||||||
|
const val = ta.value;
|
||||||
|
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||||
|
const lineContent = val.substring(lineStart, start);
|
||||||
|
|
||||||
|
if (lineContent === '• ') {
|
||||||
|
// Empty bullet: remove it and exit list
|
||||||
|
e.preventDefault();
|
||||||
|
const newVal = val.substring(0, lineStart) + val.substring(start);
|
||||||
|
onChange(newVal);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ta.selectionStart = ta.selectionEnd = lineStart;
|
||||||
|
});
|
||||||
|
} else if (lineContent.startsWith('• ')) {
|
||||||
|
// Continue bullet list on new line
|
||||||
|
e.preventDefault();
|
||||||
|
const insert = '\n• ';
|
||||||
|
const newVal =
|
||||||
|
val.substring(0, start) + insert + val.substring(ta.selectionEnd);
|
||||||
|
onChange(newVal);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ta.selectionStart = ta.selectionEnd = start + insert.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.bulletEditor}>
|
||||||
|
<div className={styles.bulletToolbar}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.bulletBtn}
|
||||||
|
onClick={toggleBullet}
|
||||||
|
title="Aufzählungspunkt ein/aus (☰)"
|
||||||
|
>
|
||||||
|
☰ Liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className={styles.bulletEditorTextarea}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
maxLength={maxLength}
|
||||||
|
rows={rows}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalProps) {
|
export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalProps) {
|
||||||
const [fromMonth, setFromMonth] = useState(1);
|
const [fromMonth, setFromMonth] = useState(1);
|
||||||
const [fromYear, setFromYear] = useState(currentYear);
|
const [fromYear, setFromYear] = useState(currentYear);
|
||||||
|
|
@ -185,12 +292,12 @@ export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalP
|
||||||
|
|
||||||
<div className={styles.modalField}>
|
<div className={styles.modalField}>
|
||||||
<label>Aufgaben</label>
|
<label>Aufgaben</label>
|
||||||
<textarea
|
<BulletEditor
|
||||||
value={tasks}
|
value={tasks}
|
||||||
onChange={(e) => setTasks(e.target.value)}
|
onChange={setTasks}
|
||||||
placeholder="Beschreiben Sie Ihre Aufgaben..."
|
placeholder="Beschreiben Sie Ihre Aufgaben... (☰ Liste für Aufzählungspunkte)"
|
||||||
maxLength={1500}
|
maxLength={1500}
|
||||||
rows={4}
|
rows={6}
|
||||||
/>
|
/>
|
||||||
<div className={`${styles.charCount} ${tasks.length > 1400 ? styles.charCountWarn : ''}`}>
|
<div className={`${styles.charCount} ${tasks.length > 1400 ? styles.charCountWarn : ''}`}>
|
||||||
{tasks.length}/1.500
|
{tasks.length}/1.500
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue