Files

297 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useDictionary } from "@/components/DictionaryContext";
interface User {
id: string;
name: string;
email: string;
role: string;
domains: string[];
telegramId?: string;
password?: string;
}
export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const { data: session } = useSession();
// Form state
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
role: "DOMAIN_ADMIN",
domains: "",
telegramId: ""
});
const [saving, setSaving] = useState(false);
const dict = useDictionary();
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch("/api/users");
const data = await res.json();
setUsers(Array.isArray(data) ? data : []);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
const openAddModal = () => {
setEditingUser(null);
setFormData({ name: "", email: "", password: "", role: "DOMAIN_ADMIN", domains: "", telegramId: "" });
setIsModalOpen(true);
};
const openEditModal = (user: User) => {
setEditingUser(user);
setFormData({
name: user.name || "",
email: user.email,
password: user.password || "",
role: user.role,
domains: user.domains.join(", "),
telegramId: user.telegramId || ""
});
setIsModalOpen(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const url = editingUser ? `/api/users/${editingUser.id}` : "/api/users";
const method = editingUser ? "PATCH" : "POST";
const payload = {
...formData,
domains: formData.domains.split(",").map(d => d.trim()).filter(Boolean)
};
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (res.ok) {
setIsModalOpen(false);
fetchUsers();
}
} catch (e) {
console.error(e);
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("Bu kullanıcıyı silmek istediğinize emin misiniz?")) return;
try {
const res = await fetch(`/api/users/${id}`, { method: "DELETE" });
if (res.ok) fetchUsers();
} catch (e) {
console.error(e);
}
};
const filtered = users.filter(
(u) =>
u.name?.toLowerCase().includes(search.toLowerCase()) ||
u.email.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<div className="page-header">
<div>
<h1 className="page-title">{dict.users.title}</h1>
<p className="page-subtitle">{dict.users.subtitle}</p>
</div>
<button className="btn btn-primary" onClick={openAddModal}>
<PlusIcon />
{dict.users.addUser}
</button>
</div>
<div className="page-body">
<div className="card" style={{ display: "flex", gap: 14, alignItems: "flex-start", border: "1px solid var(--accent-dim)", background: "var(--accent-dim)" }}>
<div style={{ color: "var(--accent-hover)", flexShrink: 0, paddingTop: 2 }}>
<InfoIcon />
</div>
<div>
<div style={{ fontWeight: 600, color: "var(--accent-hover)", marginBottom: 6 }}>Kullanıcı Yönetimi</div>
<div style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.7 }}>
{dict.users.info}
</div>
</div>
</div>
<div className="search-bar">
<div className="search-input-wrap">
<span className="search-icon"><SearchIcon /></span>
<input
type="text"
className="input search-input"
placeholder={dict.users.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"><UsersIcon /></div>
<div style={{ fontWeight: 600 }}>{dict.users.noUsers}</div>
</div>
) : (
<table>
<thead>
<tr>
<th>{dict.users.username}</th>
<th>{dict.users.role}</th>
<th>{dict.users.domains}</th>
<th>Telegram ID</th>
<th style={{ width: 100 }}></th>
</tr>
</thead>
<tbody>
{filtered.map((u) => (
<tr key={u.id}>
<td>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div className="user-avatar" style={{ width: 32, height: 32, fontSize: 13 }}>
{u.name ? u.name[0]?.toUpperCase() : "?"}
</div>
<div>
<div style={{ fontWeight: 500 }}>{u.name}</div>
<div style={{ fontSize: 11, color: "var(--text-secondary)" }}>{u.email}</div>
</div>
</div>
</td>
<td>
<span className={`badge ${u.role === "SUPER_ADMIN" ? "badge-blue" : "badge-green"}`}>
{u.role === "SUPER_ADMIN" ? `${dict.users.superAdmin}` : dict.users.domainAdmin}
</span>
</td>
<td>
{u.domains.includes("*") ? (
<span className="badge badge-blue">{dict.users.allDomains}</span>
) : (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{u.domains.map((d) => (
<span key={d} className="badge badge-green">{d}</span>
))}
</div>
)}
</td>
<td>
<code style={{ fontSize: 11 }}>{u.telegramId || "-"}</code>
</td>
<td>
<div style={{ display: "flex", gap: 4 }}>
<button className="btn btn-ghost" onClick={() => openEditModal(u)}><EditIcon /></button>
<button className="btn btn-ghost" onClick={() => handleDelete(u.id)} style={{ color: "var(--error)" }}><TrashIcon /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
{isModalOpen && (
<div className="modal-overlay">
<div className="modal-content" style={{ maxWidth: 500 }}>
<div className="modal-header">
<h2 className="modal-title">{editingUser ? dict.users.editUser : dict.users.addUser}</h2>
<button className="modal-close" onClick={() => setIsModalOpen(false)}>×</button>
</div>
<form onSubmit={handleSubmit} className="form-group" style={{ padding: 20 }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
<div>
<label className="label">İsim</label>
<input className="input" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} required />
</div>
<div>
<label className="label">E-posta</label>
<input className="input" type="email" value={formData.email} onChange={e => setFormData({...formData, email: e.target.value})} required />
</div>
</div>
<div>
<label className="label">Şifre {editingUser && "(Değiştirmek istemiyorsanız boş bırakın)"}</label>
<input className="input" type="password" value={formData.password} onChange={e => setFormData({...formData, password: e.target.value})} required={!editingUser} />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
{session?.user?.role === "SUPER_ADMIN" ? (
<div>
<label className="label">Rol</label>
<select className="input" value={formData.role} onChange={e => setFormData({...formData, role: e.target.value})}>
<option value="SUPER_ADMIN">Süper Admin</option>
<option value="DOMAIN_ADMIN">Domain Admin</option>
</select>
</div>
) : (
<div>
<label className="label">Rol</label>
<input className="input" value="Domain Admin" disabled />
</div>
)}
<div>
<label className="label">Telegram ID</label>
<input className="input" placeholder="Örn: 5009005027" value={formData.telegramId} onChange={e => setFormData({...formData, telegramId: e.target.value})} />
</div>
</div>
{session?.user?.role === "SUPER_ADMIN" ? (
<div>
<label className="label">İzinli Domainler (Virgülle ayırın, tümü için *)</label>
<input className="input" placeholder="domain1.com, domain2.com" value={formData.domains} onChange={e => setFormData({...formData, domains: e.target.value})} />
</div>
) : (
<div>
<label className="label">İzinli Domainler</label>
<input className="input" value={session?.user?.domains?.join(", ")} disabled />
</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 UsersIcon() { return <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>; }
function InfoIcon() { return <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></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>; }
function EditIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>; }