forked from baron/baron-sso
린트 적용
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { ArrowLeft, BadgeCheck, Building2, Loader2, Save, Users } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BadgeCheck,
|
||||
Building2,
|
||||
Loader2,
|
||||
Save,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
@@ -36,16 +43,16 @@ type UserSchemaField = {
|
||||
type UserFormValues = UserUpdateRequest & { metadata: Record<string, unknown> };
|
||||
|
||||
// [New] Component for per-tenant profile/schema management
|
||||
function TenantProfileCard({
|
||||
tenant,
|
||||
register,
|
||||
errors,
|
||||
isAdmin
|
||||
}: {
|
||||
tenant: any,
|
||||
register: any,
|
||||
errors: any,
|
||||
isAdmin: boolean
|
||||
function TenantProfileCard({
|
||||
tenant,
|
||||
register,
|
||||
errors,
|
||||
isAdmin,
|
||||
}: {
|
||||
tenant: any;
|
||||
register: any;
|
||||
errors: any;
|
||||
isAdmin: boolean;
|
||||
}) {
|
||||
const { data: detail, isLoading } = useQuery({
|
||||
queryKey: ["tenant", tenant.id],
|
||||
@@ -56,7 +63,12 @@ function TenantProfileCard({
|
||||
? (detail?.config?.userSchema as UserSchemaField[])
|
||||
: [];
|
||||
|
||||
if (isLoading) return <div className="p-4 border rounded-lg animate-pulse bg-muted/20">Loading schema...</div>;
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="p-4 border rounded-lg animate-pulse bg-muted/20">
|
||||
Loading schema...
|
||||
</div>
|
||||
);
|
||||
if (schema.length === 0) return null;
|
||||
|
||||
return (
|
||||
@@ -64,16 +76,23 @@ function TenantProfileCard({
|
||||
<div className="bg-muted/50 px-4 py-2 border-b border-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 size={14} className="text-primary" />
|
||||
<span className="text-xs font-bold uppercase tracking-tight">{tenant.name}</span>
|
||||
<span className="text-xs font-bold uppercase tracking-tight">
|
||||
{tenant.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono opacity-50">{tenant.slug}</span>
|
||||
</div>
|
||||
<div className="p-4 grid gap-4 md:grid-cols-2">
|
||||
{schema.map((field) => (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<Label htmlFor={`metadata.${tenant.id}.${field.key}`} className="text-xs">
|
||||
<Label
|
||||
htmlFor={`metadata.${tenant.id}.${field.key}`}
|
||||
className="text-xs"
|
||||
>
|
||||
{field.label}
|
||||
{field.required && <span className="ml-1 text-destructive">*</span>}
|
||||
{field.required && (
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
)}
|
||||
{field.adminOnly && (
|
||||
<span className="ml-2 text-[9px] bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded uppercase font-bold">
|
||||
Admin Only
|
||||
@@ -83,13 +102,24 @@ function TenantProfileCard({
|
||||
<Input
|
||||
id={`metadata.${tenant.id}.${field.key}`}
|
||||
type={
|
||||
field.type === "number" ? "number" :
|
||||
field.type === "date" ? "date" :
|
||||
field.type === "boolean" ? "checkbox" : "text"
|
||||
field.type === "number"
|
||||
? "number"
|
||||
: field.type === "date"
|
||||
? "date"
|
||||
: field.type === "boolean"
|
||||
? "checkbox"
|
||||
: "text"
|
||||
}
|
||||
className={
|
||||
field.type === "boolean" ? "w-auto h-auto" : "h-8 text-sm"
|
||||
}
|
||||
className={field.type === "boolean" ? "w-auto h-auto" : "h-8 text-sm"}
|
||||
{...register(`metadata.${tenant.id}.${field.key}`, {
|
||||
required: field.required ? t("msg.admin.users.detail.form.field_required", "필수입니다.") : false,
|
||||
required: field.required
|
||||
? t(
|
||||
"msg.admin.users.detail.form.field_required",
|
||||
"필수입니다.",
|
||||
)
|
||||
: false,
|
||||
})}
|
||||
/>
|
||||
{errors.metadata?.[tenant.id]?.[field.key] && (
|
||||
@@ -154,7 +184,8 @@ function UserDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const isAdmin = profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
||||
const isAdmin =
|
||||
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
@@ -224,7 +255,10 @@ function UserDetailPage() {
|
||||
|
||||
// Combined affiliated tenants
|
||||
const userAffiliatedTenants = [...(user.joinedTenants || [])];
|
||||
if (user.tenant && !userAffiliatedTenants.find(t => t.id === user.tenant?.id)) {
|
||||
if (
|
||||
user.tenant &&
|
||||
!userAffiliatedTenants.find((t) => t.id === user.tenant?.id)
|
||||
) {
|
||||
userAffiliatedTenants.push(user.tenant);
|
||||
}
|
||||
|
||||
@@ -281,35 +315,55 @@ function UserDetailPage() {
|
||||
<div className="rounded-lg border border-border bg-muted/30 p-4 space-y-4">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
<Building2 size={16} className="text-primary" />
|
||||
{t("ui.admin.users.detail.tenants_section.title", "소속 및 조직 정보")}
|
||||
{t(
|
||||
"ui.admin.users.detail.tenants_section.title",
|
||||
"소속 및 조직 정보",
|
||||
)}
|
||||
</h3>
|
||||
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] uppercase text-muted-foreground tracking-wider font-bold">
|
||||
{t("ui.admin.users.detail.tenants_section.primary", "대표 소속 테넌트")}
|
||||
{t(
|
||||
"ui.admin.users.detail.tenants_section.primary",
|
||||
"대표 소속 테넌트",
|
||||
)}
|
||||
</Label>
|
||||
|
||||
|
||||
{/* Select box to specify representative tenant from joined ones */}
|
||||
{userAffiliatedTenants.length > 0 ? (
|
||||
<div className="relative">
|
||||
<select
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50"
|
||||
{...register("companyCode")}
|
||||
disabled={profile?.role === "tenant_admin" && userAffiliatedTenants.length <= 1}
|
||||
disabled={
|
||||
profile?.role === "tenant_admin" &&
|
||||
userAffiliatedTenants.length <= 1
|
||||
}
|
||||
>
|
||||
<option value="">{t("ui.admin.users.detail.form.tenant_global", "시스템 전역")}</option>
|
||||
<option value="">
|
||||
{t(
|
||||
"ui.admin.users.detail.form.tenant_global",
|
||||
"시스템 전역",
|
||||
)}
|
||||
</option>
|
||||
{userAffiliatedTenants.map((t) => (
|
||||
<option key={t.id} value={t.slug}>
|
||||
{t.name} ({t.slug})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<BadgeCheck size={14} className="absolute right-8 top-3 text-primary pointer-events-none" />
|
||||
<BadgeCheck
|
||||
size={14}
|
||||
className="absolute right-8 top-3 text-primary pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 p-2 rounded-md bg-background border border-dashed border-border text-muted-foreground italic text-xs">
|
||||
{t("ui.admin.users.detail.form.tenant_global", "시스템 전역 (소속 없음)")}
|
||||
{t(
|
||||
"ui.admin.users.detail.form.tenant_global",
|
||||
"시스템 전역 (소속 없음)",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
@@ -320,15 +374,22 @@ function UserDetailPage() {
|
||||
{userAffiliatedTenants.length > 1 && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] uppercase text-muted-foreground tracking-wider font-bold">
|
||||
{t("ui.admin.users.detail.tenants_section.additional", "전체 소속 목록")}
|
||||
{t(
|
||||
"ui.admin.users.detail.tenants_section.additional",
|
||||
"전체 소속 목록",
|
||||
)}
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||
{userAffiliatedTenants.map(jt => (
|
||||
<Link
|
||||
{userAffiliatedTenants.map((jt) => (
|
||||
<Link
|
||||
key={jt.id}
|
||||
to={`/tenants/${jt.id}`}
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded border text-[11px] transition-colors ${
|
||||
jt.id === tenants.find(t => t.slug === watch("companyCode"))?.id ? "bg-primary/10 border-primary/30 text-primary font-bold" : "bg-background border-border text-muted-foreground hover:border-primary/50"
|
||||
jt.id ===
|
||||
tenants.find((t) => t.slug === watch("companyCode"))
|
||||
?.id
|
||||
? "bg-primary/10 border-primary/30 text-primary font-bold"
|
||||
: "bg-background border-border text-muted-foreground hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<Users size={10} />
|
||||
@@ -453,7 +514,10 @@ function UserDetailPage() {
|
||||
<div className="border-t pt-6 space-y-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-sm font-bold text-muted-foreground uppercase tracking-wider">
|
||||
{t("ui.admin.users.detail.custom_fields.multi_title", "테넌트별 프로필 관리")}
|
||||
{t(
|
||||
"ui.admin.users.detail.custom_fields.multi_title",
|
||||
"테넌트별 프로필 관리",
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
사용자가 소속된 각 테넌트별 맞춤 정보를 관리합니다.
|
||||
|
||||
Reference in New Issue
Block a user