Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-10 10:32:52 +00:00
committet von Server Deploy
Ursprung 7d67557be4
Commit ef153789cc
20 geänderte Dateien mit 13613 neuen und 333 gelöschten Zeilen

Datei anzeigen

@ -156,36 +156,99 @@
font-size: var(--text-sm);
}
/* View Tabs */
/* View Tabs - Modern Design */
.view-tabs {
display: flex;
gap: 2px;
padding: 3px;
background: var(--bg-tertiary);
gap: var(--spacing-1);
padding: 8px 12px;
background: rgba(0, 0, 0, 0.03);
border-radius: var(--radius-lg);
position: relative;
backdrop-filter: blur(10px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.view-tab {
padding: 6px 12px;
position: relative;
display: flex;
align-items: center;
gap: var(--spacing-2);
padding: 12px 20px;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-tertiary);
background: none;
border: none;
border-radius: var(--radius-md);
border-radius: 0;
cursor: pointer;
transition: all var(--transition-fast);
transition: all var(--transition-default);
white-space: nowrap;
}
.view-tab:hover {
color: var(--text-secondary);
/* Tab Icon */
.view-tab svg {
width: 18px;
height: 18px;
flex-shrink: 0;
transition: all var(--transition-default);
}
.view-tab.active {
/* Hover State */
.view-tab:hover {
color: var(--text-primary);
background: var(--bg-card);
box-shadow: var(--shadow-sm);
background: rgba(0, 0, 0, 0.05);
border-radius: var(--radius-md);
}
.view-tab:hover svg {
transform: translateY(-1px);
}
/* Active State with Underline */
.view-tab.active {
color: var(--primary);
background: rgba(59, 130, 246, 0.1);
border-radius: var(--radius-md);
}
.view-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary);
border-radius: 3px 3px 0 0;
animation: slideIn 0.3s ease-out;
}
/* Animation for active indicator */
@keyframes slideIn {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
/* Subtle border between tabs */
.view-tab:not(:last-child)::before {
content: '';
position: absolute;
right: 0;
top: 25%;
bottom: 25%;
width: 1px;
background: var(--border-light);
opacity: 0.5;
transition: opacity var(--transition-fast);
}
.view-tab:hover::before,
.view-tab:hover + .view-tab::before {
opacity: 0;
}
/* Search */

Datei anzeigen

@ -691,12 +691,16 @@
position: fixed;
min-width: 280px;
max-width: 350px;
max-height: 80vh;
padding: var(--spacing-4);
background: var(--bg-card);
border: 1px solid var(--border-default);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
z-index: var(--z-dropdown);
display: flex;
flex-direction: column;
overflow: hidden;
}
.calendar-day-detail-header {
@ -717,8 +721,9 @@
display: flex;
flex-direction: column;
gap: var(--spacing-2);
max-height: 300px;
flex: 1;
overflow-y: auto;
margin-bottom: var(--spacing-3);
}
.calendar-detail-task {

Datei anzeigen

@ -1,9 +1,29 @@
/**
* TASKMATE - Contacts Styles
* ==========================
* Kartenansicht für Kontakte
* Tabellenansicht für Kontakte mit erweiterten Funktionen
*/
/* =============================================================================
VIEW CONTAINER
============================================================================= */
.view-contacts {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.view-contacts .view-wrapper {
flex: 1;
overflow-y: auto;
padding: var(--spacing-6);
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
/* =============================================================================
HEADER & CONTROLS
============================================================================= */
@ -12,165 +32,299 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-md);
margin-bottom: var(--spacing-6);
flex-wrap: wrap;
gap: var(--space-sm);
gap: var(--spacing-4);
}
.contacts-controls {
.contacts-header h2 {
font-size: var(--text-2xl);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0;
}
.header-actions {
display: flex;
gap: var(--space-sm);
gap: var(--spacing-3);
align-items: center;
}
.contacts-stats {
display: flex;
align-items: center;
gap: var(--spacing-2);
color: var(--text-secondary);
font-size: var(--text-sm);
padding: var(--spacing-2) var(--spacing-3);
background: var(--bg-secondary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-default);
}
/* =============================================================================
CONTROLS BAR
============================================================================= */
.contacts-controls {
background: var(--bg-secondary);
border: 1px solid var(--border-default);
border-radius: var(--radius-xl);
padding: var(--spacing-4);
margin-bottom: var(--spacing-5);
}
.contacts-controls-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-4);
flex-wrap: wrap;
justify-content: flex-end;
}
.bulk-actions {
display: flex;
gap: var(--spacing-3);
align-items: center;
}
.bulk-actions.hidden {
display: none;
}
.bulk-actions-info {
font-size: var(--text-sm);
color: var(--text-primary);
font-weight: var(--font-medium);
}
.contacts-filters {
display: flex;
gap: var(--space-xs);
gap: var(--spacing-4);
align-items: center;
flex-wrap: wrap;
}
/* =============================================================================
GRID LAYOUT
============================================================================= */
.contacts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-md);
margin-top: var(--space-md);
}
/* =============================================================================
CONTACT CARD
============================================================================= */
.contact-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: var(--space-md);
transition: all 0.2s;
cursor: pointer;
.filter-group {
position: relative;
}
.contact-card:hover {
.filter-select {
min-width: 180px;
height: 36px;
padding: 0 var(--spacing-4);
padding-right: 36px;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
color: var(--text-primary);
font-size: var(--text-sm);
font-weight: var(--font-medium);
appearance: none;
cursor: pointer;
transition: all var(--transition-fast);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--spacing-2) center;
background-size: 14px;
}
.filter-select:hover {
border-color: var(--border-dark);
background-color: var(--bg-hover);
}
.filter-select:focus {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-focus);
outline: none;
}
.contact-card-header {
/* =============================================================================
TABLE LAYOUT
============================================================================= */
.contacts-table-container {
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-xl);
overflow: hidden;
}
.contacts-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.contacts-table th,
.contacts-table td {
padding: var(--spacing-3) var(--spacing-4);
text-align: left;
white-space: nowrap;
}
.contacts-table th {
background: var(--bg-secondary);
font-weight: var(--font-semibold);
color: var(--text-primary);
font-size: var(--text-sm);
position: sticky;
top: 0;
z-index: 10;
border-bottom: 2px solid var(--border-default);
}
.contacts-table th:first-child {
padding-left: var(--spacing-4);
width: 40px;
}
.contacts-table th:last-child {
padding-right: var(--spacing-4);
text-align: right;
}
/* Sortable headers */
.sortable {
cursor: pointer;
user-select: none;
position: relative;
padding-right: 24px;
}
.sortable:hover {
color: var(--primary);
}
.sortable::after {
content: '';
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
opacity: 0.3;
}
.sortable.sort-asc::after {
border-bottom: 6px solid var(--primary);
opacity: 1;
}
.sortable.sort-desc::after {
border-top: 6px solid var(--primary);
opacity: 1;
}
/* Table rows */
.contacts-table tbody tr {
border-bottom: 1px solid var(--border-default);
transition: background-color var(--transition-fast);
}
.contacts-table tbody tr:hover {
background-color: var(--bg-hover);
}
.contacts-table tbody tr.selected {
background-color: var(--bg-tertiary);
}
.contacts-table td {
color: var(--text-secondary);
font-size: var(--text-sm);
}
.contacts-table td:first-child {
padding-left: var(--spacing-4);
}
.contacts-table td:last-child {
padding-right: var(--spacing-4);
}
/* Checkbox column */
.checkbox-cell {
width: 40px;
text-align: center !important;
}
.table-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
appearance: none;
border: 2px solid var(--border-default);
border-radius: var(--radius-sm);
background: var(--bg-primary);
position: relative;
transition: all var(--transition-fast);
}
.table-checkbox:hover {
border-color: var(--primary);
}
.table-checkbox:checked {
background: var(--primary);
border-color: var(--primary);
}
.table-checkbox:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
}
/* Name column with avatar */
.name-cell {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-sm);
align-items: center;
gap: var(--spacing-3);
min-width: 200px;
}
.contact-avatar {
width: 48px;
height: 48px;
.contact-avatar-small {
width: 32px;
height: 32px;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
font-weight: var(--font-semibold);
font-size: 12px;
flex-shrink: 0;
}
.contact-actions {
opacity: 0;
transition: opacity 0.2s;
}
.contact-card:hover .contact-actions {
opacity: 1;
}
.contact-actions .btn-icon {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
width: 32px;
height: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
transition: all 0.2s;
}
.contact-actions .btn-icon:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* =============================================================================
CONTACT INFO
============================================================================= */
.contact-card-body {
margin-bottom: var(--space-sm);
}
.contact-name {
font-size: 18px;
font-weight: 600;
margin: 0 0 var(--space-xs);
.contact-name-link {
color: var(--text-primary);
font-weight: var(--font-medium);
text-decoration: none;
cursor: pointer;
}
.contact-company {
font-size: 14px;
.contact-name-link:hover {
color: var(--primary);
margin-bottom: 4px;
text-decoration: underline;
}
.contact-position {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: var(--space-sm);
}
.contact-email,
.contact-phone,
.contact-mobile {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.contact-email i,
.contact-phone i,
.contact-mobile i {
width: 14px;
color: var(--text-tertiary);
}
.contact-email:hover,
.contact-phone:hover,
.contact-mobile:hover {
color: var(--primary);
}
/* =============================================================================
TAGS
============================================================================= */
.contact-tags {
/* Tags cell */
.tags-cell {
display: flex;
gap: var(--spacing-1);
flex-wrap: wrap;
gap: 4px;
margin-top: var(--space-sm);
max-width: 200px;
}
.contact-tag {
@ -178,8 +332,60 @@
color: var(--text-secondary);
font-size: 11px;
padding: 2px 8px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
border: 1px solid var(--border-default);
font-weight: var(--font-medium);
white-space: nowrap;
}
/* Actions column */
.actions-cell {
text-align: right !important;
}
.table-actions {
display: flex;
gap: var(--spacing-2);
justify-content: flex-end;
}
.btn-table-action {
background: transparent;
border: 1px solid var(--border-default);
color: var(--text-secondary);
width: 32px;
height: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
cursor: pointer;
}
.btn-table-action:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
transform: scale(1.05);
}
.btn-table-action svg {
width: 16px;
height: 16px;
}
/* Export button */
.btn-export {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.btn-export svg {
width: 16px;
height: 16px;
}
/* =============================================================================
@ -188,27 +394,35 @@
.contacts-empty {
text-align: center;
padding: var(--space-xl) var(--space-md);
padding: var(--spacing-8) var(--spacing-4);
background: var(--bg-secondary);
border-radius: var(--radius);
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
border: 1px solid var(--border-default);
max-width: 500px;
margin: var(--spacing-8) auto;
}
.contacts-empty i {
font-size: 48px;
.contacts-empty .empty-icon {
color: var(--text-tertiary);
margin-bottom: var(--space-md);
margin-bottom: var(--spacing-4);
opacity: 0.5;
}
.contacts-empty .empty-icon svg {
width: 64px;
height: 64px;
}
.contacts-empty h3 {
font-size: 20px;
margin-bottom: var(--space-xs);
font-size: var(--text-xl);
font-weight: var(--font-semibold);
margin-bottom: var(--spacing-2);
color: var(--text-primary);
}
.contacts-empty p {
color: var(--text-secondary);
margin-bottom: var(--space-md);
margin-bottom: var(--spacing-4);
}
/* =============================================================================
@ -244,29 +458,105 @@
margin-top: 4px;
}
/* =============================================================================
PAGINATION
============================================================================= */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-2);
padding: var(--spacing-4);
border-top: 1px solid var(--border-default);
background: var(--bg-secondary);
}
.pagination-btn {
background: var(--bg-primary);
border: 1px solid var(--border-default);
color: var(--text-secondary);
padding: var(--spacing-2) var(--spacing-3);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: all var(--transition-fast);
}
.pagination-btn:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--primary);
color: var(--primary);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: var(--text-secondary);
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
/* =============================================================================
RESPONSIVE
============================================================================= */
@media (max-width: 768px) {
@media (max-width: 1200px) {
.contacts-table-container {
overflow-x: auto;
}
.contacts-table {
min-width: 900px;
}
}
@media (max-width: 968px) {
.view-contacts .view-wrapper {
padding: var(--spacing-4);
}
.contacts-header {
flex-direction: column;
align-items: stretch;
}
.contacts-controls {
.header-actions {
flex-direction: column;
width: 100%;
gap: var(--spacing-2);
}
.contacts-search {
max-width: none;
.contacts-controls {
padding: var(--spacing-3);
}
.contacts-grid {
grid-template-columns: 1fr;
.contacts-controls-top {
flex-direction: column;
align-items: stretch;
}
.bulk-actions {
width: 100%;
justify-content: space-between;
}
.filter-select {
min-width: 120px;
font-size: var(--text-xs);
}
.contacts-table th,
.contacts-table td {
padding: var(--spacing-2) var(--spacing-3);
}
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
@ -280,4 +570,9 @@
width: 100%;
justify-content: space-between;
}
/* Mobile: Hide less important columns */
.hide-mobile {
display: none;
}
}

Datei anzeigen

@ -112,11 +112,102 @@
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Advance Options */
/* Time Input Styling - Match Date Input */
#reminder-modal input[type="time"] {
width: 100%;
padding: 10px 14px;
font-family: var(--font-primary);
font-size: var(--text-sm);
color: var(--text-primary);
background: var(--bg-input);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
min-width: 140px;
}
#reminder-modal input[type="time"]:hover {
border-color: var(--border-dark);
}
#reminder-modal input[type="time"]:focus {
border-color: var(--primary);
box-shadow: var(--shadow-focus);
outline: none;
}
/* Advance Options - Redesigned */
.advance-options {
display: flex;
flex-direction: column;
gap: 8px;
gap: var(--spacing-3);
background: var(--bg-secondary);
padding: var(--spacing-3);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
}
/* Reminder Advance Control */
.reminder-advance-control {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.reminder-advance-inputs {
display: flex;
align-items: center;
gap: var(--spacing-2);
flex-wrap: wrap;
}
.reminder-number-input {
width: 70px;
text-align: center;
padding: 10px 8px;
font-size: var(--text-base);
font-weight: var(--font-medium);
background: white;
border: 2px solid var(--border-default);
border-radius: var(--radius-md);
}
.reminder-number-input:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.reminder-unit-select {
flex: 1;
min-width: 120px;
max-width: 180px;
padding: 10px 16px;
font-size: var(--text-base);
font-weight: var(--font-medium);
background: white;
border: 2px solid var(--border-default);
border-radius: var(--radius-md);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 40px;
}
.reminder-unit-select:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.reminder-advance-suffix {
font-size: var(--text-base);
color: var(--text-primary);
font-weight: var(--font-semibold);
white-space: nowrap;
}
.checkbox-label {
@ -175,7 +266,7 @@
}
/* =====================
CUSTOM SELECT (USER DROPDOWN)
CUSTOM SELECT (USER DROPDOWN) - Aligned with Multi-Select Design
===================== */
.custom-select {
@ -187,35 +278,38 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--bg-primary);
padding: 8px 14px;
background: white;
border: 1px solid var(--border-default);
border-radius: 6px;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all 0.2s ease;
transition: all var(--transition-fast);
min-height: 42px;
}
.custom-select-trigger:hover {
border-color: var(--border-hover);
border-color: var(--border-dark);
}
.custom-select-trigger.active {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.custom-select-value {
display: flex;
align-items: center;
gap: 8px;
gap: var(--spacing-2);
color: var(--text-primary);
font-size: 14px;
}
.custom-select-arrow {
color: var(--text-secondary);
transition: transform 0.2s ease;
width: 16px;
height: 16px;
color: var(--text-tertiary);
transition: transform var(--transition-fast);
flex-shrink: 0;
}
.custom-select.open .custom-select-arrow {
@ -227,52 +321,48 @@
top: 100%;
left: 0;
right: 0;
background: var(--bg-card);
margin-top: 4px;
background: #ffffff;
border: 1px solid var(--border-default);
border-radius: 6px;
box-shadow: var(--shadow-lg);
z-index: 1000;
max-height: 200px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
z-index: 1001;
max-height: 240px;
overflow-y: auto;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
min-width: 200px;
display: none;
/* Ensure opaque background */
background-color: #ffffff !important;
}
.custom-select.open .custom-select-options {
opacity: 1;
visibility: visible;
transform: translateY(0);
display: block;
}
.custom-select-option {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
gap: var(--spacing-3);
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--border-light);
}
.custom-select-option:last-child {
border-bottom: none;
transition: background-color var(--transition-fast);
color: var(--text-primary);
background-color: #ffffff;
position: relative;
}
.custom-select-option:hover {
background: var(--bg-hover);
background-color: #f3f4f6 !important;
}
.custom-select-option.selected {
background: var(--primary-light);
color: var(--primary);
background-color: #e0e7ff !important;
}
.option-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
@ -284,7 +374,8 @@
.option-text {
font-size: 14px;
color: #000000 !important;
color: var(--text-primary);
flex: 1;
}
.selected-user-avatar {
@ -325,11 +416,22 @@
transition: background-color 0.2s ease, color 0.2s ease;
}
#reminder-modal *:not(.btn):not(.custom-select-option):hover {
#reminder-modal *:not(.btn):not(.custom-select-option):not(.option-avatar):not(.option-text):hover {
background: transparent !important;
color: inherit !important;
}
/* Ensure dropdown options have solid background */
#reminder-modal .custom-select-options {
background-color: #ffffff !important;
isolation: isolate;
}
/* Keep text elements transparent but allow avatars to have background */
#reminder-modal .custom-select-option .option-text {
background: transparent !important;
}
/* Stelle sicher, dass Text im Modal immer lesbar bleibt */
#reminder-modal .modal-content {
background: var(--bg-card) !important;
@ -395,10 +497,11 @@
===================== */
/* Reminder Button in Calendar Toolbar */
.btn-reminder {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
.btn-reminder,
.btn-secondary.btn-reminder {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
color: white !important;
border: 2px solid #ea580c;
border: 2px solid #ea580c !important;
display: flex;
align-items: center;
gap: 6px;
@ -413,10 +516,12 @@
}
.btn-reminder:hover,
.btn-reminder:focus {
background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
.btn-reminder:focus,
.btn-secondary.btn-reminder:hover,
.btn-secondary.btn-reminder:focus {
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%) !important;
color: white !important;
border-color: #c2410c;
border-color: #dc2626 !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(234, 88, 12, 0.4);
text-decoration: none;
@ -475,6 +580,66 @@
border: none;
}
/* Calendar Reminder Add Button - Orange styling */
.calendar-week-add-reminder {
margin: var(--spacing-2) var(--spacing-3) var(--spacing-3);
padding: var(--spacing-2);
background: #f97316;
border: 1px solid #f97316;
border-radius: var(--radius-lg);
color: white !important;
font-size: var(--text-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: all var(--transition-fast);
}
.calendar-week-add-reminder:hover {
background: #ea580c;
border-color: #dc2626;
color: white !important;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4);
}
/* Secondary reminder button - orange variant */
.btn-reminder-secondary,
.btn.btn-reminder-secondary {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
color: white !important;
border: 2px solid #ea580c !important;
font-weight: var(--font-medium);
transition: all var(--transition-fast);
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 6px !important;
}
.btn-reminder-secondary .icon,
.btn.btn-reminder-secondary .icon {
color: white !important;
width: 18px !important;
height: 18px !important;
}
.btn-reminder-secondary:hover,
.btn-reminder-secondary:focus,
.btn.btn-reminder-secondary:hover,
.btn.btn-reminder-secondary:focus {
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%) !important;
color: white !important;
border-color: #dc2626 !important;
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(249, 115, 22, 0.4);
}
.btn-reminder-secondary:active,
.btn.btn-reminder-secondary:active {
transform: translateY(0);
box-shadow: 0 1px 4px rgba(249, 115, 22, 0.3);
}
/* Reminder Detail Popup */
.calendar-reminder-detail {
min-width: 300px;

Datei anzeigen

@ -173,13 +173,67 @@
<div class="header-center">
<!-- View Tabs -->
<nav class="view-tabs">
<button class="view-tab active" data-view="board">Board</button>
<button class="view-tab" data-view="list">Liste</button>
<button class="view-tab" data-view="calendar">Kalender</button>
<button class="view-tab" data-view="proposals">Genehmigung</button>
<button class="view-tab" data-view="coding">Coding</button>
<button class="view-tab" data-view="knowledge">Wissen</button>
<button class="view-tab" data-view="contacts">Kontakte</button>
<button class="view-tab active" data-view="board">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1"/>
<rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/>
<rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
Board
</button>
<button class="view-tab" data-view="list">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="8" y1="6" x2="21" y2="6"/>
<line x1="8" y1="12" x2="21" y2="12"/>
<line x1="8" y1="18" x2="21" y2="18"/>
<circle cx="3.5" cy="6" r="1" fill="currentColor"/>
<circle cx="3.5" cy="12" r="1" fill="currentColor"/>
<circle cx="3.5" cy="18" r="1" fill="currentColor"/>
</svg>
Liste
</button>
<button class="view-tab" data-view="calendar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
<circle cx="12" cy="16" r="1" fill="currentColor"/>
</svg>
Kalender
</button>
<button class="view-tab" data-view="proposals">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 4"/>
<path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/>
</svg>
Genehmigung
</button>
<button class="view-tab" data-view="coding">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 18 22 12 16 6"/>
<polyline points="8 6 2 12 8 18"/>
<line x1="12" y1="20" x2="12" y2="4" transform="rotate(-15 12 12)"/>
</svg>
Coding
</button>
<button class="view-tab" data-view="knowledge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
</svg>
Wissen
</button>
<button class="view-tab" data-view="contacts">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Kontakte
</button>
</nav>
</div>
@ -579,42 +633,103 @@
<!-- Contacts View -->
<div id="view-contacts" class="view view-contacts hidden">
<div class="contacts-header">
<h2>Kontakte</h2>
<button id="btn-new-contact" class="btn btn-primary">
<i class="fas fa-plus"></i>
Neuer Kontakt
</button>
</div>
<div class="contacts-controls">
<div class="contacts-filters">
<select id="contacts-tag-filter">
<option value="">Alle Tags</option>
</select>
<select id="contacts-sort">
<option value="created_at-desc">Neueste zuerst</option>
<option value="created_at-asc">Älteste zuerst</option>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="company-asc">Firma (A-Z)</option>
<option value="company-desc">Firma (Z-A)</option>
</select>
<div class="view-wrapper">
<div class="contacts-header">
<h2>Kontakte</h2>
<div class="header-actions">
<div class="contacts-stats">
<span id="contacts-count">0 Kontakte</span>
</div>
<button id="btn-export-contacts" class="btn btn-secondary btn-export">
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Exportieren
</button>
<button id="btn-new-contact" class="btn btn-primary">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
Neuer Kontakt
</button>
</div>
</div>
</div>
<div id="contacts-grid" class="contacts-grid">
<!-- Contact cards will be rendered here -->
</div>
<div class="contacts-controls">
<div class="contacts-controls-top">
<div class="bulk-actions hidden" id="bulk-actions">
<span class="bulk-actions-info"><span id="selected-count">0</span> ausgewählt</span>
<button id="btn-bulk-delete" class="btn btn-danger btn-sm">
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Löschen
</button>
<button id="btn-deselect-all" class="btn btn-secondary btn-sm">Abwählen</button>
</div>
<div class="contacts-filters">
<div class="filter-group">
<label for="contacts-tag-filter" class="sr-only">Tags filtern</label>
<select id="contacts-tag-filter" class="filter-select">
<option value="">Alle Tags</option>
</select>
</div>
</div>
</div>
</div>
<div id="contacts-empty" class="contacts-empty hidden">
<i class="fas fa-address-book"></i>
<h3>Keine Kontakte vorhanden</h3>
<p>Erstellen Sie Ihren ersten Kontakt.</p>
<button class="btn btn-primary" onclick="document.getElementById('btn-new-contact').click()">
<i class="fas fa-plus"></i>
Ersten Kontakt erstellen
</button>
<div class="contacts-table-container">
<table class="contacts-table" id="contacts-table">
<thead>
<tr>
<th class="checkbox-cell">
<input type="checkbox" class="table-checkbox" id="select-all-contacts">
</th>
<th class="sortable" data-sort="name">Name</th>
<th class="sortable" data-sort="company">Firma</th>
<th class="sortable hide-mobile" data-sort="position">Position</th>
<th class="sortable" data-sort="email">E-Mail</th>
<th class="hide-mobile">Telefon</th>
<th>Tags</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="contacts-tbody">
<!-- Table rows will be rendered here -->
</tbody>
</table>
<div class="pagination hidden" id="contacts-pagination">
<button class="pagination-btn" id="btn-prev-page" disabled>
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<span class="pagination-info">
Seite <span id="current-page">1</span> von <span id="total-pages">1</span>
</span>
<button class="pagination-btn" id="btn-next-page" disabled>
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
<div id="contacts-empty" class="contacts-empty hidden">
<div class="empty-icon">
<svg viewBox="0 0 24 24" width="64" height="64" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3>Keine Kontakte vorhanden</h3>
<p>Erstellen Sie Ihren ersten Kontakt.</p>
<button class="btn btn-primary" onclick="document.getElementById('btn-new-contact').click()">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
Ersten Kontakt erstellen
</button>
</div>
</div>
</div>
@ -1915,23 +2030,19 @@
<input type="hidden" id="reminder-color" value="#F59E0B">
</div>
<div class="form-group">
<label for="reminder-advance">Erinnerung</label>
<label for="reminder-advance">Vorab-Erinnerung</label>
<div class="advance-options">
<label class="checklist-item">
<input type="checkbox" name="advance-days" value="1" checked>
<span class="checklist-checkbox"></span>
<span class="checklist-text">1 Tag vorher</span>
</label>
<label class="checklist-item">
<input type="checkbox" name="advance-days" value="2">
<span class="checklist-checkbox"></span>
<span class="checklist-text">2 Tage vorher</span>
</label>
<label class="checklist-item">
<input type="checkbox" name="advance-days" value="3">
<span class="checklist-checkbox"></span>
<span class="checklist-text">3 Tage vorher</span>
</label>
<div class="reminder-advance-control">
<div class="reminder-advance-inputs">
<input type="number" id="reminder-advance-number" min="1" max="9" value="1" class="form-control reminder-number-input">
<select id="reminder-advance-unit" class="form-control reminder-unit-select">
<option value="day">Tag(e)</option>
<option value="week">Woche(n)</option>
<option value="month">Monat(e)</option>
</select>
<span class="reminder-advance-suffix">vorher</span>
</div>
</div>
</div>
</div>
</div>

Datei anzeigen

@ -1284,6 +1284,14 @@ class ApiClient {
return this.post('/coding/validate-path', { path });
}
async getCodingDirectoryUsage(id) {
return this.get(`/coding/directories/${id}/usage`);
}
async getCodingDirectoryUsageHistory(id, hours = 24) {
return this.get(`/coding/directories/${id}/usage/history?hours=${hours}`);
}
// =============================================================================
// CONTACTS
// =============================================================================

Datei anzeigen

@ -733,7 +733,7 @@ class App {
users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.username;
option.textContent = user.displayName || user.email || 'Unbekannt';
select.appendChild(option);
});
}

