281 lines
12 KiB
TypeScript
281 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "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);
|
||
|
||
// 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 }}>
|
||
<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">Telegram ID</label>
|
||
<input className="input" placeholder="Örn: 5009005027" value={formData.telegramId} onChange={e => setFormData({...formData, telegramId: e.target.value})} />
|
||
</div>
|
||
</div>
|
||
<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 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>; }
|