// Nachrichten — kontextstart, vorlagen mit auto-fill, geteilter posteingang, lesebestätigungen
// ───── Eigentümer (Team) ─────
const OWNERS = [
{ id: "o1", name: "Marie Tessmann", initials: "MT", role: "Eigentümer", color: "#2d4a3e" },
{ id: "o2", name: "Stefan Tessmann", initials: "ST", role: "Mit-Eigentümer", color: "#7a5a2e" },
{ id: "o3", name: "Lisa Voigt", initials: "LV", role: "Verwalterin", color: "#5a4a7a" },
];
const ME = "o1"; // current user
// ───── Vorlagen ─────
// Each template: id, name, category, channel, subject, build({tenant, unit, property, ctx}) -> body
const TEMPLATES = [
{
id: "rent_increase",
name: "Mieterhöhung (Indexmiete)",
category: "Vertrag",
icon: "↗",
channel: "email",
subject: "Mietanpassung gemäß Indexmiete",
fields: [
{ id: "newRent", label: "Neue Kaltmiete", type: "currency", default: (ctx) => ctx.unit ? Math.round(ctx.unit.rent * 1.038) : 0 },
{ id: "fromDate", label: "Gültig ab", type: "date", default: () => "2026-09-01" },
{ id: "indexPct", label: "Indexsteigerung", type: "percent", default: () => "3,8" },
],
build: ({ tenant, unit, property, fields }) => {
const oldRent = unit?.rent || 0;
const newRent = fields.newRent || 0;
const diff = newRent - oldRent;
const pct = fields.indexPct || "3,8";
return `Sehr geehrte/r ${tenant?.name || "Mieter/in"},
gemäß §557b BGB und der vereinbarten Indexmiete-Klausel in Ihrem Mietvertrag passen wir die Kaltmiete für ${unit?.label || "Ihre Wohnung"} (${property?.address || ""}) wie folgt an:
Bisherige Kaltmiete: ${oldRent.toLocaleString('de-DE')} €
Neue Kaltmiete: ${newRent.toLocaleString('de-DE')} €
Erhöhung: ${diff > 0 ? "+" : ""}${diff.toLocaleString('de-DE')} € (${pct} %)
Gültig ab: ${fmtDateLong(fields.fromDate)}
Grundlage: Verbraucherpreisindex des Statistischen Bundesamts. Die Berechnung finden Sie im Anhang.
Bitte passen Sie Ihren Dauerauftrag rechtzeitig an. Bei Rückfragen melden Sie sich gern.
Mit freundlichen Grüßen
Marie Tessmann`;
},
},
{
id: "appointment",
name: "Terminvorschlag",
category: "Termin",
icon: "📅",
channel: "email",
subject: "Terminvorschlag",
fields: [
{ id: "reason", label: "Anlass", type: "text", default: () => "Wohnungsbesichtigung" },
{ id: "date", label: "Datum", type: "date", default: () => "2026-05-12" },
{ id: "time", label: "Uhrzeit", type: "text", default: () => "14:00 – 15:00 Uhr" },
],
build: ({ tenant, unit, property, fields }) => `Hallo ${firstName(tenant?.name)},
ich würde gern einen Termin mit Ihnen vereinbaren — Anlass: ${fields.reason}.
Mein Vorschlag:
${fmtDateLong(fields.date)} · ${fields.time}
${property?.address || ""}${unit ? ", " + unit.label : ""}
Passt das? Falls nicht, schlagen Sie gern eine Alternative vor.
Viele Grüße
Marie Tessmann`,
},
{
id: "dunning",
name: "Mahnung Mietrückstand",
category: "Forderung",
icon: "!",
channel: "email",
subject: "Mietrückstand — Zahlungserinnerung",
fields: [
{ id: "amount", label: "Offener Betrag", type: "currency", default: (ctx) => ctx.unit?.rent || 0 },
{ id: "month", label: "Betroffener Monat", type: "text", default: () => "Mai 2026" },
{ id: "deadline", label: "Zahlungsfrist", type: "date", default: () => "2026-05-15" },
],
build: ({ tenant, unit, fields }) => `Sehr geehrte/r ${tenant?.name || "Mieter/in"},
unsere Buchhaltung verzeichnet für ${fields.month} einen Mietrückstand für ${unit?.label || ""} in Höhe von ${(fields.amount || 0).toLocaleString('de-DE')} €.
Bitte überweisen Sie den Betrag bis spätestens ${fmtDateLong(fields.deadline)} auf das bekannte Konto.
Sollte die Zahlung bereits erfolgt sein, betrachten Sie dieses Schreiben als gegenstandslos. Bei Liquiditätsproblemen melden Sie sich bitte umgehend — wir finden gemeinsam eine Lösung.
Mit freundlichen Grüßen
Marie Tessmann`,
},
{
id: "maintenance_confirm",
name: "Wartungstermin bestätigt",
category: "Wartung",
icon: "🔧",
channel: "email",
subject: "Wartungstermin",
fields: [
{ id: "issue", label: "Mangel/Arbeit", type: "text", default: (ctx) => ctx.maintenanceTitle || "Reparatur" },
{ id: "tradesperson", label: "Handwerker", type: "text", default: () => "Klempnerei Schmidt" },
{ id: "date", label: "Termin", type: "date", default: () => "2026-05-08" },
{ id: "window", label: "Zeitfenster", type: "text", default: () => "10:00 – 12:00 Uhr" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
danke für Ihre Meldung. Ich habe einen Termin für „${fields.issue}" organisiert:
${fields.tradesperson}
${fmtDateLong(fields.date)} · ${fields.window}
Bitte stellen Sie sicher, dass jemand zuhause ist oder hinterlegen Sie den Schlüssel. Der Handwerker meldet sich vorher kurz telefonisch.
Viele Grüße
Marie Tessmann`,
},
{
id: "nk_settlement",
name: "Nebenkostenabrechnung",
category: "Abrechnung",
icon: "Σ",
channel: "email",
subject: "Nebenkostenabrechnung — Ihr Saldo",
fields: [
{ id: "year", label: "Abrechnungsjahr", type: "text", default: () => "2025" },
{ id: "balance", label: "Saldo (− = Guthaben)", type: "currency", default: () => -148 },
],
build: ({ tenant, unit, fields }) => {
const bal = fields.balance || 0;
const isCredit = bal < 0;
return `Sehr geehrte/r ${tenant?.name || "Mieter/in"},
anbei die Nebenkostenabrechnung für ${unit?.label || "Ihre Wohnung"} für das Jahr ${fields.year}.
Ihr Saldo: ${isCredit ? "Guthaben " : "Nachzahlung "}${Math.abs(bal).toLocaleString('de-DE')} €
${isCredit
? "Den Betrag erhalten Sie in den nächsten 14 Tagen auf Ihr Konto erstattet."
: "Bitte überweisen Sie den Betrag innerhalb von 30 Tagen."}
Eine detaillierte Aufstellung finden Sie im Anhang.
Mit freundlichen Grüßen
Marie Tessmann`;
},
},
{
id: "modernization",
name: "Modernisierungs-Ankündigung",
category: "Vertrag",
icon: "🏗",
channel: "email",
subject: "Modernisierungsmaßnahme — Ankündigung",
fields: [
{ id: "measure", label: "Maßnahme", type: "text", default: () => "Fassadendämmung" },
{ id: "from", label: "Beginn", type: "date", default: () => "2026-08-01" },
{ id: "to", label: "Ende", type: "date", default: () => "2026-09-30" },
],
build: ({ tenant, property, fields }) => `Sehr geehrte/r ${tenant?.name || "Mieter/in"},
hiermit kündigen wir gemäß §555c BGB die folgende Modernisierungsmaßnahme im Objekt ${property?.address || ""} an:
Maßnahme: ${fields.measure}
Zeitraum: ${fmtDateLong(fields.from)} bis ${fmtDateLong(fields.to)}
Eine ausführliche Beschreibung der Arbeiten, der voraussichtlichen Belastungen und der zu erwartenden Mietanpassung finden Sie im Anhang.
Bei Rückfragen stehe ich Ihnen gern zur Verfügung.
Mit freundlichen Grüßen
Marie Tessmann`,
},
// ───── Hausmeister / Objekt-weite Maßnahmen (oft Rundmail) ─────
{
id: "water_shutoff",
name: "Wasserabstellung",
category: "Hausmeister",
icon: "💧",
channel: "email",
bulkFriendly: true,
subject: "Wasserabstellung im Haus",
fields: [
{ id: "date", label: "Datum", type: "date", default: () => "2026-05-14" },
{ id: "from", label: "Von", type: "text", default: () => "08:00 Uhr" },
{ id: "to", label: "Bis (ca.)", type: "text", default: () => "13:00 Uhr" },
{ id: "reason", label: "Grund", type: "text", default: () => "Reparatur Hauptleitung" },
],
build: ({ tenant, property, fields }) => `Hallo ${firstName(tenant?.name)},
aufgrund von Arbeiten (${fields.reason}) wird das Wasser im Objekt ${property?.address || ""} abgestellt:
${fmtDateLong(fields.date)} · ${fields.from} – ${fields.to}
Bitte rechtzeitig Wasser abkochen oder bevorraten. Nach Wiederinbetriebnahme empfiehlt sich, kurz die Hähne ablaufen zu lassen.
Vielen Dank für Ihr Verständnis.
Marie Tessmann`,
},
{
id: "stairwell_cleaning",
name: "Treppenhausreinigung",
category: "Hausmeister",
icon: "🧹",
channel: "email",
bulkFriendly: true,
subject: "Treppenhausreinigung — Hinweis",
fields: [
{ id: "weekday", label: "Wochentag", type: "text", default: () => "Donnerstag" },
{ id: "time", label: "Uhrzeit", type: "text", default: () => "ab 09:00 Uhr" },
{ id: "company", label: "Reinigungsfirma", type: "text", default: () => "Sauberwerk GmbH" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
ab sofort übernimmt ${fields.company} die wöchentliche Treppenhausreinigung — jeden ${fields.weekday}, ${fields.time}.
Bitte stellen Sie bis dahin Schuhe, Fußmatten und persönliche Gegenstände aus dem Hausflur. Die Kosten sind Bestandteil der Nebenkostenabrechnung.
Viele Grüße
Marie Tessmann`,
},
{
id: "chimney_sweep",
name: "Schornsteinfeger-Termin",
category: "Hausmeister",
icon: "🔥",
channel: "email",
bulkFriendly: true,
subject: "Schornsteinfeger — Pflichttermin",
fields: [
{ id: "date", label: "Datum", type: "date", default: () => "2026-05-20" },
{ id: "window", label: "Zeitfenster", type: "text", default: () => "10:00 – 14:00 Uhr" },
{ id: "name", label: "Schornsteinfeger", type: "text", default: () => "Bezirksschornsteinfeger Krause" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
der Schornsteinfeger (${fields.name}) führt die jährliche Pflichtüberprüfung durch:
${fmtDateLong(fields.date)} · ${fields.window}
Bitte sorgen Sie dafür, dass die Wohnung zugänglich ist (jemand zuhause oder Schlüssel hinterlegt). Der Termin ist gesetzlich verpflichtend (§1 SchfHwG).
Vielen Dank.
Marie Tessmann`,
},
{
id: "smoke_detector",
name: "Rauchmelder-Wartung",
category: "Hausmeister",
icon: "🚨",
channel: "email",
bulkFriendly: true,
subject: "Jährliche Rauchmelder-Wartung",
fields: [
{ id: "date", label: "Datum", type: "date", default: () => "2026-06-04" },
{ id: "window", label: "Zeitfenster", type: "text", default: () => "14:00 – 17:00 Uhr" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
die DIN 14676 schreibt eine jährliche Funktionsprüfung der Rauchmelder vor. Termin in Ihrer Wohnung:
${fmtDateLong(fields.date)} · ${fields.window}
Sollten Sie verhindert sein, melden Sie sich bitte für eine Alternative — der Termin ist verpflichtend.
Viele Grüße
Marie Tessmann`,
},
{
id: "winter_service",
name: "Winterdienst-Hinweis",
category: "Hausmeister",
icon: "❄",
channel: "email",
bulkFriendly: true,
subject: "Winterdienst & Streupflicht",
fields: [
{ id: "season", label: "Saison", type: "text", default: () => "November 2026 – März 2027" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
zur Erinnerung — in der Saison ${fields.season} gilt im Objekt der Streu- und Räumplan im Hausflur. Werktags 7:00 – 20:00 Uhr, Sonn- und Feiertags 9:00 – 20:00 Uhr.
Bitte halten Sie Ihre Tage ein. Bei Verhinderung organisieren Sie bitte einen Tausch mit einem Nachbarn.
Viele Grüße
Marie Tessmann`,
},
{
id: "garbage_calendar",
name: "Müllkalender-Hinweis",
category: "Hausmeister",
icon: "🗑",
channel: "email",
bulkFriendly: true,
subject: "Müllabfuhr-Änderung",
fields: [
{ id: "wasteType", label: "Abfallart", type: "text", default: () => "Gelbe Tonne" },
{ id: "newDay", label: "Neuer Abholtag", type: "text", default: () => "Mittwoch (alle 14 Tage, ungerade KW)" },
{ id: "fromDate", label: "Gültig ab", type: "date", default: () => "2026-06-01" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
ab ${fmtDateLong(fields.fromDate)} ändert sich die Abholung der ${fields.wasteType}: ${fields.newDay}.
Bitte stellen Sie die Tonne am Vorabend bis 7:00 Uhr bereit.
Viele Grüße
Marie Tessmann`,
},
{
id: "house_rules",
name: "Hausordnung-Erinnerung",
category: "Hausmeister",
icon: "📜",
channel: "email",
bulkFriendly: true,
subject: "Erinnerung: Hausordnung",
fields: [
{ id: "topic", label: "Anlass", type: "text", default: () => "Ruhezeiten und Fahrradstellplätze" },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
ich möchte freundlich an die Hausordnung erinnern, insbesondere zum Thema ${fields.topic}.
Ruhezeiten gelten werktags 22:00 – 7:00 Uhr und durchgehend an Sonn- und Feiertagen. Fahrräder gehören in den Fahrradkeller, nicht ins Treppenhaus (Brandschutz).
Vielen Dank für die Rücksichtnahme — eine gute Nachbarschaft macht das Wohnen für alle angenehmer.
Viele Grüße
Marie Tessmann`,
},
{
id: "garden_service",
name: "Gartenpflege-Termin",
category: "Hausmeister",
icon: "🌿",
channel: "email",
bulkFriendly: true,
subject: "Gartenpflege im Haus",
fields: [
{ id: "date", label: "Datum", type: "date", default: () => "2026-05-22" },
{ id: "company", label: "Gärtner", type: "text", default: () => "Grünwerk Stahl" },
],
build: ({ tenant, fields, property }) => `Hallo ${firstName(tenant?.name)},
am ${fmtDateLong(fields.date)} kümmert sich ${fields.company} um den Garten / die Außenanlagen von ${property?.address || ""}.
Bitte räumen Sie persönliche Gegenstände (Stühle, Spielzeug, Wäsche) aus den Außenbereichen.
Viele Grüße
Marie Tessmann`,
},
// ───── Mieter-Service ─────
{
id: "key_lost",
name: "Antwort: Schlüsselverlust",
category: "Mieter-Service",
icon: "🔑",
channel: "email",
subject: "Schlüsselverlust",
fields: [
{ id: "newKeyCost", label: "Neue Schlüssel", type: "currency", default: () => 85 },
{ id: "lockChange", label: "Zylindertausch (falls nötig)", type: "currency", default: () => 280 },
],
build: ({ tenant, fields }) => `Hallo ${firstName(tenant?.name)},
danke für Ihre umgehende Meldung. Vorgehen:
1. Wir lassen ${(fields.newKeyCost || 0).toLocaleString('de-DE')} € für einen Ersatzschlüssel anfertigen.
2. Falls der Schlüssel nicht aufgefunden wird, tauschen wir aus Sicherheitsgründen den Zylinder (~${(fields.lockChange || 0).toLocaleString('de-DE')} €).
3. Die Kosten gehen zu Ihren Lasten — bitte prüfen Sie, ob Ihre Hausratversicherung den Verlust abdeckt.
Bitte bestätigen Sie kurz das Vorgehen.
Viele Grüße
Marie Tessmann`,
},
{
id: "owner_change",
name: "Eigentümerwechsel",
category: "Vertrag",
icon: "↻",
channel: "email",
bulkFriendly: true,
subject: "Wichtige Information — Eigentümerwechsel",
fields: [
{ id: "fromDate", label: "Ab Datum", type: "date", default: () => "2026-07-01" },
{ id: "newOwner", label: "Neuer Eigentümer", type: "text", default: () => "Tessmann Immobilien GmbH" },
{ id: "newIban", label: "Neue IBAN", type: "text", default: () => "DE12 3456 7890 1234 5678 90" },
],
build: ({ tenant, property, unit, fields }) => `Sehr geehrte/r ${tenant?.name || "Mieter/in"},
hiermit informieren wir Sie, dass das Objekt ${property?.address || ""} ab ${fmtDateLong(fields.fromDate)} in das Eigentum von ${fields.newOwner} übergeht.
Ihr Mietvertrag bleibt unverändert bestehen (§566 BGB — „Kauf bricht nicht Miete"). Ab dem genannten Datum überweisen Sie die Miete bitte auf folgendes Konto:
${fields.newOwner}
IBAN: ${fields.newIban}
Verwendungszweck: ${property?.name || ""}${unit?.label ? `, ${unit.label}` : ""}, Mieter ${tenant?.name || ""}
Bitte passen Sie Ihren Dauerauftrag rechtzeitig an.
Mit freundlichen Grüßen
Marie Tessmann`,
},
];
function firstName(name) {
if (!name) return "";
if (name.startsWith("Familie")) return name;
if (name.startsWith("Dr.")) return name.split(" ").slice(0, 2).join(" ");
return name.split(" ")[0];
}
function fmtDateLong(s) {
if (!s) return "";
const d = new Date(s);
return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' });
}
// ───── Threads (mit assignees + read receipts) ─────
// Leer in Production — Threads entstehen erst durch echte Mieter-Kommunikation.
const INITIAL_THREADS = [];
const INITIAL_THREADS_DEMO_KEEP = [
{
id: "th1", with: "Anna Hofmann", tenantId: "t1", role: "Mieter", propertyId: "p1", unitId: "u1",
channel: "email", unread: 2, lastDate: "2026-04-30T14:24",
needsReply: true, assignedTo: "o1",
messages: [
{ id: 1, from: "tenant", date: "2026-04-30T14:22", body: "Guten Tag Frau Tessmann, in der Küche tropft seit gestern der Wasserhahn — könnte jemand vorbeischauen? Mit freundlichen Grüßen, Anna Hofmann" },
{ id: 2, from: "tenant", date: "2026-04-30T14:24", body: "Anbei ein Foto.", attachment: "wasserhahn_tropft.jpg" },
]
},
{
id: "th2", with: "Café Mira GbR", tenantId: "t12", role: "Pächter", propertyId: "p4", unitId: "u13",
channel: "email", unread: 0, lastDate: "2026-04-29T09:14",
needsReply: false, assignedTo: "o2",
messages: [
{ id: 1, from: "tenant", date: "2026-04-28T17:10", body: "Hallo, die Indexmiete-Anpassung ab September — könnten wir das einmal kurz telefonisch besprechen?",
receipt: { sentBy: null } },
{ id: 2, from: "owner", senderId: "o2", date: "2026-04-29T09:14", body: "Gerne, ich rufe morgen zwischen 10 und 12 an.",
receipt: { sent: "2026-04-29T09:14", delivered: "2026-04-29T09:14:08", read: "2026-04-29T09:18" } },
]
},
{
id: "th3", with: "Familie Schulz", tenantId: "t3", role: "Mieter", propertyId: "p1", unitId: "u3",
channel: "chat", unread: 1, lastDate: "2026-04-30T08:45",
needsReply: true, assignedTo: null,
messages: [
{ id: 1, from: "tenant", date: "2026-04-30T08:45", body: "Können wir am Samstag den Keller besichtigen? Wir wollten alte Kartons aussortieren." },
]
},
{
id: "th4", with: "Tobias Lang", tenantId: "t5", role: "Mieter", propertyId: "p2", unitId: "u6",
channel: "email", unread: 0, lastDate: "2026-04-25T11:00",
needsReply: false, assignedTo: "o1",
messages: [
{ id: 1, from: "owner", senderId: "o1", date: "2026-04-22T10:00", body: "Modernisierungs-Ankündigung Fassade — siehe Anhang.", attachment: "Modernisierung_Stadtpark7.pdf",
receipt: { sent: "2026-04-22T10:00", delivered: "2026-04-22T10:00:15", read: "2026-04-22T10:42" } },
{ id: 2, from: "tenant", date: "2026-04-25T11:00", body: "Verstanden, danke für die frühzeitige Info." },
]
},
{
id: "th5", with: "Jens Maier", tenantId: "t7", role: "Mieter", propertyId: "p2", unitId: "u8",
channel: "email", unread: 1, lastDate: "2026-04-28T16:30",
needsReply: true, assignedTo: "o3",
messages: [
{ id: 1, from: "owner", senderId: "o1", date: "2026-04-12T09:00", body: "Bestätigung Ihrer Kündigung zum 31.07.2026.",
receipt: { sent: "2026-04-12T09:00", delivered: "2026-04-12T09:00:12", read: "2026-04-12T11:08" } },
{ id: 2, from: "tenant", date: "2026-04-28T16:30", body: "Können wir die Übergabe schon auf den 25.07. vorziehen? Danke." },
]
},
{
id: "th6", with: "Lukas Wolf", tenantId: "t11", role: "Mieter", propertyId: "p4", unitId: "u12",
channel: "chat", unread: 0, lastDate: "2026-04-26T19:12",
needsReply: false, assignedTo: "o1",
messages: [
{ id: 1, from: "owner", senderId: "o1", date: "2026-04-26T19:00", body: "Mahnung: 980 € Mietrückstand — bitte bis 15.05. überweisen.",
receipt: { sent: "2026-04-26T19:00", delivered: "2026-04-26T19:00:04", read: "2026-04-26T19:11" } },
{ id: 2, from: "tenant", date: "2026-04-26T19:12", body: "Wird Anfang Mai überwiesen, sorry." },
]
},
];
// ───── State Store ─────
const MessagesContext = React.createContext(null);
function MessagesProvider({ children }) {
const [threads, setThreads] = React.useState(INITIAL_THREADS);
// composer prefill: { threadId?, tenantId?, propertyId?, unitId?, templateId?, ctx? }
const [pendingCompose, setPendingCompose] = React.useState(null);
const value = {
threads, setThreads,
pendingCompose, setPendingCompose,
appendMessage: (threadId, msg) => {
setThreads(prev => prev.map(t => t.id === threadId
? { ...t, messages: [...t.messages, msg], lastDate: msg.date, needsReply: false }
: t));
},
markRead: (threadId) => {
setThreads(prev => prev.map(t => t.id === threadId ? { ...t, unread: 0 } : t));
},
assignThread: (threadId, ownerId) => {
setThreads(prev => prev.map(t => t.id === threadId ? { ...t, assignedTo: ownerId } : t));
},
};
return