Datei anzeigen

@ -524,9 +524,19 @@ class CalendarViewManager {
e.stopPropagation();
this.createTaskForDate(dateString);
}
}, ['+ Aufgabe']);
}, ['Aufgabe hinzufügen']);
dayEl.appendChild(addBtn);
// Add reminder button
const addReminderBtn = createElement('button', {
className: 'calendar-week-add-reminder',
onclick: (e) => {
e.stopPropagation();
this.createReminderForDate(dateString);
}
}, ['Erinnerung hinzufügen']);
dayEl.appendChild(addReminderBtn);
return dayEl;
}
@ -920,28 +930,57 @@ class CalendarViewManager {
className: 'btn btn-primary btn-block',
style: { marginTop: 'var(--spacing-md)' },
onclick: () => this.createTaskForDate(dateString)
}, ['+ Aufgabe hinzufügen']));
}, ['Aufgabe hinzufügen']));
// Add reminder button
popup.appendChild(createElement('button', {
className: 'btn btn-secondary btn-block',
className: 'btn btn-reminder-secondary btn-block',
style: { marginTop: 'var(--spacing-sm)' },
onclick: () => this.createReminderForDate(dateString)
}, ['🔔 Erinnerung hinzufügen']));
}));
// Add HTML content with SVG icon
const button = popup.lastElementChild;
button.innerHTML = '<svg class="icon" viewBox="0 0 24 24" width="18" height="18" style="margin-right: 6px; flex-shrink: 0;"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 0 1-3.46 0" stroke="white" stroke-width="2" fill="none"></path></svg> Erinnerung hinzufügen';
// Position popup - different logic for week vs month view
const rect = anchorEl.getBoundingClientRect();
const popupHeight = 400; // Estimated max height
const popupWidth = 350;
const padding = 8;
let popupTop, popupLeft;
if (this.viewMode === 'week') {
// For week view, position at the top of the day element
popupTop = Math.max(150, rect.top + 50); // Ensure it's visible, minimum 150px from top
popupLeft = Math.min(rect.left, window.innerWidth - 350);
} else {
// For month view, position below the day element
popupTop = rect.bottom + 8;
popupLeft = Math.min(rect.left, window.innerWidth - 350);
popupTop = rect.bottom + padding;
// Check if popup would go below viewport
if (popupTop + popupHeight > window.innerHeight - padding) {
// Position above the element instead
popupTop = rect.top - popupHeight - padding;
// If still not enough space above, position at bottom of viewport
if (popupTop < padding) {
popupTop = window.innerHeight - popupHeight - padding;
}
}
}
// Horizontal positioning
popupLeft = rect.left;
// Check if popup would go beyond right edge
if (popupLeft + popupWidth > window.innerWidth - padding) {
popupLeft = window.innerWidth - popupWidth - padding;
}
// Check if popup would go beyond left edge
if (popupLeft < padding) {
popupLeft = padding;
}
popup.style.top = `${popupTop}px`;
@ -1087,15 +1126,39 @@ class CalendarViewManager {
// Add reminder button
popup.appendChild(createElement('button', {
className: 'btn btn-secondary btn-block',
className: 'btn btn-reminder-secondary btn-block',
style: { marginTop: 'var(--spacing-md)' },
onclick: () => this.createReminderForDate(dateString)
}, ['+ Weitere Erinnerung']));
}, ['Weitere Erinnerung']));
// Position popup
const rect = anchorEl.getBoundingClientRect();
let popupTop = rect.bottom + 8;
let popupLeft = Math.min(rect.left, window.innerWidth - 350);
const popupHeight = 400; // Estimated max height
const popupWidth = 350;
const padding = 8;
let popupTop = rect.bottom + padding;
let popupLeft = rect.left;
// Check if popup would go below viewport
if (popupTop + popupHeight > window.innerHeight - padding) {
// Position above the element instead
popupTop = rect.top - popupHeight - padding;
// If still not enough space above, position at bottom of viewport
if (popupTop < padding) {
popupTop = window.innerHeight - popupHeight - padding;
}
}
// Horizontal positioning
if (popupLeft + popupWidth > window.innerWidth - padding) {
popupLeft = window.innerWidth - popupWidth - padding;
}
if (popupLeft < padding) {
popupLeft = padding;
}
popup.style.top = `${popupTop}px`;
popup.style.left = `${popupLeft}px`;

