update FDISK sync
This commit is contained in:
@@ -17,6 +17,16 @@ const ChatMessageView: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [input, setInput] = useState('');
|
||||
const prevActiveRef = useRef(false);
|
||||
|
||||
// Invalidate messages immediately when this room + panel becomes active
|
||||
useEffect(() => {
|
||||
const active = !!selectedRoomToken && chatPanelOpen;
|
||||
if (active && !prevActiveRef.current) {
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'messages', selectedRoomToken] });
|
||||
}
|
||||
prevActiveRef.current = active;
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient]);
|
||||
|
||||
const room = rooms.find((r) => r.token === selectedRoomToken);
|
||||
|
||||
|
||||
@@ -26,6 +26,27 @@ import type { Notification, NotificationSchwere } from '../../types/notification
|
||||
|
||||
const POLL_INTERVAL_MS = 15_000; // 15 seconds
|
||||
|
||||
function playNotificationSound() {
|
||||
try {
|
||||
const ctx = new AudioContext();
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.value = 600;
|
||||
const now = ctx.currentTime;
|
||||
gain.gain.setValueAtTime(0, now);
|
||||
gain.gain.linearRampToValueAtTime(0.3, now + 0.02);
|
||||
gain.gain.linearRampToValueAtTime(0, now + 0.15);
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + 0.15);
|
||||
oscillator.onended = () => ctx.close();
|
||||
} catch {
|
||||
// Audio blocked before first user interaction — fail silently
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only allow window.open for URLs whose origin matches the current app origin.
|
||||
* External-looking URLs (different host or protocol-relative) are rejected to
|
||||
@@ -83,6 +104,9 @@ const NotificationBell: React.FC = () => {
|
||||
|
||||
// Find notifications we haven't seen before
|
||||
const newOnes = unread.filter((n) => !knownIdsRef.current!.has(n.id));
|
||||
if (newOnes.length > 0) {
|
||||
playNotificationSound();
|
||||
}
|
||||
newOnes.forEach((n) => {
|
||||
knownIdsRef.current!.add(n.id);
|
||||
const severity = n.schwere === 'fehler' ? 'error' : n.schwere === 'warnung' ? 'warning' : 'info';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { nextcloudApi } from '../services/nextcloud';
|
||||
import { useLayout } from './LayoutContext';
|
||||
import type { NextcloudConversation } from '../types/nextcloud.types';
|
||||
@@ -21,6 +21,17 @@ interface ChatProviderProps {
|
||||
export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
|
||||
const [selectedRoomToken, setSelectedRoomToken] = useState<string | null>(null);
|
||||
const { chatPanelOpen } = useLayout();
|
||||
const queryClient = useQueryClient();
|
||||
const prevPanelOpenRef = useRef(chatPanelOpen);
|
||||
|
||||
// Invalidate rooms/connection when panel opens so data is fresh immediately
|
||||
useEffect(() => {
|
||||
if (chatPanelOpen && !prevPanelOpenRef.current) {
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'connection'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||
}
|
||||
prevPanelOpenRef.current = chatPanelOpen;
|
||||
}, [chatPanelOpen, queryClient]);
|
||||
|
||||
const { data: connData } = useQuery({
|
||||
queryKey: ['nextcloud', 'connection'],
|
||||
|
||||
@@ -69,7 +69,9 @@ export async function syncToDatabase(
|
||||
const profileResult = await client.query<{ user_id: string }>(
|
||||
`SELECT mp.user_id
|
||||
FROM mitglieder_profile mp
|
||||
WHERE mp.fdisk_standesbuch_nr = $1`,
|
||||
JOIN users u ON u.id = mp.user_id
|
||||
WHERE mp.fdisk_standesbuch_nr = $1
|
||||
AND u.last_login_at IS NOT NULL`,
|
||||
[member.standesbuchNr]
|
||||
);
|
||||
|
||||
@@ -78,18 +80,20 @@ export async function syncToDatabase(
|
||||
if (profileResult.rows.length > 0) {
|
||||
userId = profileResult.rows[0].user_id;
|
||||
} else {
|
||||
// Fallback: match by name (case-insensitive)
|
||||
// Fallback: match by name (case-insensitive), only logged-in users
|
||||
const nameResult = await client.query<{ id: string }>(
|
||||
`SELECT u.id
|
||||
FROM users u
|
||||
JOIN mitglieder_profile mp ON mp.user_id = u.id
|
||||
WHERE LOWER(u.given_name) = LOWER($1)
|
||||
AND LOWER(u.family_name) = LOWER($2)
|
||||
LIMIT 1`,
|
||||
AND u.last_login_at IS NOT NULL`,
|
||||
[member.vorname, member.zuname]
|
||||
);
|
||||
|
||||
if (nameResult.rows.length > 0) {
|
||||
if (nameResult.rows.length > 1) {
|
||||
log(`WARN: skipping ${member.vorname} ${member.zuname} (Standesbuch-Nr ${member.standesbuchNr}) — duplicate name match (${nameResult.rows.length} users)`);
|
||||
} else if (nameResult.rows.length === 1) {
|
||||
userId = nameResult.rows[0].id;
|
||||
// Store the Standesbuch-Nr now that we found a match
|
||||
await client.query(
|
||||
|
||||
Reference in New Issue
Block a user