mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat(frontend): E-Mail-Popup, Aktivitaeten-Zeitstrahl + Profil in Tab-Leiste
- EmailsTab: Outlook-aehnlicher Detail-Popup beim Klick auf E-Mail (Von/An/Datum/Anhang-Meta, Body-Vorschau, In Kontakt speichern als EMAIL-Aktivitaet) - Neues EmailsTab.module.css fuer kompakte Liste und Modal - ContactDetailPage: Aktivitaeten-Filterleiste (Typ + Zeitraum Von/Bis) - ContactDetailPage: Zeitstrahl mit vertikaler Verbindungslinie, farbigen Typ-Badges (Note/Call/Email/Meeting/Task/FollowUp) - DashboardPage: Profil-Bereich (Theme-Schalter, Avatar, Name, Logout) in Tab-Leiste integriert und leicht farblich abgesetzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
17a81c97ef
commit
2af54246c8
6 changed files with 1050 additions and 90 deletions
|
|
@ -191,22 +191,99 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- Timeline ---- */
|
||||
/* ---- Aktivitäten-Filter ---- */
|
||||
.activityFilterBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.625rem 0.875rem;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.activityFilterSelect {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.activityFilterDate {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text);
|
||||
height: 30px;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.activityFilterLabel {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activityFilterReset {
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activityFilterReset:hover {
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.activityFilterCount {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ---- Timeline (Zeitstrahl) ---- */
|
||||
.timeline {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Verbindungslinie links */
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px; /* Mitte des 32px Icons */
|
||||
top: 24px;
|
||||
bottom: 16px;
|
||||
width: 2px;
|
||||
background: var(--color-border);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.timelineItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
gap: 0.875rem;
|
||||
padding: 0.625rem 0;
|
||||
}
|
||||
|
||||
.timelineItem:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timelineIcon {
|
||||
|
|
@ -214,35 +291,81 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg-card);
|
||||
border: 2px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
/* Farbige Icon-Kreise pro Typ */
|
||||
.timelineIconNote { border-color: #94a3b8; color: #64748b; }
|
||||
.timelineIconCall { border-color: #22c55e; color: #16a34a; }
|
||||
.timelineIconEmail { border-color: #3b82f6; color: #2563eb; }
|
||||
.timelineIconMeeting { border-color: #a855f7; color: #9333ea; }
|
||||
.timelineIconTask { border-color: #f59e0b; color: #d97706; }
|
||||
.timelineIconFollowup { border-color: #ef4444; color: #dc2626; }
|
||||
|
||||
.timelineContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 0.1875rem;
|
||||
}
|
||||
|
||||
.timelineSubject {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.timelineMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.1875rem;
|
||||
}
|
||||
|
||||
.timelineTypeBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.0625rem 0.4375rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timelineBadgeNote { background: #f1f5f9; color: #64748b; }
|
||||
.timelineBadgeCall { background: #dcfce7; color: #16a34a; }
|
||||
.timelineBadgeEmail { background: #dbeafe; color: #2563eb; }
|
||||
.timelineBadgeMeeting { background: #f3e8ff; color: #9333ea; }
|
||||
.timelineBadgeTask { background: #fef9c3; color: #d97706; }
|
||||
.timelineBadgeFollowup { background: #fee2e2; color: #dc2626; }
|
||||
|
||||
.timelineDate {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.timelineDesc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 0.375rem;
|
||||
margin-top: 0.3125rem;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.timelineEmpty {
|
||||
text-align: center;
|
||||
padding: 1.5rem 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* ---- Notes ---- */
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ export function ContactDetailPage() {
|
|||
const [isDeleteOpen, setDeleteOpen] = useState(false);
|
||||
const [m365Tab, setM365Tab] = useState<M365Tab>('emails');
|
||||
|
||||
// Aktivitäten-Filter
|
||||
const [actTypeFilter, setActTypeFilter] = useState<ActivityType | 'ALL'>('ALL');
|
||||
const [actDateFrom, setActDateFrom] = useState('');
|
||||
const [actDateTo, setActDateTo] = useState('');
|
||||
|
||||
if (isLoading) return <p>Laden…</p>;
|
||||
if (error || !data)
|
||||
return (
|
||||
|
|
@ -128,9 +133,31 @@ export function ContactDetailPage() {
|
|||
);
|
||||
|
||||
const contact = data.data;
|
||||
const activities: Activity[] = contact.activities ?? [];
|
||||
const allActivities: Activity[] = contact.activities ?? [];
|
||||
const deals = dealsData?.data ?? [];
|
||||
|
||||
// Gefilterte Aktivitäten
|
||||
const activities = allActivities.filter((act) => {
|
||||
if (actTypeFilter !== 'ALL' && act.type !== actTypeFilter) return false;
|
||||
if (actDateFrom) {
|
||||
const actDate = new Date(act.createdAt).toISOString().slice(0, 10);
|
||||
if (actDate < actDateFrom) return false;
|
||||
}
|
||||
if (actDateTo) {
|
||||
const actDate = new Date(act.createdAt).toISOString().slice(0, 10);
|
||||
if (actDate > actDateTo) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const isFilterActive = actTypeFilter !== 'ALL' || actDateFrom !== '' || actDateTo !== '';
|
||||
|
||||
function resetFilters() {
|
||||
setActTypeFilter('ALL');
|
||||
setActDateFrom('');
|
||||
setActDateTo('');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Back link */}
|
||||
|
|
@ -495,6 +522,7 @@ export function ContactDetailPage() {
|
|||
|
||||
{/* ── Aktivitäten (full width below) ── */}
|
||||
<div className={styles.card} style={{ marginTop: '1.5rem' }}>
|
||||
{/* Karten-Header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
|
|
@ -505,6 +533,18 @@ export function ContactDetailPage() {
|
|||
>
|
||||
<h2 className={styles.cardTitle} style={{ margin: 0 }}>
|
||||
Aktivitäten
|
||||
{allActivities.length > 0 && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '0.5rem',
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: 400,
|
||||
color: 'var(--color-text-muted)',
|
||||
}}
|
||||
>
|
||||
({allActivities.length})
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setActivityOpen(true)}
|
||||
|
|
@ -523,31 +563,113 @@ export function ContactDetailPage() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter-Leiste */}
|
||||
{allActivities.length > 0 && (
|
||||
<div className={styles.activityFilterBar}>
|
||||
<span className={styles.activityFilterLabel}>Typ:</span>
|
||||
<select
|
||||
className={styles.activityFilterSelect}
|
||||
value={actTypeFilter}
|
||||
onChange={(e) =>
|
||||
setActTypeFilter(e.target.value as ActivityType | 'ALL')
|
||||
}
|
||||
>
|
||||
<option value="ALL">Alle</option>
|
||||
{(Object.keys(ACTIVITY_TYPE_LABELS) as ActivityType[]).map(
|
||||
(t) => (
|
||||
<option key={t} value={t}>
|
||||
{ACTIVITY_TYPE_LABELS[t]}
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
|
||||
<span className={styles.activityFilterLabel}>Von:</span>
|
||||
<input
|
||||
type="date"
|
||||
className={styles.activityFilterDate}
|
||||
value={actDateFrom}
|
||||
onChange={(e) => setActDateFrom(e.target.value)}
|
||||
/>
|
||||
|
||||
<span className={styles.activityFilterLabel}>Bis:</span>
|
||||
<input
|
||||
type="date"
|
||||
className={styles.activityFilterDate}
|
||||
value={actDateTo}
|
||||
onChange={(e) => setActDateTo(e.target.value)}
|
||||
/>
|
||||
|
||||
{isFilterActive && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.activityFilterReset}
|
||||
onClick={resetFilters}
|
||||
>
|
||||
× Filter zurücksetzen
|
||||
</button>
|
||||
)}
|
||||
|
||||
<span className={styles.activityFilterCount}>
|
||||
{activities.length} von {allActivities.length}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zeitstrahl */}
|
||||
{activities.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem' }}>
|
||||
Keine Aktivitäten vorhanden
|
||||
<p className={styles.timelineEmpty}>
|
||||
{isFilterActive
|
||||
? 'Keine Aktivitäten für den gewählten Filter'
|
||||
: 'Keine Aktivitäten vorhanden'}
|
||||
</p>
|
||||
) : (
|
||||
<div className={styles.timeline}>
|
||||
{activities.map((act) => (
|
||||
<div key={act.id} className={styles.timelineItem}>
|
||||
<div className={styles.timelineIcon}>
|
||||
{activityIcon(act.type)}
|
||||
</div>
|
||||
<div className={styles.timelineContent}>
|
||||
<div className={styles.timelineSubject}>{act.subject}</div>
|
||||
<div className={styles.timelineMeta}>
|
||||
{ACTIVITY_TYPE_LABELS[act.type]} ·{' '}
|
||||
{formatDate(act.createdAt)}
|
||||
{activities.map((act) => {
|
||||
const iconClass = {
|
||||
NOTE: styles.timelineIconNote,
|
||||
CALL: styles.timelineIconCall,
|
||||
EMAIL: styles.timelineIconEmail,
|
||||
MEETING: styles.timelineIconMeeting,
|
||||
TASK: styles.timelineIconTask,
|
||||
FOLLOWUP: styles.timelineIconFollowup,
|
||||
}[act.type] ?? '';
|
||||
|
||||
const badgeClass = {
|
||||
NOTE: styles.timelineBadgeNote,
|
||||
CALL: styles.timelineBadgeCall,
|
||||
EMAIL: styles.timelineBadgeEmail,
|
||||
MEETING: styles.timelineBadgeMeeting,
|
||||
TASK: styles.timelineBadgeTask,
|
||||
FOLLOWUP: styles.timelineBadgeFollowup,
|
||||
}[act.type] ?? '';
|
||||
|
||||
return (
|
||||
<div key={act.id} className={styles.timelineItem}>
|
||||
<div className={`${styles.timelineIcon} ${iconClass}`}>
|
||||
{activityIcon(act.type)}
|
||||
</div>
|
||||
{act.description && (
|
||||
<div className={styles.timelineDesc}>
|
||||
{act.description}
|
||||
<div className={styles.timelineContent}>
|
||||
<div className={styles.timelineSubject}>{act.subject}</div>
|
||||
<div className={styles.timelineMeta}>
|
||||
<span
|
||||
className={`${styles.timelineTypeBadge} ${badgeClass}`}
|
||||
>
|
||||
{ACTIVITY_TYPE_LABELS[act.type]}
|
||||
</span>
|
||||
<span className={styles.timelineDate}>
|
||||
{formatDate(act.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{act.description && (
|
||||
<div className={styles.timelineDesc}>
|
||||
{act.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
332
packages/frontend/src/crm/contacts/EmailsTab.module.css
Normal file
332
packages/frontend/src/crm/contacts/EmailsTab.module.css
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
/* ============================================================
|
||||
EmailsTab – E-Mail-Liste + Detail-Modal
|
||||
============================================================ */
|
||||
|
||||
/* ---- E-Mail-Liste ---- */
|
||||
.emailList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3125rem;
|
||||
}
|
||||
|
||||
.emailItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-left-width: 3px;
|
||||
border-left-color: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.emailItem:hover {
|
||||
background: var(--color-bg-subtle, var(--color-bg));
|
||||
}
|
||||
|
||||
.emailItemUnread {
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.emailMeta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emailRow1 {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.emailSubject {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.emailSubjectUnread {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.emailTime {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.emailFrom {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.emailPreview {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 0.125rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ---- Status-Dots ---- */
|
||||
.unreadDot {
|
||||
flex-shrink: 0;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.readSpacer {
|
||||
flex-shrink: 0;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
/* ---- Anhang-Badge ---- */
|
||||
.attachBadge {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.125rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Detail-Modal
|
||||
============================================================ */
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid var(--color-border);
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
max-height: 82vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Modal-Header */
|
||||
.modalHeader {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modalHeaderLeft {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.modalSubject {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.modalUnreadBadge {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.modalClose {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1.375rem;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color 0.12s, background 0.12s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modalClose:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
/* Modal-Body */
|
||||
.modalBody {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Meta-Tabelle (Von / An / Datum / Anhang) */
|
||||
.metaTable {
|
||||
display: grid;
|
||||
grid-template-columns: 52px 1fr;
|
||||
gap: 0.3125rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.metaLabel {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
padding-top: 0.0625rem;
|
||||
}
|
||||
|
||||
.metaValue {
|
||||
color: var(--color-text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Body-Vorschau */
|
||||
.bodyPreview {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.65;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.bodyHint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Speichern-Bereich */
|
||||
.saveSection {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.saveBtn {
|
||||
padding: 0.375rem 0.875rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.12s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.saveBtn:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.saveBtnSuccess {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.saveBtnError {
|
||||
background: var(--color-error, #dc2626);
|
||||
}
|
||||
|
||||
.saveMsg {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Modal-Footer */
|
||||
.modalFooter {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modalFooterDate {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.outlookBtn {
|
||||
padding: 0.375rem 0.875rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
|
||||
.outlookBtn:hover {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { useContactEmails, useIntegrations } from '../hooks';
|
||||
import { useState } from 'react';
|
||||
import { useContactEmails, useIntegrations, useCreateActivity } from '../hooks';
|
||||
import { integrationsApi } from '../api';
|
||||
import type { M365Email } from '../types';
|
||||
import type { M365Email, M365EmailAddress } from '../types';
|
||||
import styles from './EmailsTab.module.css';
|
||||
|
||||
interface Props {
|
||||
contactId: string;
|
||||
|
|
@ -16,19 +18,194 @@ function formatEmailDate(iso: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
function formatEmailTime(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
if (d.toDateString() === now.toDateString()) {
|
||||
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
function recipientsText(
|
||||
recipients: Array<{ emailAddress: M365EmailAddress }>,
|
||||
): string {
|
||||
return recipients
|
||||
.map((r) => r.emailAddress.name ?? r.emailAddress.address ?? '—')
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
/* ── Detail-Popup (Outlook-ähnlich) ───────────────────────────────────────── */
|
||||
|
||||
function EmailDetailModal({
|
||||
email,
|
||||
contactId,
|
||||
onClose,
|
||||
}: {
|
||||
email: M365Email;
|
||||
contactId: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const createActivity = useCreateActivity();
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
|
||||
const fromName =
|
||||
email.from?.emailAddress?.name ?? email.from?.emailAddress?.address ?? '—';
|
||||
const toText = email.toRecipients?.length
|
||||
? recipientsText(email.toRecipients)
|
||||
: '—';
|
||||
|
||||
async function handleSave() {
|
||||
if (saveState === 'saved') return;
|
||||
setSaveState('saving');
|
||||
try {
|
||||
await createActivity.mutateAsync({
|
||||
contactId,
|
||||
type: 'EMAIL',
|
||||
subject: email.subject ?? '(kein Betreff)',
|
||||
description: email.bodyPreview ?? undefined,
|
||||
scheduledAt: email.receivedDateTime,
|
||||
completedAt: email.receivedDateTime,
|
||||
});
|
||||
setSaveState('saved');
|
||||
} catch {
|
||||
setSaveState('error');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.backdrop} onClick={onClose}>
|
||||
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className={styles.modalHeader}>
|
||||
<div className={styles.modalHeaderLeft}>
|
||||
<h2 className={styles.modalSubject}>{email.subject ?? '(kein Betreff)'}</h2>
|
||||
{!email.isRead && (
|
||||
<span className={styles.modalUnreadBadge}>Ungelesen</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.modalClose}
|
||||
onClick={onClose}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className={styles.modalBody}>
|
||||
{/* Meta: Von / An / Datum / Anhang */}
|
||||
<div className={styles.metaTable}>
|
||||
<span className={styles.metaLabel}>Von</span>
|
||||
<span className={styles.metaValue}>{fromName}</span>
|
||||
|
||||
<span className={styles.metaLabel}>An</span>
|
||||
<span className={styles.metaValue}>{toText}</span>
|
||||
|
||||
<span className={styles.metaLabel}>Datum</span>
|
||||
<span className={styles.metaValue}>{formatEmailDate(email.receivedDateTime)}</span>
|
||||
|
||||
{email.hasAttachments && (
|
||||
<>
|
||||
<span className={styles.metaLabel}>Anhang</span>
|
||||
<span className={styles.metaValue}>📎 Anhänge vorhanden</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body-Vorschau */}
|
||||
{email.bodyPreview ? (
|
||||
<>
|
||||
<div className={styles.bodyPreview}>{email.bodyPreview}</div>
|
||||
<p className={styles.bodyHint}>
|
||||
Vorschau — vollständige E-Mail in Outlook öffnen
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className={styles.bodyHint}>Kein Vorschautext verfügbar</p>
|
||||
)}
|
||||
|
||||
{/* In Kontakt speichern */}
|
||||
<div className={styles.saveSection}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.saveBtn}${saveState === 'saved' ? ` ${styles.saveBtnSuccess}` : ''}${saveState === 'error' ? ` ${styles.saveBtnError}` : ''}`}
|
||||
onClick={handleSave}
|
||||
disabled={saveState === 'saving' || saveState === 'saved'}
|
||||
>
|
||||
{saveState === 'idle' && '+ In Kontakt speichern'}
|
||||
{saveState === 'saving' && 'Speichern…'}
|
||||
{saveState === 'saved' && '✓ Gespeichert'}
|
||||
{saveState === 'error' && '✗ Fehler – erneut versuchen'}
|
||||
</button>
|
||||
{saveState === 'saved' && (
|
||||
<span className={styles.saveMsg}>
|
||||
E-Mail als Aktivität gespeichert
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className={styles.modalFooter}>
|
||||
<span className={styles.modalFooterDate}>
|
||||
{formatEmailDate(email.receivedDateTime)}
|
||||
</span>
|
||||
<a
|
||||
href={email.webLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.outlookBtn}
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M6 2H2a1 1 0 00-1 1v10a1 1 0 001 1h12a1 1 0 001-1V8" />
|
||||
<path d="M10 1h5v5" />
|
||||
<path d="M6 10L15 1" />
|
||||
</svg>
|
||||
In Outlook öffnen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Hauptkomponente ──────────────────────────────────────────────────────── */
|
||||
|
||||
export function EmailsTab({ contactId }: Props) {
|
||||
const { data: integrationsData } = useIntegrations();
|
||||
const isConnected = integrationsData?.data?.some(
|
||||
(i) => i.provider === 'MICROSOFT_365' && i.connected,
|
||||
) ?? false;
|
||||
const isConnected =
|
||||
(integrationsData?.data?.some(
|
||||
(i) => i.provider === 'MICROSOFT_365' && i.connected,
|
||||
) ?? false);
|
||||
|
||||
const { data, isLoading, error } = useContactEmails(contactId);
|
||||
const emails: M365Email[] = data?.data ?? [];
|
||||
const [selectedEmail, setSelectedEmail] = useState<M365Email | null>(null);
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div style={{ padding: '1.5rem 0', textAlign: 'center' }}>
|
||||
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.9375rem', marginBottom: '1rem' }}>
|
||||
<p
|
||||
style={{
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: '0.9375rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
Verbinden Sie Microsoft 365, um E-Mails zu diesem Kontakt zu sehen.
|
||||
</p>
|
||||
<button
|
||||
|
|
@ -55,68 +232,99 @@ export function EmailsTab({ contactId }: Props) {
|
|||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', padding: '1rem 0' }}>Laden…</p>;
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: '0.875rem',
|
||||
padding: '1rem 0',
|
||||
}}
|
||||
>
|
||||
Laden…
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <p style={{ color: 'var(--color-error)', fontSize: '0.875rem', padding: '1rem 0' }}>E-Mails konnten nicht geladen werden.</p>;
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
color: 'var(--color-error)',
|
||||
fontSize: '0.875rem',
|
||||
padding: '1rem 0',
|
||||
}}
|
||||
>
|
||||
E-Mails konnten nicht geladen werden.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (emails.length === 0) {
|
||||
return <p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem', padding: '1rem 0' }}>Keine E-Mails gefunden.</p>;
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
color: 'var(--color-text-muted)',
|
||||
fontSize: '0.875rem',
|
||||
padding: '1rem 0',
|
||||
}}
|
||||
>
|
||||
Keine E-Mails gefunden.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{emails.map((email) => (
|
||||
<a
|
||||
key={email.id}
|
||||
href={email.webLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '0.75rem 1rem',
|
||||
background: email.isRead ? 'var(--color-bg-card)' : 'var(--color-bg-subtle, var(--color-bg-card))',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
textDecoration: 'none',
|
||||
borderLeft: email.isRead ? undefined : '3px solid var(--color-primary)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '0.5rem' }}>
|
||||
<span style={{
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: email.isRead ? 400 : 600,
|
||||
color: 'var(--color-text)',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{email.subject || '(kein Betreff)'}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', flexShrink: 0 }}>
|
||||
{formatEmailDate(email.receivedDateTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginTop: '0.25rem' }}>
|
||||
Von: {email.from?.emailAddress?.name ?? email.from?.emailAddress?.address ?? '—'}
|
||||
</div>
|
||||
{email.bodyPreview && (
|
||||
<div style={{
|
||||
fontSize: '0.8125rem',
|
||||
color: 'var(--color-text-secondary)',
|
||||
marginTop: '0.25rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{email.bodyPreview}
|
||||
<>
|
||||
<div className={styles.emailList}>
|
||||
{emails.map((email) => (
|
||||
<button
|
||||
key={email.id}
|
||||
type="button"
|
||||
className={`${styles.emailItem}${!email.isRead ? ` ${styles.emailItemUnread}` : ''}`}
|
||||
onClick={() => setSelectedEmail(email)}
|
||||
>
|
||||
{!email.isRead ? (
|
||||
<span className={styles.unreadDot} aria-hidden="true" />
|
||||
) : (
|
||||
<span className={styles.readSpacer} />
|
||||
)}
|
||||
<div className={styles.emailMeta}>
|
||||
<div className={styles.emailRow1}>
|
||||
<span
|
||||
className={`${styles.emailSubject}${!email.isRead ? ` ${styles.emailSubjectUnread}` : ''}`}
|
||||
>
|
||||
{email.subject ?? '(kein Betreff)'}
|
||||
</span>
|
||||
<span className={styles.emailTime}>
|
||||
{formatEmailTime(email.receivedDateTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.emailFrom}>
|
||||
Von:{' '}
|
||||
{email.from?.emailAddress?.name ??
|
||||
email.from?.emailAddress?.address ??
|
||||
'—'}
|
||||
</div>
|
||||
{email.bodyPreview && (
|
||||
<div className={styles.emailPreview}>{email.bodyPreview}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
{email.hasAttachments && (
|
||||
<span className={styles.attachBadge} title="Anhänge vorhanden">
|
||||
📎
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedEmail !== null && (
|
||||
<EmailDetailModal
|
||||
email={selectedEmail}
|
||||
contactId={contactId}
|
||||
onClose={() => setSelectedEmail(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
.tabBar {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
margin-bottom: 1.75rem;
|
||||
|
|
@ -55,6 +56,102 @@
|
|||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* ── Profil-Bereich in der Tab-Leiste (rechts) ── */
|
||||
|
||||
.tabBarProfile {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0 0.75rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
/* leicht farblich abgesetzt */
|
||||
box-shadow: inset 0 -2px 0 var(--color-border);
|
||||
}
|
||||
|
||||
.tabBarThemeGroup {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.tabBarThemeBtn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.tabBarThemeBtn:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.tabBarThemeBtnActive {
|
||||
background: var(--color-primary) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.tabBarUser {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.1875rem 0.4375rem 0.1875rem 0.1875rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tabBarUser:hover {
|
||||
background: var(--color-bg);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.tabBarUserName {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tabBarLogout {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tabBarLogout:hover {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* ── Home-Layout (Inhalt links + Agenda rechts) ── */
|
||||
|
||||
.homeLayout {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { UserAvatar } from '../components/UserAvatar';
|
||||
import { useTheme } from '../theme/ThemeContext';
|
||||
import { WeatherWidget } from '../components/WeatherWidget';
|
||||
import { AnalogClock } from '../components/AnalogClock';
|
||||
import { DashboardEmailTab } from './DashboardEmailTab';
|
||||
|
|
@ -688,11 +690,24 @@ function HomeTab({
|
|||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
const THEME_OPTIONS_DASH = [
|
||||
{ value: 'light' as const, icon: '☀' },
|
||||
{ value: 'dark' as const, icon: '☾' },
|
||||
{ value: 'system' as const, icon: '⚙' },
|
||||
];
|
||||
|
||||
export function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const { user, logout } = useAuth();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { mode, setMode } = useTheme();
|
||||
const [activeTab, setActiveTab] = useState<DashboardTab>('home');
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
// Immer auf Home-Tab springen wenn Dashboard-NavLink geklickt wird
|
||||
useEffect(() => {
|
||||
setActiveTab('home');
|
||||
|
|
@ -712,6 +727,69 @@ export function DashboardPage() {
|
|||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Profil-Bereich rechts in der Tab-Leiste */}
|
||||
<div className={styles.tabBarProfile}>
|
||||
{/* Theme-Schalter */}
|
||||
<div className={styles.tabBarThemeGroup}>
|
||||
{THEME_OPTIONS_DASH.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
className={`${styles.tabBarThemeBtn}${mode === opt.value ? ` ${styles.tabBarThemeBtnActive}` : ''}`}
|
||||
onClick={() => setMode(opt.value)}
|
||||
title={opt.value === 'light' ? 'Hell' : opt.value === 'dark' ? 'Dunkel' : 'System'}
|
||||
>
|
||||
{opt.icon}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Benutzer (→ Profil) */}
|
||||
<div
|
||||
className={styles.tabBarUser}
|
||||
onClick={() => navigate('/profile')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') navigate('/profile');
|
||||
}}
|
||||
title="Profil bearbeiten"
|
||||
>
|
||||
<UserAvatar
|
||||
firstName={user?.firstName ?? ''}
|
||||
lastName={user?.lastName ?? ''}
|
||||
avatar={user?.avatar}
|
||||
size={24}
|
||||
/>
|
||||
<span className={styles.tabBarUserName}>
|
||||
{user?.firstName} {user?.lastName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Abmelden */}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.tabBarLogout}
|
||||
onClick={handleLogout}
|
||||
title="Abmelden"
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M10 3h3a1 1 0 011 1v8a1 1 0 01-1 1h-3" />
|
||||
<path d="M7 11l3-3-3-3" />
|
||||
<path d="M10 8H3" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab-Inhalt */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue