feat: Persoenliche Farbe fuer freigegebene Kalender
CalendarShare bekommt color-Spalte. Im Kalender-Menue kann jeder Benutzer eine eigene Anzeigefarbe fuer einen mit ihm geteilten Kalender setzen, ohne dass sich dadurch die Farbe beim Eigentuemer oder anderen Share-Empfaengern aendert. * Owner: Farbe aendert den Kalender direkt (wie bisher). * Share-Empfaenger: Farbe landet in CalendarShare.color und wird nur fuer ihn ausgeliefert (list_calendars injiziert sie in 'color', Owner-Farbe bleibt in 'owner_color' als Referenz). Neuer Endpoint: PUT /calendars/<id>/my-color. UI-Hinweis: "Nur fuer deine Ansicht - <Owner> behaelt seine Farbe". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2170f4a7b1
commit
e85338761d
|
|
@ -100,6 +100,11 @@ def list_calendars():
|
|||
calendar_id=c.id, shared_with_id=user.id
|
||||
).first()
|
||||
d['permission'] = share.permission if share else 'read'
|
||||
# Per-user color override: the owner's color is kept in 'owner_color'
|
||||
# so the UI can show both, and 'color' reflects what this user picked.
|
||||
d['owner_color'] = c.color
|
||||
if share and share.color:
|
||||
d['color'] = share.color
|
||||
d['owner_name'] = c.owner.username
|
||||
result.append(d)
|
||||
|
||||
|
|
@ -146,6 +151,33 @@ def update_calendar(cal_id):
|
|||
return jsonify(cal.to_dict()), 200
|
||||
|
||||
|
||||
@api_bp.route('/calendars/<int:cal_id>/my-color', methods=['PUT'])
|
||||
@token_required
|
||||
def set_my_calendar_color(cal_id):
|
||||
"""Personal display color for a shared calendar. Doesn't affect the
|
||||
owner's calendar color or any other user's view."""
|
||||
user = request.current_user
|
||||
cal = db.session.get(Calendar, cal_id)
|
||||
if not cal:
|
||||
return jsonify({'error': 'Nicht gefunden'}), 404
|
||||
|
||||
color = (request.get_json() or {}).get('color', '').strip()
|
||||
|
||||
if cal.owner_id == user.id:
|
||||
# Owner -> update the calendar itself
|
||||
if color:
|
||||
cal.color = color
|
||||
db.session.commit()
|
||||
return jsonify({'color': cal.color}), 200
|
||||
|
||||
share = CalendarShare.query.filter_by(calendar_id=cal_id, shared_with_id=user.id).first()
|
||||
if not share:
|
||||
return jsonify({'error': 'Kein Zugriff'}), 403
|
||||
share.color = color or None
|
||||
db.session.commit()
|
||||
return jsonify({'color': share.color or cal.color}), 200
|
||||
|
||||
|
||||
@api_bp.route('/calendars/<int:cal_id>', methods=['DELETE'])
|
||||
@token_required
|
||||
def delete_calendar(cal_id):
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class CalendarShare(db.Model):
|
|||
calendar_id = db.Column(db.Integer, db.ForeignKey('calendars.id'), nullable=False, index=True)
|
||||
shared_with_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
|
||||
permission = db.Column(db.String(20), nullable=False, default='read') # 'read' or 'readwrite'
|
||||
color = db.Column(db.String(7), nullable=True) # Persoenliche Anzeige-Farbe
|
||||
|
||||
shared_with = db.relationship('User', backref='shared_calendars')
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,19 @@
|
|||
<div v-if="selectedCal" class="cal-menu-content">
|
||||
<p><strong>{{ selectedCal.name }}</strong></p>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
{{ selectedCal.permission === 'owner' ? 'Farbe' : 'Persoenliche Anzeigefarbe' }}
|
||||
</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<InputText :modelValue="selectedCal.color" @change="onColorChange($event)"
|
||||
type="color" style="width: 60px; height: 36px" />
|
||||
<span v-if="selectedCal.permission !== 'owner'" class="color-hint">
|
||||
Nur fuer deine Ansicht - {{ selectedCal.owner_name }} behaelt seine Farbe
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedCal.permission === 'owner'" class="field">
|
||||
<label>Mit Benutzer teilen</label>
|
||||
<div class="share-row">
|
||||
|
|
@ -821,6 +834,20 @@ function copyText(text) {
|
|||
toast.add({ severity: 'info', summary: 'Kopiert', life: 1500 })
|
||||
}
|
||||
|
||||
async function onColorChange(ev) {
|
||||
if (!selectedCal.value) return
|
||||
const color = ev.target.value
|
||||
try {
|
||||
const res = await apiClient.put(`/calendars/${selectedCal.value.id}/my-color`, { color })
|
||||
selectedCal.value.color = res.data.color
|
||||
await loadCalendars()
|
||||
refreshEvents()
|
||||
toast.add({ severity: 'success', summary: 'Farbe aktualisiert', life: 2000 })
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'Fehler', detail: err.response?.data?.error, life: 4000 })
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCalendar() {
|
||||
if (!selectedCal.value) return
|
||||
await apiClient.delete(`/calendars/${selectedCal.value.id}`)
|
||||
|
|
@ -935,6 +962,7 @@ onUnmounted(() => {
|
|||
font-size: 0.8rem; word-break: break-all; flex: 1; }
|
||||
.caldav-clients { font-size: 0.75rem; color: var(--p-text-muted-color); margin-top: 0.5rem; }
|
||||
.caldav-clients div { margin-bottom: 0.15rem; }
|
||||
.color-hint { font-size: 0.75rem; color: var(--p-text-muted-color); font-style: italic; }
|
||||
.hint-badge { font-size: 0.75rem; color: var(--p-primary-700); display: inline-flex; gap: 0.25rem; align-items: center; }
|
||||
.fc-event-content-inner { display: flex; align-items: center; gap: 0.2rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; padding: 0 2px; }
|
||||
.fc-event-content-inner .fc-icon { flex-shrink: 0; font-size: 0.85em; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue