TorbatYar/frontend/app/admin/services/page.tsx
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

227 lines
7.5 KiB
TypeScript
Raw 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 { FormEvent, useCallback, useEffect, useState } from "react";
import { api, ApiError, type ServiceEntry, type ServiceStatus } from "@/lib/api";
import {
AdminPageHeader,
Banner,
Button,
EmptyState,
Modal,
SelectField,
StatusBadge,
TextField,
} from "@/components/admin/controls";
const STATUS_OPTIONS: { value: ServiceStatus; label: string }[] = [
{ value: "active", label: "فعال" },
{ value: "inactive", label: "غیرفعال" },
{ value: "maintenance", label: "در حال تعمیر" },
];
export default function AdminServicesPage() {
const [services, setServices] = useState<ServiceEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [open, setOpen] = useState(false);
const [busyId, setBusyId] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError("");
try {
const page = await api.services.list(1, 100);
setServices(page.items);
} catch (e) {
setError(e instanceof ApiError ? e.message : "خطا در بارگذاری سرویس‌ها");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
const changeStatus = async (svc: ServiceEntry, status: ServiceStatus) => {
if (status === svc.status) return;
setBusyId(svc.id);
setError("");
try {
await api.services.updateStatus(svc.id, status);
await load();
} catch (e) {
setError(e instanceof ApiError ? e.message : "تغییر وضعیت ناموفق بود");
} finally {
setBusyId(null);
}
};
return (
<div className="space-y-6">
<AdminPageHeader
title="سرویس‌ها"
subtitle="ثبت و مدیریت وضعیت سرویس‌های پلتفرم"
action={<Button onClick={() => setOpen(true)}>ثبت سرویس</Button>}
/>
{error && <Banner variant="error">{error}</Banner>}
<section className="overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm">
{loading ? (
<EmptyState message="در حال بارگذاری..." />
) : services.length === 0 ? (
<EmptyState message="هنوز سرویسی ثبت نشده است." />
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-sm">
<thead className="bg-gray-50 text-right">
<tr>
<th className="px-4 py-3 font-semibold text-secondary">نام</th>
<th className="px-4 py-3 font-semibold text-secondary">کلید</th>
<th className="px-4 py-3 font-semibold text-secondary">آدرس</th>
<th className="px-4 py-3 font-semibold text-secondary">وضعیت</th>
<th className="px-4 py-3 font-semibold text-secondary">تغییر وضعیت</th>
</tr>
</thead>
<tbody>
{services.map((s) => (
<tr key={s.id} className="border-t border-gray-100">
<td className="px-4 py-3 font-medium text-secondary">{s.name}</td>
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
{s.service_key}
</td>
<td className="px-4 py-3 font-mono text-gray-500" dir="ltr">
{s.base_url || "—"}
</td>
<td className="px-4 py-3">
<StatusBadge status={s.status} />
</td>
<td className="px-4 py-3">
<select
className="rounded-lg border border-gray-200 px-2 py-1 text-xs focus:border-primary focus:outline-none disabled:opacity-50"
value={s.status}
disabled={busyId === s.id}
onChange={(e) => changeStatus(s, e.target.value as ServiceStatus)}
>
{STATUS_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
<CreateServiceModal
open={open}
onClose={() => setOpen(false)}
onCreated={() => {
setOpen(false);
void load();
}}
/>
</div>
);
}
function CreateServiceModal({
open,
onClose,
onCreated,
}: {
open: boolean;
onClose: () => void;
onCreated: () => void;
}) {
const [serviceKey, setServiceKey] = useState("");
const [name, setName] = useState("");
const [baseUrl, setBaseUrl] = useState("");
const [healthUrl, setHealthUrl] = useState("");
const [status, setStatus] = useState<ServiceStatus>("active");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError("");
setSaving(true);
try {
await api.services.create({
service_key: serviceKey.trim(),
name: name.trim(),
base_url: baseUrl.trim() || null,
health_check_url: healthUrl.trim() || null,
status,
});
setServiceKey("");
setName("");
setBaseUrl("");
setHealthUrl("");
onCreated();
} catch (err) {
setError(err instanceof ApiError ? err.message : "ثبت سرویس ناموفق بود");
} finally {
setSaving(false);
}
};
return (
<Modal open={open} title="ثبت سرویس جدید" onClose={onClose}>
<form onSubmit={handleSubmit} className="space-y-4">
<TextField
label="کلید سرویس"
required
dir="ltr"
className="font-mono"
value={serviceKey}
onValue={setServiceKey}
disabled={saving}
placeholder="sms-service"
/>
<TextField label="نام" required value={name} onValue={setName} disabled={saving} />
<TextField
label="آدرس پایه (اختیاری)"
dir="ltr"
className="font-mono"
value={baseUrl}
onValue={setBaseUrl}
disabled={saving}
placeholder="http://sms-service:9000"
/>
<TextField
label="آدرس health check (اختیاری)"
dir="ltr"
className="font-mono"
value={healthUrl}
onValue={setHealthUrl}
disabled={saving}
placeholder="http://sms-service:9000/health"
/>
<SelectField
label="وضعیت"
value={status}
onValue={(v) => setStatus(v as ServiceStatus)}
options={STATUS_OPTIONS}
disabled={saving}
/>
{error && <Banner variant="error">{error}</Banner>}
<div className="flex justify-end gap-3 pt-2">
<Button variant="outline" onClick={onClose} type="button">
انصراف
</Button>
<Button type="submit" loading={saving} loadingText="در حال ثبت...">
ثبت سرویس
</Button>
</div>
</form>
</Modal>
);
}