Implement database migration, notification logs, and one-click Mailcow setup
This commit is contained in:
212
app/[lang]/dashboard/mappings/page.tsx
Normal file
212
app/[lang]/dashboard/mappings/page.tsx
Normal 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>; }
|
||||
Reference in New Issue
Block a user