Datei anzeigen

@ -1,7 +1,7 @@
/**
* TASKMATE - Contacts Manager
* ===========================
* Kontaktverwaltung mit Kartenansicht
* Kontaktverwaltung mit Tabellenansicht
*/
import api from './api.js';
@ -12,11 +12,14 @@ class ContactsManager {
constructor() {
this.contacts = [];
this.filteredContacts = [];
this.selectedContacts = new Set();
this.allTags = new Set();
this.searchQuery = '';
this.filterTag = '';
this.sortBy = 'created_at';
this.sortOrder = 'desc';
this.currentPage = 1;
this.itemsPerPage = 25;
this.initialized = false;
}
@ -30,16 +33,30 @@ class ContactsManager {
// DOM Elements
this.contactsView = $('#view-contacts');
this.contactsGrid = $('#contacts-grid');
this.contactsTable = $('#contacts-table');
this.contactsTbody = $('#contacts-tbody');
this.contactsEmpty = $('#contacts-empty');
this.tagFilter = $('#contacts-tag-filter');
this.sortSelect = $('#contacts-sort');
this.selectAllCheckbox = $('#select-all-contacts');
this.bulkActions = $('#bulk-actions');
this.selectedCountSpan = $('#selected-count');
this.contactsCountSpan = $('#contacts-count');
this.newContactBtn = $('#btn-new-contact');
this.exportBtn = $('#btn-export-contacts');
this.bulkDeleteBtn = $('#btn-bulk-delete');
this.deselectAllBtn = $('#btn-deselect-all');
// Pagination
this.pagination = $('#contacts-pagination');
this.currentPageSpan = $('#current-page');
this.totalPagesSpan = $('#total-pages');
this.prevPageBtn = $('#btn-prev-page');
this.nextPageBtn = $('#btn-next-page');
console.log('[Contacts] DOM Elements check:');
console.log(' contactsView:', this.contactsView);
console.log(' newContactBtn:', this.newContactBtn);
console.log(' contactsGrid:', this.contactsGrid);
console.log(' contactsTable:', this.contactsTable);
// Modal Elements
this.contactModal = $('#contact-modal');
@ -87,19 +104,46 @@ class ContactsManager {
if (this.tagFilter) {
this.tagFilter.addEventListener('change', (e) => {
this.filterTag = e.target.value;
this.currentPage = 1;
this.filterContacts();
});
}
// Sort
if (this.sortSelect) {
this.sortSelect.addEventListener('change', (e) => {
const [sortBy, sortOrder] = e.target.value.split('-');
this.sortBy = sortBy;
this.sortOrder = sortOrder;
// Table sorting
$$('.sortable').forEach(th => {
th.addEventListener('click', (e) => {
const sortField = th.dataset.sort;
if (this.sortBy === sortField) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortBy = sortField;
this.sortOrder = 'asc';
}
// Update UI
$$('.sortable').forEach(el => el.classList.remove('sort-asc', 'sort-desc'));
th.classList.add(this.sortOrder === 'asc' ? 'sort-asc' : 'sort-desc');
this.sortContacts();
this.renderContacts();
});
});
// Select all checkbox
if (this.selectAllCheckbox) {
this.selectAllCheckbox.addEventListener('change', (e) => {
const checked = e.target.checked;
const visibleContacts = this.getPaginatedContacts();
if (checked) {
visibleContacts.forEach(contact => this.selectedContacts.add(contact.id));
} else {
visibleContacts.forEach(contact => this.selectedContacts.delete(contact.id));
}
this.updateBulkActions();
this.renderContacts();
});
}
// New Contact Button
@ -114,6 +158,46 @@ class ContactsManager {
console.warn('[Contacts] newContactBtn not found!');
}
// Export Button
if (this.exportBtn) {
this.exportBtn.addEventListener('click', () => this.exportContacts());
}
// Bulk Delete Button
if (this.bulkDeleteBtn) {
this.bulkDeleteBtn.addEventListener('click', () => this.bulkDelete());
}
// Deselect All Button
if (this.deselectAllBtn) {
this.deselectAllBtn.addEventListener('click', () => {
this.selectedContacts.clear();
this.selectAllCheckbox.checked = false;
this.updateBulkActions();
this.renderContacts();
});
}
// Pagination buttons
if (this.prevPageBtn) {
this.prevPageBtn.addEventListener('click', () => {
if (this.currentPage > 1) {
this.currentPage--;
this.renderContacts();
}
});
}
if (this.nextPageBtn) {
this.nextPageBtn.addEventListener('click', () => {
const totalPages = Math.ceil(this.filteredContacts.length / this.itemsPerPage);
if (this.currentPage < totalPages) {
this.currentPage++;
this.renderContacts();
}
});
}
// Modal Form
if (this.contactForm) {
this.contactForm.addEventListener('submit', (e) => {
@ -161,6 +245,7 @@ class ContactsManager {
socket.on('contact:deleted', (data) => {
console.log('[Contacts] Socket: contact deleted', data);
this.contacts = this.contacts.filter(c => c.id !== data.contactId);
this.selectedContacts.delete(data.contactId);
this.updateTagsList();
this.filterContacts();
});
@ -216,6 +301,7 @@ class ContactsManager {
});
this.sortContacts();
this.updateContactsCount();
this.renderContacts();
}
@ -243,56 +329,77 @@ class ContactsManager {
});
}
getPaginatedContacts() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return this.filteredContacts.slice(start, end);
}
renderContacts() {
if (!this.contactsGrid) return;
if (!this.contactsTbody) return;
if (this.filteredContacts.length === 0) {
this.contactsGrid.classList.add('hidden');
this.contactsTable.parentElement.classList.add('hidden');
this.contactsEmpty.classList.remove('hidden');
return;
}
this.contactsGrid.classList.remove('hidden');
this.contactsTable.parentElement.classList.remove('hidden');
this.contactsEmpty.classList.add('hidden');
const html = this.filteredContacts.map(contact => this.createContactCard(contact)).join('');
this.contactsGrid.innerHTML = html;
const paginatedContacts = this.getPaginatedContacts();
const html = paginatedContacts.map(contact => this.createContactRow(contact)).join('');
this.contactsTbody.innerHTML = html;
// Bind card events
this.bindCardEvents();
// Update pagination
this.updatePagination();
// Bind row events
this.bindRowEvents();
}
createContactCard(contact) {
createContactRow(contact) {
const displayName = this.getContactDisplayName(contact);
const initials = this.getContactInitials(contact);
const tags = contact.tags || [];
const isSelected = this.selectedContacts.has(contact.id);
return `
<div class="contact-card" data-contact-id="${contact.id}">
<div class="contact-card-header">
<div class="contact-avatar">
${initials}
<tr data-contact-id="${contact.id}" ${isSelected ? 'class="selected"' : ''}>
<td class="checkbox-cell">
<input type="checkbox" class="table-checkbox contact-checkbox" data-contact-id="${contact.id}" ${isSelected ? 'checked' : ''}>
</td>
<td>
<div class="name-cell">
<div class="contact-avatar-small">${initials}</div>
<a href="#" class="contact-name-link" data-contact-id="${contact.id}">${displayName}</a>
</div>
<div class="contact-actions">
<button class="btn-icon btn-edit-contact" title="Bearbeiten">
<i class="fas fa-edit"></i>
</td>
<td>${contact.company || '-'}</td>
<td class="hide-mobile">${contact.position || '-'}</td>
<td>${contact.email ? `<a href="mailto:${contact.email}">${contact.email}</a>` : '-'}</td>
<td class="hide-mobile">${contact.phone || '-'}</td>
<td>
<div class="tags-cell">
${tags.length > 0 ? tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('') : '-'}
</div>
</td>
<td class="actions-cell">
<div class="table-actions">
<button class="btn-table-action btn-edit-contact" data-contact-id="${contact.id}" title="Bearbeiten">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button class="btn-table-action btn-delete-contact-inline" data-contact-id="${contact.id}" title="Löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14M10 11v6M14 11v6"/>
</svg>
</button>
</div>
</div>
<div class="contact-card-body">
<h3 class="contact-name">${displayName}</h3>
${contact.company ? `<div class="contact-company">${contact.company}</div>` : ''}
${contact.position ? `<div class="contact-position">${contact.position}</div>` : ''}
${contact.email ? `<div class="contact-email"><i class="fas fa-envelope"></i> ${contact.email}</div>` : ''}
${contact.phone ? `<div class="contact-phone"><i class="fas fa-phone"></i> ${contact.phone}</div>` : ''}
${contact.mobile ? `<div class="contact-mobile"><i class="fas fa-mobile"></i> ${contact.mobile}</div>` : ''}
</div>
${tags.length > 0 ? `
<div class="contact-tags">
${tags.map(tag => `<span class="contact-tag">${tag}</span>`).join('')}
</div>
` : ''}
</div>
</td>
</tr>
`;
}
@ -325,26 +432,97 @@ class ContactsManager {
return initials || '?';
}
bindCardEvents() {
bindRowEvents() {
// Contact checkboxes
$$('.contact-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const contactId = parseInt(e.target.dataset.contactId);
if (e.target.checked) {
this.selectedContacts.add(contactId);
} else {
this.selectedContacts.delete(contactId);
}
this.updateBulkActions();
this.updateRowSelection(contactId, e.target.checked);
});
});
// Edit buttons
$$('.btn-edit-contact').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const card = btn.closest('.contact-card');
const contactId = parseInt(card.dataset.contactId);
const contactId = parseInt(btn.dataset.contactId);
this.editContact(contactId);
});
});
// Card click
$$('.contact-card').forEach(card => {
card.addEventListener('click', () => {
const contactId = parseInt(card.dataset.contactId);
// Delete inline buttons
$$('.btn-delete-contact-inline').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const contactId = parseInt(btn.dataset.contactId);
this.deleteContactInline(contactId);
});
});
// Name links
$$('.contact-name-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const contactId = parseInt(link.dataset.contactId);
this.editContact(contactId);
});
});
}
updateRowSelection(contactId, selected) {
const row = $(`tr[data-contact-id="${contactId}"]`);
if (row) {
if (selected) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
}
}
updateBulkActions() {
const count = this.selectedContacts.size;
if (count > 0) {
this.bulkActions.classList.remove('hidden');
this.selectedCountSpan.textContent = count;
} else {
this.bulkActions.classList.add('hidden');
}
// Update select all checkbox state
const visibleContacts = this.getPaginatedContacts();
const allSelected = visibleContacts.length > 0 &&
visibleContacts.every(contact => this.selectedContacts.has(contact.id));
this.selectAllCheckbox.checked = allSelected;
}
updateContactsCount() {
const count = this.filteredContacts.length;
this.contactsCountSpan.textContent = `${count} ${count === 1 ? 'Kontakt' : 'Kontakte'}`;
}
updatePagination() {
const totalPages = Math.ceil(this.filteredContacts.length / this.itemsPerPage);
if (totalPages <= 1) {
this.pagination.classList.add('hidden');
return;
}
this.pagination.classList.remove('hidden');
this.currentPageSpan.textContent = this.currentPage;
this.totalPagesSpan.textContent = totalPages;
this.prevPageBtn.disabled = this.currentPage === 1;
this.nextPageBtn.disabled = this.currentPage === totalPages;
}
updateTagsList() {
// Collect all unique tags
this.allTags.clear();
@ -370,6 +548,120 @@ class ContactsManager {
}
}
async bulkDelete() {
const count = this.selectedContacts.size;
if (count === 0) return;
const contactNames = Array.from(this.selectedContacts).map(id => {
const contact = this.contacts.find(c => c.id === id);
return contact ? this.getContactDisplayName(contact) : '';
}).filter(Boolean);
const message = count === 1
? `Möchten Sie den Kontakt "${contactNames[0]}" wirklich löschen?`
: `Möchten Sie ${count} Kontakte wirklich löschen?`;
if (!confirm(message)) return;
try {
// Delete contacts one by one
for (const contactId of this.selectedContacts) {
await api.deleteContact(contactId);
}
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: `${count} ${count === 1 ? 'Kontakt' : 'Kontakte'} gelöscht`, type: 'success' }
}));
this.selectedContacts.clear();
await this.loadContacts();
} catch (error) {
console.error('[Contacts] Error during bulk delete:', error);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Fehler beim Löschen der Kontakte', type: 'error' }
}));
}
}
async deleteContactInline(contactId) {
const contact = this.contacts.find(c => c.id === contactId);
if (!contact) return;
const displayName = this.getContactDisplayName(contact);
if (!confirm(`Möchten Sie den Kontakt "${displayName}" wirklich löschen?`)) {
return;
}
try {
await api.deleteContact(contactId);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Kontakt gelöscht', type: 'success' }
}));
await this.loadContacts();
} catch (error) {
console.error('[Contacts] Error deleting contact:', error);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Fehler beim Löschen', type: 'error' }
}));
}
}
async exportContacts() {
try {
let contactsToExport = this.filteredContacts;
// Create CSV content
const headers = ['Vorname', 'Nachname', 'Firma', 'Position', 'E-Mail', 'Telefon', 'Mobil', 'Adresse', 'PLZ', 'Stadt', 'Land', 'Website', 'Tags', 'Notizen'];
const rows = [headers];
contactsToExport.forEach(contact => {
const row = [
contact.firstName || '',
contact.lastName || '',
contact.company || '',
contact.position || '',
contact.email || '',
contact.phone || '',
contact.mobile || '',
contact.address || '',
contact.postalCode || '',
contact.city || '',
contact.country || '',
contact.website || '',
(contact.tags || []).join(', '),
contact.notes || ''
];
rows.push(row);
});
// Convert to CSV
const csvContent = rows.map(row =>
row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',')
).join('\n');
// Create blob and download
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `kontakte_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: `${contactsToExport.length} Kontakte exportiert`, type: 'success' }
}));
} catch (error) {
console.error('[Contacts] Error exporting contacts:', error);
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message: 'Fehler beim Exportieren', type: 'error' }
}));
}
}
showContactModal(contact = null) {
console.log('[Contacts] showContactModal called with:', contact);
console.log('[Contacts] contactModal element:', this.contactModal);

Datei anzeigen

@ -18,6 +18,7 @@ class KnowledgeManager {
this.expandedEntries = new Set();
this.initialized = false;
this.searchDebounceTimer = null;
this.pendingFiles = null;
// Drag & Drop State
this.draggedCategoryId = null;
@ -140,6 +141,14 @@ class KnowledgeManager {
// File Upload
this.fileInput?.addEventListener('change', (e) => this.handleFileSelect(e));
// Click on file upload area to trigger file input
this.fileUploadArea?.addEventListener('click', (e) => {
// Don't trigger if clicking on the label (it has its own handler)
if (!e.target.closest('.file-input-label')) {
this.fileInput?.click();
}
});
// Drag & Drop for file upload
if (this.fileUploadArea) {
@ -382,8 +391,8 @@ class KnowledgeManager {
<div class="knowledge-attachment-item" data-attachment-id="${att.id}">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" fill="none"/><path d="M14 2v6h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<div class="knowledge-attachment-info">
<span class="knowledge-attachment-name">${this.escapeHtml(att.original_name)}</span>
<span class="knowledge-attachment-size">${this.formatFileSize(att.size_bytes)}</span>
<span class="knowledge-attachment-name">${this.escapeHtml(att.originalName || att.original_name || '')}</span>
<span class="knowledge-attachment-size">${this.formatFileSize(att.sizeBytes || att.size_bytes || 0)}</span>
</div>
<div class="knowledge-attachment-actions">
<a href="${api.getKnowledgeAttachmentDownloadUrl(att.id)}" class="btn-icon" title="Herunterladen" download>
@ -957,7 +966,8 @@ class KnowledgeManager {
this.entryModalTitle.textContent = isEdit ? 'Eintrag bearbeiten' : 'Neuer Eintrag';
this.entryForm?.reset();
this.deleteEntryBtn?.classList.toggle('hidden', !isEdit);
this.attachmentsSection.style.display = isEdit ? 'block' : 'none';
// Show attachments section always, but with a note for new entries
this.attachmentsSection.style.display = 'block';
if (isEdit) {
const entry = await this.loadEntryWithAttachments(entryId);
@ -972,7 +982,7 @@ class KnowledgeManager {
} else {
this.entryIdInput.value = '';
this.entryCategoryIdInput.value = this.selectedCategory?.id || '';
this.attachmentsContainer.innerHTML = '';
this.attachmentsContainer.innerHTML = '<p class="text-muted" style="text-align: center; padding: 20px; color: var(--text-secondary); font-size: var(--text-sm);">Speichern Sie zuerst den Eintrag, um Dateien hochzuladen.</p>';
}
this.openModal(this.entryModal, 'knowledge-entry-modal');
@ -1010,8 +1020,15 @@ class KnowledgeManager {
await api.updateKnowledgeEntry(entryId, data);
this.showToast('Eintrag aktualisiert', 'success');
} else {
await api.createKnowledgeEntry(data);
const newEntry = await api.createKnowledgeEntry(data);
this.showToast('Eintrag erstellt', 'success');
// If there are pending files, upload them now
if (this.pendingFiles && this.pendingFiles.length > 0) {
this.showToast('Lade Dateien hoch...', 'info');
await this.uploadFiles(this.pendingFiles);
this.pendingFiles = null;
}
}
this.closeEntryModal();
@ -1071,7 +1088,10 @@ class KnowledgeManager {
async uploadFiles(files) {
const entryId = parseInt(this.entryIdInput?.value);
if (!entryId) {
this.showToast('Bitte zuerst den Eintrag speichern', 'error');
// For new entries, show a more helpful message
this.showToast('Speichern Sie zuerst den Eintrag, dann können Sie Dateien hochladen', 'info');
// Store files temporarily to upload after save
this.pendingFiles = files;
return;
}

Datei anzeigen

@ -237,9 +237,10 @@ class ReminderManager {
$('#reminder-color').value = '#F59E0B';
// Reset advance days
$$('input[name="advance-days"]').forEach(cb => {
cb.checked = cb.value === '1';
});
const advanceNumberEl = $('#reminder-advance-number');
const advanceUnitEl = $('#reminder-advance-unit');
if (advanceNumberEl) advanceNumberEl.value = '1';
if (advanceUnitEl) advanceUnitEl.value = 'day';
}
}
@ -342,11 +343,20 @@ class ReminderManager {
const freshOptions = $('#reminder-assignee-options');
const freshValueDisplay = freshTrigger.querySelector('.custom-select-value');
// Clear any inline styles from dropdown
freshOptions.style.width = '';
freshOptions.style.left = '';
freshOptions.style.top = '';
freshOptions.style.bottom = '';
// Toggle dropdown
freshTrigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
wrapper.classList.toggle('open');
// Simple toggle - CSS handles positioning
console.log('[Reminder] Dropdown toggled, open:', wrapper.classList.contains('open'));
});
@ -410,9 +420,23 @@ class ReminderManager {
// Set advance days
const advanceDays = reminder.advance_days || ['1'];
$$('input[name="advance-days"]').forEach(cb => {
cb.checked = advanceDays.includes(cb.value);
});
const advanceNumberEl = $('#reminder-advance-number');
const advanceUnitEl = $('#reminder-advance-unit');
// Parse the first advance day value to set number and unit
if (advanceDays.length > 0 && advanceNumberEl && advanceUnitEl) {
const advanceValue = parseInt(advanceDays[0]);
if (advanceValue <= 9) {
advanceNumberEl.value = advanceValue;
advanceUnitEl.value = 'day';
} else if (advanceValue % 7 === 0 && advanceValue <= 63) {
advanceNumberEl.value = advanceValue / 7;
advanceUnitEl.value = 'week';
} else if (advanceValue % 30 === 0 && advanceValue <= 270) {
advanceNumberEl.value = advanceValue / 30;
advanceUnitEl.value = 'month';
}
}
} catch (error) {
console.error('Error loading reminder:', error);
@ -434,16 +458,19 @@ class ReminderManager {
try {
const formData = new FormData(this.form);
// Get advance days
const advanceDays = [];
$$('input[name="advance-days"]:checked').forEach(cb => {
advanceDays.push(cb.value);
});
// Get advance days from new inputs
const advanceNumber = parseInt($('#reminder-advance-number').value) || 1;
const advanceUnit = $('#reminder-advance-unit').value || 'day';
if (advanceDays.length === 0) {
throw new Error('Bitte wählen Sie mindestens eine Erinnerungszeit aus');
let advanceDaysValue = advanceNumber;
if (advanceUnit === 'week') {
advanceDaysValue = advanceNumber * 7;
} else if (advanceUnit === 'month') {
advanceDaysValue = advanceNumber * 30;
}
const advanceDays = [String(advanceDaysValue)];
const data = {
project_id: store.get('currentProjectId'),
title: formData.get('reminder-title') || $('#reminder-title').value,

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '265';
const CACHE_VERSION = '292';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;