From 62a5121ffe6bc7e2b8bd7e4792945b15f9a7b870 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 2 Jun 2026 18:59:07 +0330 Subject: [PATCH] feat(identity+admin): CRM analytics + customer notes + user power-actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modeled on the legacy DivineGateWeb admin (CRM + Security/* actions): - identity-svc AdminService + AdminController (admin-gated): - GET /v1/admin/crm/analytics — signups/buyers/conversion/revenue + daily series (from identity.users + identity.payments) - GET/PUT /v1/users/{id}/crm — tags / note / pipeline status (user_crm table, mig 20) - power-actions: POST /v1/users/{id}/{balance,password,charge,moderator,grant-plan} - admin UI: /admin/crm dashboard (funnel cards + daily signup/revenue bars); per-user "مدیریت" modal in Users (balance, render charge, plan days, password, moderator, CRM notes) Co-Authored-By: Claude Opus 4.8 --- .../db/migrations/20_identity_user_crm.sql | 15 ++ messages/en.json | 3 +- messages/fa.json | 3 +- .../Application/Services/AdminService.cs | 137 ++++++++++++++++++ .../Controllers/AdminController.cs | 75 ++++++++++ .../Domain/Entities/UserCrm.cs | 11 ++ .../Infrastructure/Data/IdentityDbContext.cs | 9 ++ .../FlatRender.IdentitySvc/Models/Admin.cs | 29 ++++ .../FlatRender.IdentitySvc/Program.cs | 1 + src/app/[locale]/admin/crm/page.tsx | 7 + src/app/[locale]/admin/layout.tsx | 1 + src/components/admin/CrmAdmin.tsx | 96 ++++++++++++ src/components/admin/UserActions.tsx | 120 +++++++++++++++ src/components/admin/admin-resources.tsx | 8 +- 14 files changed, 512 insertions(+), 3 deletions(-) create mode 100644 backend/db/migrations/20_identity_user_crm.sql create mode 100644 services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs create mode 100644 services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs create mode 100644 services/identity/FlatRender.IdentitySvc/Domain/Entities/UserCrm.cs create mode 100644 services/identity/FlatRender.IdentitySvc/Models/Admin.cs create mode 100644 src/app/[locale]/admin/crm/page.tsx create mode 100644 src/components/admin/CrmAdmin.tsx create mode 100644 src/components/admin/UserActions.tsx diff --git a/backend/db/migrations/20_identity_user_crm.sql b/backend/db/migrations/20_identity_user_crm.sql new file mode 100644 index 0000000..fc5fe6c --- /dev/null +++ b/backend/db/migrations/20_identity_user_crm.sql @@ -0,0 +1,15 @@ +-- ===================================================================== +-- IDENTITY SCHEMA — Part 20: CRM notes/tags per customer +-- Lightweight CRM overlay on identity.users (tags, free-form note, pipeline status). +-- Acquisition/conversion analytics are computed live from users + payments. +-- ===================================================================== + +SET search_path TO identity, public; + +CREATE TABLE IF NOT EXISTS user_crm ( + user_id UUID PRIMARY KEY, + tags TEXT[] NOT NULL DEFAULT '{}', + note TEXT, + status TEXT NOT NULL DEFAULT 'new', -- new | contacted | customer | churned + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/messages/en.json b/messages/en.json index 989d013..eb462a1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -325,7 +325,8 @@ "discounts": "Discounts", "siteSettings": "Settings", "messaging": "Messaging", - "marketing": "Marketing" + "marketing": "Marketing", + "crm": "CRM" }, "appAdminNodesPage": { "title": "Render Nodes", diff --git a/messages/fa.json b/messages/fa.json index b973d83..0e79cfd 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -325,7 +325,8 @@ "discounts": "تخفیف‌ها", "siteSettings": "تنظیمات سایت", "messaging": "پیام‌رسانی", - "marketing": "بازاریابی" + "marketing": "بازاریابی", + "crm": "مدیریت مشتریان" }, "appAdminNodesPage": { "title": "نودهای رندر", diff --git a/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs b/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs new file mode 100644 index 0000000..3bdc5cb --- /dev/null +++ b/services/identity/FlatRender.IdentitySvc/Application/Services/AdminService.cs @@ -0,0 +1,137 @@ +using FlatRender.IdentitySvc.Domain.Entities; +using FlatRender.IdentitySvc.Domain.Enums; +using FlatRender.IdentitySvc.Infrastructure.Data; +using FlatRender.IdentitySvc.Models; +using Microsoft.EntityFrameworkCore; + +namespace FlatRender.IdentitySvc.Application.Services; + +/// Admin CRM analytics, customer notes/tags, and per-user power-actions. +public class AdminService(IdentityDbContext db) +{ + // ── CRM acquisition / conversion analytics ────────────────────────────── + + public async Task GetCrmAnalyticsAsync(Guid tenantId, DateTime start, DateTime end) + { + start = start.Date; + end = end.Date.AddDays(1); // inclusive of the end day + if ((end - start).TotalDays > 366) end = start.AddDays(366); + + var signups = await db.Users + .Where(u => u.TenantId == tenantId && u.DeletedAt == null && u.RegisterDate >= start && u.RegisterDate < end) + .Select(u => new { u.Id, u.RegisterDate }) + .ToListAsync(); + var ids = signups.Select(s => s.Id).ToList(); + + var buyerIds = await db.Payments + .Where(p => p.TenantId == tenantId && p.Status == PaymentStatus.Succeeded && ids.Contains(p.UserId)) + .Select(p => p.UserId).Distinct().ToListAsync(); + + var pays = await db.Payments + .Where(p => p.TenantId == tenantId && p.Status == PaymentStatus.Succeeded && p.CreatedAt >= start && p.CreatedAt < end) + .Select(p => new { p.UserId, p.AmountMinor, p.CreatedAt }) + .ToListAsync(); + + var payingAllTime = await db.Payments + .Where(p => p.TenantId == tenantId && p.Status == PaymentStatus.Succeeded) + .Select(p => p.UserId).Distinct().CountAsync(); + + var daily = new List(); + for (var d = start; d < end; d = d.AddDays(1)) + { + var de = d.AddDays(1); + var dayPays = pays.Where(p => p.CreatedAt >= d && p.CreatedAt < de).ToList(); + daily.Add(new CrmDailyPoint( + d.ToString("yyyy-MM-dd"), + signups.Count(u => u.RegisterDate >= d && u.RegisterDate < de), + dayPays.Select(p => p.UserId).Distinct().Count(), + dayPays.Sum(p => p.AmountMinor))); + } + + var total = signups.Count; + var buyers = buyerIds.Count; + return new CrmAnalyticsResponse( + total, buyers, total - buyers, + total > 0 ? Math.Round((double)buyers / total * 100, 1) : 0, + pays.Sum(p => p.AmountMinor), payingAllTime, daily); + } + + // ── CRM notes / tags ──────────────────────────────────────────────────── + + public async Task GetUserCrmAsync(Guid userId) + { + var c = await db.Set().FindAsync(userId); + return c == null + ? new UserCrmResponse([], null, "new") + : new UserCrmResponse(c.Tags, c.Note, c.Status); + } + + public async Task UpsertUserCrmAsync(Guid userId, UpsertUserCrmRequest req) + { + var c = await db.Set().FindAsync(userId); + var isNew = c == null; + c ??= new UserCrm { UserId = userId }; + if (req.Tags != null) c.Tags = req.Tags; + if (req.Note != null) c.Note = req.Note; + if (req.Status != null) c.Status = req.Status; + c.UpdatedAt = DateTime.UtcNow; + if (isNew) db.Add(c); + await db.SaveChangesAsync(); + return new UserCrmResponse(c.Tags, c.Note, c.Status); + } + + // ── Per-user power-actions ────────────────────────────────────────────── + + private async Task RequireUser(Guid id) => + await db.Users.FindAsync(id) ?? throw new KeyNotFoundException("User not found"); + + public async Task SetBalanceAsync(Guid userId, long amountMinor, bool add) + { + var u = await RequireUser(userId); + u.BalanceMinor = add ? u.BalanceMinor + amountMinor : amountMinor; + u.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + public async Task ResetPasswordAsync(Guid userId, string newPassword) + { + var u = await RequireUser(userId); + u.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword); + u.PasswordSetAt = DateTime.UtcNow; + u.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + public async Task AddChargeAsync(Guid userId, int seconds, int renderCount) + { + var u = await RequireUser(userId); + if (seconds != 0) u.UserDailyFreeChargeSec += seconds; + if (renderCount != 0) + { + u.MaxDailyRenderCount += renderCount; + u.DailyRemainRenderCount += renderCount; + } + u.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + public async Task SetModeratorAsync(Guid userId, bool enabled) + { + var u = await RequireUser(userId); + u.IsTenantAdmin = enabled; + u.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } + + public async Task GrantPlanDaysAsync(Guid userId, Guid planId, int days) + { + var plan = await db.UserPlans + .Where(p => p.UserId == userId && (planId == Guid.Empty || p.PlanId == planId)) + .OrderByDescending(p => p.ExpiresAt).FirstOrDefaultAsync() + ?? throw new KeyNotFoundException("No plan found for this user to extend"); + var baseDate = plan.ExpiresAt > DateTime.UtcNow ? plan.ExpiresAt : DateTime.UtcNow; + plan.ExpiresAt = baseDate.AddDays(days); + plan.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + } +} diff --git a/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs b/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs new file mode 100644 index 0000000..edb9549 --- /dev/null +++ b/services/identity/FlatRender.IdentitySvc/Controllers/AdminController.cs @@ -0,0 +1,75 @@ +using FlatRender.IdentitySvc.Application.Services; +using FlatRender.IdentitySvc.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace FlatRender.IdentitySvc.Controllers; + +[ApiController] +[Authorize(Roles = "Admin")] +public class AdminController(AdminService svc) : ControllerBase +{ + private Guid TenantId => + Guid.TryParse(User.FindFirst("tenant_id")?.Value, out var t) + ? t : Guid.Parse("00000000-0000-0000-0000-000000000001"); + + // ── CRM analytics ──────────────────────────────────────────────────────── + [HttpGet("v1/admin/crm/analytics")] + public async Task Crm([FromQuery] DateTime? start, [FromQuery] DateTime? end) + { + var s = start ?? DateTime.UtcNow.AddDays(-30); + var e = end ?? DateTime.UtcNow; + return Ok(await svc.GetCrmAnalyticsAsync(TenantId, s, e)); + } + + // ── CRM notes / tags ─────────────────────────────────────────────────────── + [HttpGet("v1/users/{userId:guid}/crm")] + public async Task GetCrm(Guid userId) => Ok(await svc.GetUserCrmAsync(userId)); + + [HttpPut("v1/users/{userId:guid}/crm")] + public async Task PutCrm(Guid userId, [FromBody] UpsertUserCrmRequest req) + => Ok(await svc.UpsertUserCrmAsync(userId, req)); + + // ── Power-actions ────────────────────────────────────────────────────────── + [HttpPost("v1/users/{userId:guid}/balance")] + public async Task Balance(Guid userId, [FromBody] SetBalanceRequest req) + { + await svc.SetBalanceAsync(userId, req.AmountMinor, req.Add); + return Ok(new { ok = true }); + } + + [HttpPost("v1/users/{userId:guid}/password")] + public async Task Password(Guid userId, [FromBody] ResetPasswordRequest req) + { + await svc.ResetPasswordAsync(userId, req.NewPassword); + return Ok(new { ok = true }); + } + + [HttpPost("v1/users/{userId:guid}/charge")] + public async Task Charge(Guid userId, [FromBody] AddChargeRequest req) + { + await svc.AddChargeAsync(userId, req.Seconds, req.RenderCount); + return Ok(new { ok = true }); + } + + [HttpPost("v1/users/{userId:guid}/moderator")] + public async Task Moderator(Guid userId, [FromBody] SetFlagRequest req) + { + await svc.SetModeratorAsync(userId, req.Enabled); + return Ok(new { ok = true }); + } + + [HttpPost("v1/users/{userId:guid}/grant-plan")] + public async Task GrantPlan(Guid userId, [FromBody] GrantPlanDaysRequest req) + { + try + { + await svc.GrantPlanDaysAsync(userId, req.PlanId, req.Days); + return Ok(new { ok = true }); + } + catch (KeyNotFoundException ex) + { + return BadRequest(new { error = new { message = ex.Message } }); + } + } +} diff --git a/services/identity/FlatRender.IdentitySvc/Domain/Entities/UserCrm.cs b/services/identity/FlatRender.IdentitySvc/Domain/Entities/UserCrm.cs new file mode 100644 index 0000000..c33bef1 --- /dev/null +++ b/services/identity/FlatRender.IdentitySvc/Domain/Entities/UserCrm.cs @@ -0,0 +1,11 @@ +namespace FlatRender.IdentitySvc.Domain.Entities; + +/// Lightweight CRM overlay for a customer (tags / note / pipeline status). +public class UserCrm +{ + public Guid UserId { get; set; } + public string[] Tags { get; set; } = []; + public string? Note { get; set; } + public string Status { get; set; } = "new"; // new | contacted | customer | churned + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/services/identity/FlatRender.IdentitySvc/Infrastructure/Data/IdentityDbContext.cs b/services/identity/FlatRender.IdentitySvc/Infrastructure/Data/IdentityDbContext.cs index fa1343b..0665ab7 100644 --- a/services/identity/FlatRender.IdentitySvc/Infrastructure/Data/IdentityDbContext.cs +++ b/services/identity/FlatRender.IdentitySvc/Infrastructure/Data/IdentityDbContext.cs @@ -28,6 +28,9 @@ public class IdentityDbContext(DbContextOptions options) : Db public DbSet Discounts => Set(); public DbSet UsedDiscounts => Set(); + // CRM + public DbSet UserCrms => Set(); + // Gamification public DbSet Quests => Set(); public DbSet UserQuestProgresses => Set(); @@ -47,6 +50,12 @@ public class IdentityDbContext(DbContextOptions options) : Db ConfigureUsers(mb); ConfigureBilling(mb); ConfigureGamification(mb); + + mb.Entity(e => + { + e.ToTable("user_crm"); + e.HasKey(x => x.UserId); + }); } private static void ConfigureTenants(ModelBuilder mb) diff --git a/services/identity/FlatRender.IdentitySvc/Models/Admin.cs b/services/identity/FlatRender.IdentitySvc/Models/Admin.cs new file mode 100644 index 0000000..ebe4c60 --- /dev/null +++ b/services/identity/FlatRender.IdentitySvc/Models/Admin.cs @@ -0,0 +1,29 @@ +namespace FlatRender.IdentitySvc.Models; + +// ── CRM analytics (acquisition / conversion funnel) ────────────────────────── + +public record CrmDailyPoint(string Date, int Signups, int Buyers, long RevenueMinor); + +public record CrmAnalyticsResponse( + int TotalSignups, + int Buyers, + int NonBuyers, + double ConversionRate, + long RevenueMinor, + int PayingUsersAllTime, + List Daily +); + +// ── CRM notes / tags per customer ──────────────────────────────────────────── + +public record UserCrmResponse(string[] Tags, string? Note, string Status); + +public record UpsertUserCrmRequest(string[]? Tags, string? Note, string? Status); + +// ── User admin power-actions ───────────────────────────────────────────────── + +public record SetBalanceRequest(long AmountMinor, bool Add); // Add=false → set absolute +public record ResetPasswordRequest(string NewPassword); +public record AddChargeRequest(int Seconds, int RenderCount); // grant render seconds / daily renders +public record GrantPlanDaysRequest(Guid PlanId, int Days); +public record SetFlagRequest(bool Enabled); diff --git a/services/identity/FlatRender.IdentitySvc/Program.cs b/services/identity/FlatRender.IdentitySvc/Program.cs index 566418c..fcb83e7 100644 --- a/services/identity/FlatRender.IdentitySvc/Program.cs +++ b/services/identity/FlatRender.IdentitySvc/Program.cs @@ -78,6 +78,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // ── HTTP clients ─────────────────────────────────────────────────────────── builder.Services.AddHttpClient("zarinpal", client => diff --git a/src/app/[locale]/admin/crm/page.tsx b/src/app/[locale]/admin/crm/page.tsx new file mode 100644 index 0000000..ead909f --- /dev/null +++ b/src/app/[locale]/admin/crm/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { CrmAdmin } from "@/components/admin/CrmAdmin"; + +export default function Page() { + return ; +} diff --git a/src/app/[locale]/admin/layout.tsx b/src/app/[locale]/admin/layout.tsx index afc5625..89cfd7a 100644 --- a/src/app/[locale]/admin/layout.tsx +++ b/src/app/[locale]/admin/layout.tsx @@ -26,6 +26,7 @@ export default async function AdminLayout({ { href: "/admin/ai", label: t("aiContent") }, { href: "/admin/messaging", label: t("messaging") }, { href: "/admin/marketing", label: t("marketing") }, + { href: "/admin/crm", label: t("crm") }, { href: "/admin/users", label: t("users") }, { href: "/admin/plans", label: t("plans") }, { href: "/admin/discounts", label: t("discounts") }, diff --git a/src/components/admin/CrmAdmin.tsx b/src/components/admin/CrmAdmin.tsx new file mode 100644 index 0000000..646d785 --- /dev/null +++ b/src/components/admin/CrmAdmin.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +interface Daily { date: string; signups: number; buyers: number; revenue_minor: number } +interface Analytics { + total_signups: number; buyers: number; non_buyers: number; + conversion_rate: number; revenue_minor: number; paying_users_all_time: number; + daily: Daily[]; +} + +const card = "rounded-xl border border-[#1e2235] bg-[#0f1120] p-5"; +const inp = "rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"; +const btn = "rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50"; + +function iso(d: Date) { return d.toISOString().slice(0, 10); } +function toman(minor: number) { return (minor / 10).toLocaleString("fa-IR"); } // minor=rial → toman + +export function CrmAdmin() { + const today = new Date(); + const monthAgo = new Date(Date.now() - 30 * 864e5); + const [start, setStart] = useState(iso(monthAgo)); + const [end, setEnd] = useState(iso(today)); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + setLoading(true); + const res = await fetch(`/api/admin/resource/admin/crm/analytics?start=${start}&end=${end}`, { cache: "no-store" }); + setData(res.ok ? await res.json() : null); + setLoading(false); + }, [start, end]); + useEffect(() => { load(); }, [load]); + + const maxSignup = Math.max(1, ...(data?.daily ?? []).map((d) => d.signups)); + const maxRev = Math.max(1, ...(data?.daily ?? []).map((d) => d.revenue_minor)); + + const stat = (label: string, value: string, sub?: string) => ( +
+
{label}
+
{value}
+ {sub &&
{sub}
} +
+ ); + + return ( +
+
+
+

CRM — تحلیل جذب و تبدیل

+

ثبت‌نام‌ها، خریداران، نرخ تبدیل و درآمد در بازهٔ زمانی انتخابی.

+
+
+
setStart(e.target.value)} />
+
setEnd(e.target.value)} />
+ +
+
+ + {data && ( + <> +
+ {stat("کل ثبت‌نام", data.total_signups.toLocaleString("fa-IR"))} + {stat("خریداران", data.buyers.toLocaleString("fa-IR"))} + {stat("بدون خرید", data.non_buyers.toLocaleString("fa-IR"))} + {stat("نرخ تبدیل", data.conversion_rate.toLocaleString("fa-IR") + "٪")} + {stat("درآمد بازه", toman(data.revenue_minor) + " تومان", `کل مشتریان: ${data.paying_users_all_time.toLocaleString("fa-IR")}`)} +
+ +
+
ثبت‌نام روزانه
+
+ {data.daily.map((d) => ( +
+
+
+ ))} +
+
+ +
+
درآمد روزانه
+
+ {data.daily.map((d) => ( +
+
+
+ ))} +
+
+ + )} + {!data && !loading &&

داده‌ای دریافت نشد.

} +
+ ); +} diff --git a/src/components/admin/UserActions.tsx b/src/components/admin/UserActions.tsx new file mode 100644 index 0000000..c60ed6b --- /dev/null +++ b/src/components/admin/UserActions.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useState } from "react"; + +const inp = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"; +const lbl = "mb-1 block text-xs font-medium text-gray-400"; +const btn = "rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50"; +const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]"; + +/** Per-user admin power-actions + CRM notes, opened as a modal from the Users table. */ +export function UserActions({ row }: { row: Record; reload?: () => void }) { + const id = String(row.id); + const [open, setOpen] = useState(false); + const [msg, setMsg] = useState(null); + const [busy, setBusy] = useState(false); + + // form state + const [balance, setBalance] = useState(""); const [balanceAdd, setBalanceAdd] = useState(true); + const [pw, setPw] = useState(""); + const [seconds, setSeconds] = useState(""); const [renders, setRenders] = useState(""); + const [planDays, setPlanDays] = useState(""); + const [tags, setTags] = useState(""); const [note, setNote] = useState(""); const [status, setStatus] = useState("new"); + + const call = async (path: string, body: object, ok: string) => { + setBusy(true); setMsg(null); + const res = await fetch(`/api/admin/resource/${path}`, { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), + }); + const d = await res.json().catch(() => null); + setMsg(res.ok ? ok : (d?.error?.message ?? d?.error ?? "خطا")); + setBusy(false); + }; + + const loadCrm = async () => { + const r = await fetch(`/api/admin/resource/users/${id}/crm`, { cache: "no-store" }).then((x) => x.json()).catch(() => null); + if (r) { setTags((r.tags ?? []).join(", ")); setNote(r.note ?? ""); setStatus(r.status ?? "new"); } + }; + const saveCrm = async () => { + setBusy(true); setMsg(null); + const res = await fetch(`/api/admin/resource/users/${id}/crm`, { + method: "PUT", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tags: tags.split(",").map((t) => t.trim()).filter(Boolean), note, status }), + }); + setMsg(res.ok ? "یادداشت ذخیره شد ✓" : "خطا"); setBusy(false); + }; + + return ( + <> + + {open && ( +
setOpen(false)}> +
e.stopPropagation()}> +

مدیریت کاربر: {String(row.email ?? row.full_name ?? id)}

+ {msg &&

{msg}

} + +
+
+ +
+ setBalance(e.target.value)} /> + + +
+
+ +
+ +
+ setSeconds(e.target.value)} /> + setRenders(e.target.value)} /> + +
+
+ +
+ +
+ setPlanDays(e.target.value)} /> + +
+
+ +
+ +
+ setPw(e.target.value)} placeholder="رمز جدید" /> + +
+
+ +
+ دسترسی مدیر (مدراتور) +
+ + +
+
+ +
+ +
+ setTags(e.target.value)} /> + +