update
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS fdisk_standesbuch_nr VARCHAR(32);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mitglieder_profile_fdisk_standesbuch_nr ON mitglieder_profile(fdisk_standesbuch_nr);
|
||||||
@@ -64,6 +64,7 @@ export interface MitgliederProfile {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
|
|
||||||
mitglieds_nr: string | null;
|
mitglieds_nr: string | null;
|
||||||
|
fdisk_standesbuch_nr: string | null;
|
||||||
dienstgrad: DienstgradEnum | null;
|
dienstgrad: DienstgradEnum | null;
|
||||||
dienstgrad_seit: Date | null;
|
dienstgrad_seit: Date | null;
|
||||||
funktion: FunktionEnum[];
|
funktion: FunktionEnum[];
|
||||||
@@ -172,6 +173,7 @@ export interface MemberFilters {
|
|||||||
*/
|
*/
|
||||||
export const CreateMemberProfileSchema = z.object({
|
export const CreateMemberProfileSchema = z.object({
|
||||||
mitglieds_nr: z.string().max(32).optional(),
|
mitglieds_nr: z.string().max(32).optional(),
|
||||||
|
fdisk_standesbuch_nr: z.string().max(32).optional(),
|
||||||
dienstgrad: z.enum(DIENSTGRAD_VALUES).optional(),
|
dienstgrad: z.enum(DIENSTGRAD_VALUES).optional(),
|
||||||
dienstgrad_seit: z.coerce.date().optional(),
|
dienstgrad_seit: z.coerce.date().optional(),
|
||||||
funktion: z.array(z.enum(FUNKTION_VALUES)).default([]),
|
funktion: z.array(z.enum(FUNKTION_VALUES)).default([]),
|
||||||
|
|||||||
@@ -197,13 +197,13 @@ router.post(
|
|||||||
'/fdisk-sync/trigger',
|
'/fdisk-sync/trigger',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('admin:access'),
|
requirePermission('admin:access'),
|
||||||
async (_req: Request, res: Response): Promise<void> => {
|
async (req: Request, res: Response): Promise<void> => {
|
||||||
if (!FDISK_SYNC_URL) {
|
if (!FDISK_SYNC_URL) {
|
||||||
res.status(503).json({ success: false, message: 'FDISK sync service not configured' });
|
res.status(503).json({ success: false, message: 'FDISK sync service not configured' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${FDISK_SYNC_URL}/trigger`, {}, { timeout: 5000 });
|
const response = await axios.post(`${FDISK_SYNC_URL}/trigger`, req.body, { timeout: 5000 });
|
||||||
res.json({ success: true, data: response.data });
|
res.json({ success: true, data: response.data });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (axios.isAxiosError(err) && err.response?.status === 409) {
|
if (axios.isAxiosError(err) && err.response?.status === 409) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class MemberService {
|
|||||||
mp.id AS mp_id,
|
mp.id AS mp_id,
|
||||||
mp.user_id AS mp_user_id,
|
mp.user_id AS mp_user_id,
|
||||||
mp.mitglieds_nr AS mp_mitglieds_nr,
|
mp.mitglieds_nr AS mp_mitglieds_nr,
|
||||||
|
mp.fdisk_standesbuch_nr AS mp_fdisk_standesbuch_nr,
|
||||||
mp.dienstgrad AS mp_dienstgrad,
|
mp.dienstgrad AS mp_dienstgrad,
|
||||||
mp.dienstgrad_seit AS mp_dienstgrad_seit,
|
mp.dienstgrad_seit AS mp_dienstgrad_seit,
|
||||||
mp.funktion AS mp_funktion,
|
mp.funktion AS mp_funktion,
|
||||||
@@ -83,6 +84,7 @@ class MemberService {
|
|||||||
id: row.mp_id,
|
id: row.mp_id,
|
||||||
user_id: row.mp_user_id,
|
user_id: row.mp_user_id,
|
||||||
mitglieds_nr: row.mp_mitglieds_nr,
|
mitglieds_nr: row.mp_mitglieds_nr,
|
||||||
|
fdisk_standesbuch_nr: row.mp_fdisk_standesbuch_nr ?? null,
|
||||||
dienstgrad: row.mp_dienstgrad,
|
dienstgrad: row.mp_dienstgrad,
|
||||||
dienstgrad_seit: row.mp_dienstgrad_seit,
|
dienstgrad_seit: row.mp_dienstgrad_seit,
|
||||||
funktion: row.mp_funktion ?? [],
|
funktion: row.mp_funktion ?? [],
|
||||||
@@ -283,6 +285,7 @@ class MemberService {
|
|||||||
INSERT INTO mitglieder_profile (
|
INSERT INTO mitglieder_profile (
|
||||||
user_id,
|
user_id,
|
||||||
mitglieds_nr,
|
mitglieds_nr,
|
||||||
|
fdisk_standesbuch_nr,
|
||||||
dienstgrad,
|
dienstgrad,
|
||||||
dienstgrad_seit,
|
dienstgrad_seit,
|
||||||
funktion,
|
funktion,
|
||||||
@@ -300,8 +303,8 @@ class MemberService {
|
|||||||
bemerkungen,
|
bemerkungen,
|
||||||
bild_url
|
bild_url
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||||
$10, $11, $12, $13, $14, $15, $16, $17, $18
|
$11, $12, $13, $14, $15, $16, $17, $18, $19
|
||||||
)
|
)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
@@ -309,6 +312,7 @@ class MemberService {
|
|||||||
const values = [
|
const values = [
|
||||||
userId,
|
userId,
|
||||||
data.mitglieds_nr ?? null,
|
data.mitglieds_nr ?? null,
|
||||||
|
data.fdisk_standesbuch_nr ?? null,
|
||||||
data.dienstgrad ?? null,
|
data.dienstgrad ?? null,
|
||||||
data.dienstgrad_seit ?? null,
|
data.dienstgrad_seit ?? null,
|
||||||
data.funktion ?? [],
|
data.funktion ?? [],
|
||||||
@@ -387,6 +391,7 @@ class MemberService {
|
|||||||
|
|
||||||
const fieldMap: Record<string, any> = {
|
const fieldMap: Record<string, any> = {
|
||||||
mitglieds_nr: rest.mitglieds_nr,
|
mitglieds_nr: rest.mitglieds_nr,
|
||||||
|
fdisk_standesbuch_nr: rest.fdisk_standesbuch_nr,
|
||||||
funktion: rest.funktion,
|
funktion: rest.funktion,
|
||||||
status: rest.status,
|
status: rest.status,
|
||||||
eintrittsdatum: rest.eintrittsdatum,
|
eintrittsdatum: rest.eintrittsdatum,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box, Button, Card, CardContent, Chip, CircularProgress, IconButton, Tooltip, Typography,
|
Box, Button, Card, CardContent, Checkbox, Chip, CircularProgress, FormControlLabel, IconButton, Tooltip, Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import SyncIcon from '@mui/icons-material/Sync';
|
import SyncIcon from '@mui/icons-material/Sync';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
@@ -12,6 +12,7 @@ function FdiskSyncTab() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const logBoxRef = useRef<HTMLDivElement>(null);
|
const logBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [force, setForce] = useState(false);
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ['admin', 'fdisk-sync', 'logs'],
|
queryKey: ['admin', 'fdisk-sync', 'logs'],
|
||||||
@@ -27,7 +28,7 @@ function FdiskSyncTab() {
|
|||||||
}, [data?.logs.length]);
|
}, [data?.logs.length]);
|
||||||
|
|
||||||
const triggerMutation = useMutation({
|
const triggerMutation = useMutation({
|
||||||
mutationFn: adminApi.fdiskSyncTrigger,
|
mutationFn: (forceSync: boolean) => adminApi.fdiskSyncTrigger(forceSync),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showSuccess('Sync gestartet');
|
showSuccess('Sync gestartet');
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'fdisk-sync', 'logs'] });
|
queryClient.invalidateQueries({ queryKey: ['admin', 'fdisk-sync', 'logs'] });
|
||||||
@@ -71,12 +72,16 @@ function FdiskSyncTab() {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<SyncIcon />}
|
startIcon={<SyncIcon />}
|
||||||
onClick={() => triggerMutation.mutate()}
|
onClick={() => triggerMutation.mutate(force)}
|
||||||
disabled={running || triggerMutation.isPending}
|
disabled={running || triggerMutation.isPending}
|
||||||
>
|
>
|
||||||
Jetzt synchronisieren
|
Jetzt synchronisieren
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Checkbox checked={force} onChange={(e) => setForce(e.target.checked)} />}
|
||||||
|
label="Alle Mitglieder erzwungen synchronisieren"
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Autocomplete,
|
||||||
Button,
|
Button,
|
||||||
Chip,
|
Chip,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -41,9 +42,11 @@ import {
|
|||||||
MemberWithProfile,
|
MemberWithProfile,
|
||||||
StatusEnum,
|
StatusEnum,
|
||||||
DienstgradEnum,
|
DienstgradEnum,
|
||||||
|
FunktionEnum,
|
||||||
TshirtGroesseEnum,
|
TshirtGroesseEnum,
|
||||||
DIENSTGRAD_VALUES,
|
DIENSTGRAD_VALUES,
|
||||||
STATUS_VALUES,
|
STATUS_VALUES,
|
||||||
|
FUNKTION_VALUES,
|
||||||
TSHIRT_GROESSE_VALUES,
|
TSHIRT_GROESSE_VALUES,
|
||||||
STATUS_LABELS,
|
STATUS_LABELS,
|
||||||
STATUS_COLORS,
|
STATUS_COLORS,
|
||||||
@@ -239,6 +242,7 @@ function MitgliedDetail() {
|
|||||||
funktion: member.profile.funktion,
|
funktion: member.profile.funktion,
|
||||||
status: member.profile.status,
|
status: member.profile.status,
|
||||||
eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined,
|
eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined,
|
||||||
|
austrittsdatum: toGermanDate(member.profile.austrittsdatum) || undefined,
|
||||||
geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined,
|
geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined,
|
||||||
telefon_mobil: member.profile.telefon_mobil ?? undefined,
|
telefon_mobil: member.profile.telefon_mobil ?? undefined,
|
||||||
telefon_privat: member.profile.telefon_privat ?? undefined,
|
telefon_privat: member.profile.telefon_privat ?? undefined,
|
||||||
@@ -248,6 +252,7 @@ function MitgliedDetail() {
|
|||||||
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
|
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
|
||||||
schuhgroesse: member.profile.schuhgroesse ?? undefined,
|
schuhgroesse: member.profile.schuhgroesse ?? undefined,
|
||||||
bemerkungen: member.profile.bemerkungen ?? undefined,
|
bemerkungen: member.profile.bemerkungen ?? undefined,
|
||||||
|
fdisk_standesbuch_nr: member.profile.fdisk_standesbuch_nr ?? undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [member]);
|
}, [member]);
|
||||||
@@ -263,6 +268,7 @@ function MitgliedDetail() {
|
|||||||
const payload: UpdateMemberProfileData = {
|
const payload: UpdateMemberProfileData = {
|
||||||
...formData,
|
...formData,
|
||||||
eintrittsdatum: formData.eintrittsdatum ? fromGermanDate(formData.eintrittsdatum) || undefined : undefined,
|
eintrittsdatum: formData.eintrittsdatum ? fromGermanDate(formData.eintrittsdatum) || undefined : undefined,
|
||||||
|
austrittsdatum: formData.austrittsdatum ? fromGermanDate(formData.austrittsdatum) || undefined : undefined,
|
||||||
geburtsdatum: formData.geburtsdatum ? fromGermanDate(formData.geburtsdatum) || undefined : undefined,
|
geburtsdatum: formData.geburtsdatum ? fromGermanDate(formData.geburtsdatum) || undefined : undefined,
|
||||||
dienstgrad_seit: formData.dienstgrad_seit ? fromGermanDate(formData.dienstgrad_seit) || undefined : undefined,
|
dienstgrad_seit: formData.dienstgrad_seit ? fromGermanDate(formData.dienstgrad_seit) || undefined : undefined,
|
||||||
};
|
};
|
||||||
@@ -282,12 +288,23 @@ function MitgliedDetail() {
|
|||||||
// Reset form to current profile values
|
// Reset form to current profile values
|
||||||
if (member?.profile) {
|
if (member?.profile) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
mitglieds_nr: member.profile.mitglieds_nr ?? undefined,
|
||||||
|
dienstgrad: member.profile.dienstgrad ?? undefined,
|
||||||
|
dienstgrad_seit: toGermanDate(member.profile.dienstgrad_seit) || undefined,
|
||||||
|
funktion: member.profile.funktion,
|
||||||
|
status: member.profile.status,
|
||||||
|
eintrittsdatum: toGermanDate(member.profile.eintrittsdatum) || undefined,
|
||||||
|
austrittsdatum: toGermanDate(member.profile.austrittsdatum) || undefined,
|
||||||
|
geburtsdatum: toGermanDate(member.profile.geburtsdatum) || undefined,
|
||||||
telefon_mobil: member.profile.telefon_mobil ?? undefined,
|
telefon_mobil: member.profile.telefon_mobil ?? undefined,
|
||||||
telefon_privat: member.profile.telefon_privat ?? undefined,
|
telefon_privat: member.profile.telefon_privat ?? undefined,
|
||||||
notfallkontakt_name: member.profile.notfallkontakt_name ?? undefined,
|
notfallkontakt_name: member.profile.notfallkontakt_name ?? undefined,
|
||||||
notfallkontakt_telefon: member.profile.notfallkontakt_telefon ?? undefined,
|
notfallkontakt_telefon: member.profile.notfallkontakt_telefon ?? undefined,
|
||||||
|
fuehrerscheinklassen: member.profile.fuehrerscheinklassen,
|
||||||
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
|
tshirt_groesse: member.profile.tshirt_groesse ?? undefined,
|
||||||
schuhgroesse: member.profile.schuhgroesse ?? undefined,
|
schuhgroesse: member.profile.schuhgroesse ?? undefined,
|
||||||
|
bemerkungen: member.profile.bemerkungen ?? undefined,
|
||||||
|
fdisk_standesbuch_nr: member.profile.fdisk_standesbuch_nr ?? undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -542,6 +559,35 @@ function MitgliedDetail() {
|
|||||||
onChange={(e) => handleFieldChange('geburtsdatum', e.target.value || undefined)}
|
onChange={(e) => handleFieldChange('geburtsdatum', e.target.value || undefined)}
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Austrittsdatum"
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
placeholder="TT.MM.JJJJ"
|
||||||
|
value={formData.austrittsdatum ?? ''}
|
||||||
|
onChange={(e) => handleFieldChange('austrittsdatum', e.target.value || undefined)}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Standesbuchnummer"
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
value={formData.fdisk_standesbuch_nr ?? ''}
|
||||||
|
onChange={(e) => handleFieldChange('fdisk_standesbuch_nr', e.target.value || undefined)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={[...FUNKTION_VALUES]}
|
||||||
|
value={formData.funktion ?? []}
|
||||||
|
onChange={(_e, newValue) => handleFieldChange('funktion', newValue as FunktionEnum[])}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Funktion" size="small" />
|
||||||
|
)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -574,6 +620,13 @@ function MitgliedDetail() {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<FieldRow
|
||||||
|
label="Austrittsdatum"
|
||||||
|
value={profile?.austrittsdatum
|
||||||
|
? new Date(profile.austrittsdatum).toLocaleDateString('de-AT')
|
||||||
|
: null}
|
||||||
|
/>
|
||||||
|
<FieldRow label="Standesbuchnummer" value={profile?.fdisk_standesbuch_nr ?? null} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -700,7 +753,19 @@ function MitgliedDetail() {
|
|||||||
title="Führerscheinklassen"
|
title="Führerscheinklassen"
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{profile?.fuehrerscheinklassen && profile.fuehrerscheinklassen.length > 0 ? (
|
{editMode && canWrite ? (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
freeSolo
|
||||||
|
options={['AM', 'A', 'B', 'BE', 'C', 'CE', 'C1', 'C1E', 'D', 'DE', 'D1', 'D1E', 'F']}
|
||||||
|
value={formData.fuehrerscheinklassen ?? []}
|
||||||
|
onChange={(_e, newValue) => handleFieldChange('fuehrerscheinklassen', newValue)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Führerscheinklassen" size="small" placeholder="Klasse hinzufügen…" />
|
||||||
|
)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
) : profile?.fuehrerscheinklassen && profile.fuehrerscheinklassen.length > 0 ? (
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||||
{profile.fuehrerscheinklassen.map((k) => (
|
{profile.fuehrerscheinklassen.map((k) => (
|
||||||
<Chip key={k} label={k} size="small" variant="outlined" />
|
<Chip key={k} label={k} size="small" variant="outlined" />
|
||||||
|
|||||||
@@ -28,5 +28,5 @@ export const adminApi = {
|
|||||||
broadcast: (data: BroadcastPayload) => api.post<ApiResponse<{ sent: number }>>('/api/admin/notifications/broadcast', data).then(r => r.data.data),
|
broadcast: (data: BroadcastPayload) => api.post<ApiResponse<{ sent: number }>>('/api/admin/notifications/broadcast', data).then(r => r.data.data),
|
||||||
getPingHistory: (serviceId: string) => api.get<ApiResponse<PingHistoryEntry[]>>(`/api/admin/services/${serviceId}/ping-history`).then(r => r.data.data),
|
getPingHistory: (serviceId: string) => api.get<ApiResponse<PingHistoryEntry[]>>(`/api/admin/services/${serviceId}/ping-history`).then(r => r.data.data),
|
||||||
fdiskSyncLogs: () => api.get<ApiResponse<FdiskSyncLogsResponse>>('/api/admin/fdisk-sync/logs').then(r => r.data.data),
|
fdiskSyncLogs: () => api.get<ApiResponse<FdiskSyncLogsResponse>>('/api/admin/fdisk-sync/logs').then(r => r.data.data),
|
||||||
fdiskSyncTrigger: () => api.post<ApiResponse<{ started: boolean }>>('/api/admin/fdisk-sync/trigger').then(r => r.data.data),
|
fdiskSyncTrigger: (force = false) => api.post<ApiResponse<{ started: boolean }>>('/api/admin/fdisk-sync/trigger', { force }).then(r => r.data.data),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export interface MitgliederProfile {
|
|||||||
tshirt_groesse: TshirtGroesseEnum | null;
|
tshirt_groesse: TshirtGroesseEnum | null;
|
||||||
schuhgroesse: string | null;
|
schuhgroesse: string | null;
|
||||||
bemerkungen: string | null;
|
bemerkungen: string | null;
|
||||||
|
fdisk_standesbuch_nr: string | null;
|
||||||
bild_url: string | null;
|
bild_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ function mapDienstgrad(raw: string): string | null {
|
|||||||
export async function syncToDatabase(
|
export async function syncToDatabase(
|
||||||
pool: Pool,
|
pool: Pool,
|
||||||
members: FdiskMember[],
|
members: FdiskMember[],
|
||||||
ausbildungen: FdiskAusbildung[]
|
ausbildungen: FdiskAusbildung[],
|
||||||
|
force = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
@@ -62,6 +63,7 @@ export async function syncToDatabase(
|
|||||||
|
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
let unchanged = 0;
|
let unchanged = 0;
|
||||||
|
let forced = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
@@ -161,12 +163,20 @@ export async function syncToDatabase(
|
|||||||
if (changes.length > 0) {
|
if (changes.length > 0) {
|
||||||
log(`Updated ${member.vorname} ${member.zuname} (${member.standesbuchNr}): ${changes.join(', ')}`);
|
log(`Updated ${member.vorname} ${member.zuname} (${member.standesbuchNr}): ${changes.join(', ')}`);
|
||||||
updated++;
|
updated++;
|
||||||
|
} else if (force) {
|
||||||
|
// Force mode: explicitly update timestamp and log even unchanged rows
|
||||||
|
await client.query(
|
||||||
|
`UPDATE mitglieder_profile SET updated_at = NOW() WHERE user_id = $1`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
log(`Forced update ${member.vorname} ${member.zuname} (${member.standesbuchNr}): no changes, timestamp refreshed`);
|
||||||
|
forced++;
|
||||||
} else {
|
} else {
|
||||||
unchanged++;
|
unchanged++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`Members: ${updated} changed, ${unchanged} unchanged, ${skipped} skipped (no dashboard account)`);
|
log(`Members: ${updated} changed, ${unchanged} unchanged, ${forced} forced, ${skipped} skipped (no dashboard account)`);
|
||||||
|
|
||||||
// Upsert Ausbildungen
|
// Upsert Ausbildungen
|
||||||
let ausbildungNew = 0;
|
let ausbildungNew = 0;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function msUntilMidnight(): number {
|
|||||||
return midnight.getTime() - now.getTime();
|
return midnight.getTime() - now.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSync(): Promise<void> {
|
async function runSync(force = false): Promise<void> {
|
||||||
if (syncRunning) {
|
if (syncRunning) {
|
||||||
log('Sync already in progress, skipping');
|
log('Sync already in progress, skipping');
|
||||||
return;
|
return;
|
||||||
@@ -64,9 +64,10 @@ async function runSync(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (force) log('Force mode: ON');
|
||||||
log('Starting FDISK sync');
|
log('Starting FDISK sync');
|
||||||
const { members, ausbildungen } = await scrapeAll(username, password);
|
const { members, ausbildungen } = await scrapeAll(username, password);
|
||||||
await syncToDatabase(pool, members, ausbildungen);
|
await syncToDatabase(pool, members, ausbildungen, force);
|
||||||
log(`Sync complete — ${members.length} members, ${ausbildungen.length} Ausbildungen`);
|
log(`Sync complete — ${members.length} members, ${ausbildungen.length} Ausbildungen`);
|
||||||
} finally {
|
} finally {
|
||||||
syncRunning = false;
|
syncRunning = false;
|
||||||
@@ -87,9 +88,20 @@ function startHttpServer(port: number) {
|
|||||||
res.end(JSON.stringify({ running: true, message: 'Sync already in progress' }));
|
res.end(JSON.stringify({ running: true, message: 'Sync already in progress' }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.writeHead(200);
|
let body = '';
|
||||||
res.end(JSON.stringify({ started: true }));
|
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||||
runSync().catch(err => log(`ERROR during manual sync: ${err.message}`));
|
req.on('end', () => {
|
||||||
|
let force = false;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
force = parsed?.force === true;
|
||||||
|
} catch {
|
||||||
|
// no body or invalid JSON — force stays false
|
||||||
|
}
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ started: true, force }));
|
||||||
|
runSync(force).catch(err => log(`ERROR during manual sync: ${err.message}`));
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end(JSON.stringify({ message: 'Not found' }));
|
res.end(JSON.stringify({ message: 'Not found' }));
|
||||||
|
|||||||
Reference in New Issue
Block a user