Implement database migration, notification logs, and one-click Mailcow setup

This commit is contained in:
AyrisAI
2026-05-14 16:49:11 +03:00
parent f328296c64
commit b024e20027
18 changed files with 1067 additions and 166 deletions

View File

@@ -0,0 +1,212 @@
"use client";
import { useState, useEffect } from "react";
import { useDictionary } from "@/components/DictionaryContext";
interface User {
id: string;
name: string;
email: string;
}
interface Mapping {
id: string;
email: string;
userId: string;
user: User;
}
export default function MappingsPage() {
const [mappings, setMappings] = useState<Mapping[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
// Form state
const [newEmail, setNewEmail] = useState("");
const [newUserId, setNewUserId] = useState("");
const [saving, setSaving] = useState(false);
const dict = useDictionary();
const fetchData = async () => {
setLoading(true);
try {
const [mRes, uRes] = await Promise.all([
fetch("/api/mappings"),
fetch("/api/users")
]);
const [mData, uData] = await Promise.all([mRes.json(), uRes.json()]);
setMappings(Array.isArray(mData) ? mData : []);
setUsers(Array.isArray(uData) ? uData : []);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleAdd = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const res = await fetch("/api/mappings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: newEmail, userId: newUserId })
});
if (res.ok) {
setIsModalOpen(false);
setNewEmail("");
setNewUserId("");
fetchData();
}
} catch (e) {
console.error(e);
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("Bu eşleştirmeyi silmek istediğinize emin misiniz?")) return;
try {
const res = await fetch(`/api/mappings/${id}`, { method: "DELETE" });
if (res.ok) fetchData();
} catch (e) {
console.error(e);
}
};
const filtered = mappings.filter(m =>
m.email.toLowerCase().includes(search.toLowerCase()) ||
m.user.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<div className="page-header">
<div>
<h1 className="page-title">{dict.mappings.title}</h1>
<p className="page-subtitle">{dict.mappings.subtitle}</p>
</div>
<button className="btn btn-primary" onClick={() => setIsModalOpen(true)}>
<PlusIcon />
{dict.mappings.addMapping}
</button>
</div>
<div className="page-body">
<div className="search-bar">
<div className="search-input-wrap">
<span className="search-icon"><SearchIcon /></span>
<input
type="text"
className="input search-input"
placeholder={dict.mappings.searchPlaceholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="table-wrap">
{loading ? (
<div className="empty-state"><span className="spinner" /></div>
) : filtered.length === 0 ? (
<div className="empty-state">
<div className="empty-icon"><LinkIcon /></div>
<div style={{ fontWeight: 600 }}>{dict.mappings.noMappings}</div>
</div>
) : (
<table>
<thead>
<tr>
<th>{dict.mappings.email}</th>
<th>{dict.mappings.user}</th>
<th style={{ width: 80 }}></th>
</tr>
</thead>
<tbody>
{filtered.map((m) => (
<tr key={m.id}>
<td>
<div style={{ fontWeight: 500 }}>{m.email}</div>
</td>
<td>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div className="user-avatar" style={{ width: 24, height: 24, fontSize: 10 }}>
{m.user.name[0]?.toUpperCase()}
</div>
<span>{m.user.name}</span>
<span style={{ fontSize: 11, color: "var(--text-secondary)" }}>({m.user.email})</span>
</div>
</td>
<td>
<button className="btn btn-ghost" onClick={() => handleDelete(m.id)} style={{ color: "var(--error)" }}>
<TrashIcon />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{isModalOpen && (
<div className="modal-overlay">
<div className="modal-content" style={{ maxWidth: 450 }}>
<div className="modal-header">
<h2 className="modal-title">{dict.mappings.addMapping}</h2>
<button className="modal-close" onClick={() => setIsModalOpen(false)}>×</button>
</div>
<form onSubmit={handleAdd} className="form-group" style={{ padding: 20 }}>
<div>
<label className="label">{dict.mappings.email}</label>
<input
className="input"
placeholder="info@domain.com"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
required
/>
</div>
<div>
<label className="label">{dict.mappings.user}</label>
<select
className="input"
value={newUserId}
onChange={(e) => setNewUserId(e.target.value)}
required
>
<option value="">{dict.mappings.user} seçin...</option>
{users.map(u => (
<option key={u.id} value={u.id}>{u.name} ({u.email})</option>
))}
</select>
</div>
<div style={{ display: "flex", gap: 10, marginTop: 10 }}>
<button type="button" className="btn btn-ghost" style={{ flex: 1 }} onClick={() => setIsModalOpen(false)}>İptal</button>
<button type="submit" className="btn btn-primary" style={{ flex: 1 }} disabled={saving}>
{saving ? <span className="spinner" /> : "Kaydet"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}
function PlusIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>; }
function SearchIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>; }
function LinkIcon() { return <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>; }
function TrashIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }