Quellcode des ICON CRM (5)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { formatStamp, type Lead } from '../../types';
|
||||
import { leadsApi } from '../../lib/api';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import { PrimaryButton, SecondaryButton } from '../ui/form';
|
||||
import { useNavigation } from '../../lib/navigation';
|
||||
import { useToast } from '../Toast';
|
||||
|
||||
/*
|
||||
* Erinnerungen an Leads.
|
||||
*
|
||||
* Zwei Faelle, beide als Pop-up:
|
||||
*
|
||||
* faellig eine gesetzte Erinnerung ist erreicht ("in 2 Tagen anrufen")
|
||||
* alt ein Lead wurde einen Monat lang nicht angefasst; dann fragt das
|
||||
* CRM, ob er archiviert werden soll. Wird er das nicht, laeuft die
|
||||
* Frist von vorn - das erledigt der Server, indem das Quittieren
|
||||
* `last_contact_at` auf jetzt setzt.
|
||||
*
|
||||
* Abgefragt wird beim Anmelden und danach stuendlich. Oefter waere sinnlos:
|
||||
* die Fristen sind Tage, nicht Minuten.
|
||||
*/
|
||||
|
||||
const POLL_MS = 60 * 60 * 1000;
|
||||
|
||||
export const ReminderWatcher: React.FC<{ onChanged: () => void }> = ({ onChanged }) => {
|
||||
const { toast } = useToast();
|
||||
const { open } = useNavigation();
|
||||
const [queue, setQueue] = useState<Array<{ lead: Lead; kind: 'due' | 'stale' }>>([]);
|
||||
const [staleDays, setStaleDays] = useState(30);
|
||||
|
||||
const check = useCallback(async () => {
|
||||
try {
|
||||
const result = await leadsApi.reminders();
|
||||
setStaleDays(result.stale_days);
|
||||
setQueue([
|
||||
...result.due.map((lead) => ({ lead, kind: 'due' as const })),
|
||||
...result.stale.map((lead) => ({ lead, kind: 'stale' as const })),
|
||||
]);
|
||||
} catch {
|
||||
/* Kein Netz oder abgelaufene Sitzung - beim naechsten Durchlauf erneut. */
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void check();
|
||||
const timer = setInterval(() => void check(), POLL_MS);
|
||||
return () => clearInterval(timer);
|
||||
}, [check]);
|
||||
|
||||
const current = queue[0];
|
||||
if (!current) return null;
|
||||
|
||||
const { lead, kind } = current;
|
||||
const next = () => setQueue((list) => list.slice(1));
|
||||
|
||||
const acknowledge = async (snoozeDays?: number) => {
|
||||
try {
|
||||
await leadsApi.acknowledge(lead.id, snoozeDays);
|
||||
if (snoozeDays) toast(`Erinnerung um ${snoozeDays} Tag(e) verschoben.`, 'success');
|
||||
} catch (err) {
|
||||
toast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error');
|
||||
} finally {
|
||||
next();
|
||||
onChanged();
|
||||
}
|
||||
};
|
||||
|
||||
const archive = async () => {
|
||||
try {
|
||||
await leadsApi.update(lead.id, { archived: true });
|
||||
toast(`"${lead.title}" archiviert.`, 'success');
|
||||
} catch (err) {
|
||||
toast(err instanceof Error ? err.message : 'Archivieren fehlgeschlagen.', 'error');
|
||||
} finally {
|
||||
next();
|
||||
onChanged();
|
||||
}
|
||||
};
|
||||
|
||||
// Vorschlag fuer das Verschieben: das am Lead gepflegte Intervall, sonst zwei Tage.
|
||||
const snooze = lead.reminder_days && lead.reminder_days > 0 ? lead.reminder_days : 2;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
size="sm"
|
||||
onClose={() => void acknowledge()}
|
||||
title={kind === 'due' ? 'Erinnerung' : 'Lead liegt lange still'}
|
||||
subtitle={queue.length > 1 ? `${queue.length} offene Erinnerungen` : undefined}
|
||||
footer={
|
||||
kind === 'due' ? (
|
||||
<>
|
||||
<SecondaryButton type="button" onClick={() => void acknowledge(snooze)}>
|
||||
In {snooze} Tag(en) erneut
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="button" onClick={() => void acknowledge()}>
|
||||
Erledigt
|
||||
</PrimaryButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SecondaryButton type="button" onClick={() => void acknowledge()}>
|
||||
Nicht archivieren
|
||||
</SecondaryButton>
|
||||
<PrimaryButton type="button" onClick={archive}>
|
||||
Archivieren
|
||||
</PrimaryButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-ink-800">
|
||||
{kind === 'due' ? (
|
||||
<>
|
||||
<strong>{lead.title}</strong>
|
||||
{lead.company_name ? ` – ${lead.company_name}` : ''}
|
||||
{lead.contact_name ? ` · ${lead.contact_name} anrufen` : ''}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Achtung: <strong>{lead.title}</strong> wurde seit {staleDays} Tagen nicht kontaktiert. Soll
|
||||
der Lead archiviert werden?
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-2 rounded-lg border border-ink-200 p-3">
|
||||
{lead.company_name && (
|
||||
<span className="flex items-center gap-2 text-sm text-ink-700">
|
||||
<Avatar name={lead.company_name} src={lead.company_logo} size={24} square />
|
||||
{lead.company_name}
|
||||
</span>
|
||||
)}
|
||||
{lead.contact_name && (
|
||||
<span className="flex items-center gap-2 text-sm text-ink-700">
|
||||
<Avatar name={lead.contact_name} src={lead.contact_avatar} size={24} />
|
||||
{lead.contact_name}
|
||||
{lead.contact_phone ? ` · ${lead.contact_phone}` : ''}
|
||||
</span>
|
||||
)}
|
||||
<p className="text-xs text-ink-400">
|
||||
Zuletzt angefasst: {formatStamp(lead.last_contact_at ?? lead.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
next();
|
||||
open('lead', lead.id);
|
||||
}}
|
||||
className="mt-3 text-sm font-medium text-brand-700 hover:underline"
|
||||
>
|
||||
Lead öffnen
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user