diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index 0e7b67ef..9d9c1240 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -123,6 +123,7 @@ function createEmptyAppointment(): AppointmentDraft { tenantId: "", tenantName: "", tenantSlug: "", + isPrimary: false, isOwner: false, jobTitle: "", position: "", @@ -554,6 +555,15 @@ function UserDetailPage() { ); }; + const setPrimaryAppointment = (targetIndex: number) => { + setAdditionalAppointments((current) => + current.map((appointment, index) => ({ + ...appointment, + isPrimary: index === targetIndex, + })), + ); + }; + const handleUserTypeChange = (value: string) => { const nextType = value as UserType; setUserType(nextType); @@ -645,6 +655,9 @@ function UserDetailPage() { Array.isArray(rawAppointments) ? (rawAppointments as UserAppointment[]).map((appointment) => ({ ...appointment, + isPrimary: + appointment.isPrimary === true || + appointment.tenantId === primaryFromMetadata?.id, draftId: createDraftId(), })) : isUserHanmacFamily @@ -654,6 +667,7 @@ function UserDetailPage() { tenantId: tenant.id, tenantName: tenant.name, tenantSlug: tenant.slug, + isPrimary: tenant.id === fallbackAppointment?.id, isOwner: metadata.primaryTenantIsOwner === true && tenant.id === fallbackAppointment?.id, @@ -667,6 +681,7 @@ function UserDetailPage() { tenantId: fallbackAppointment.id, tenantName: fallbackAppointment.name, tenantSlug: fallbackAppointment.slug, + isPrimary: true, isOwner: metadata.primaryTenantIsOwner === true, jobTitle: user.jobTitle, position: user.position, @@ -781,7 +796,15 @@ function UserDetailPage() { payload.metadata = { ...metadata, additionalAppointments: appointments, + primaryTenantId: primary?.tenantId, + primaryTenantName: primary?.tenantName, + primaryTenantSlug: primary?.tenantSlug, + primaryTenantIsOwner: primary?.isOwner ?? false, }; + payload.tenantSlug = primary?.tenantSlug; + payload.primaryTenantId = primary?.tenantId; + payload.primaryTenantName = primary?.tenantName; + payload.primaryTenantIsOwner = primary?.isOwner ?? false; } mutation.mutate(payload); diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx index 7b064b2e..ad8fa582 100644 --- a/adminfront/src/features/users/UserListPage.tsx +++ b/adminfront/src/features/users/UserListPage.tsx @@ -33,6 +33,13 @@ import { DialogTrigger, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../../components/ui/select"; import { Switch } from "../../components/ui/switch"; import { Table, @@ -547,7 +554,7 @@ function UserListPage() { /> requestSort("id")} >
@@ -556,7 +563,7 @@ function UserListPage() {
requestSort("name_email")} >
@@ -568,7 +575,7 @@ function UserListPage() {
requestSort("status")} >
@@ -577,7 +584,7 @@ function UserListPage() {
requestSort("tenant_dept")} >
@@ -594,7 +601,7 @@ function UserListPage() { visibleColumns[field.key] !== false && ( requestSort(field.key)} >
@@ -605,7 +612,7 @@ function UserListPage() { ), )} requestSort("createdAt")} >
@@ -693,7 +700,7 @@ function UserListPage() { )}
- {" "} +
{ const nowMs = 1_700_000_000_000; - it("returns false when remaining time is above the 5 minute threshold", () => { + it("returns false when remaining time is above the 10 minute threshold", () => { expect( shouldAttemptSlidingSessionRenew({ expiresAtSec: Math.floor( @@ -24,7 +24,7 @@ describe("shouldAttemptSlidingSessionRenew", () => { ).toBe(false); }); - it("returns true when remaining time is within the 5 minute threshold", () => { + it("returns true when remaining time is within the 10 minute threshold", () => { expect( shouldAttemptSlidingSessionRenew({ expiresAtSec: Math.floor( diff --git a/adminfront/src/lib/sessionSliding.ts b/adminfront/src/lib/sessionSliding.ts index 9caff6cd..9fd60fda 100644 --- a/adminfront/src/lib/sessionSliding.ts +++ b/adminfront/src/lib/sessionSliding.ts @@ -1,4 +1,4 @@ -export const SESSION_RENEW_THRESHOLD_MS = 5 * 60 * 1000; +export const SESSION_RENEW_THRESHOLD_MS = 10 * 60 * 1000; export const SESSION_RENEW_THROTTLE_MS = 30 * 1000; type SlidingSessionRenewDecisionParams = { diff --git a/adminfront/src/locales/template.toml b/adminfront/src/locales/template.toml index 79844871..f02882e6 100644 --- a/adminfront/src/locales/template.toml +++ b/adminfront/src/locales/template.toml @@ -131,12 +131,17 @@ empty = "" import_error = "" import_success = "" loading = "" +no_results = "" subtitle = "" [msg.admin.groups.members] +add_modal_desc = "" add_success = "" +all_added = "" count = "" empty = "" +move_modal_desc = "" +move_success = "" remove_confirm = "" remove_success = "" title = "" @@ -234,6 +239,9 @@ subtitle = "" desc = "" empty = "" limit_notice = "" +remove_confirm = "" +remove_error = "" +remove_success = "" [msg.admin.tenants.registry] count = "" @@ -250,6 +258,7 @@ empty = "" subtitle = "" [msg.admin.users] +confirm_remove_org = "" export_error = "" status_error = "" @@ -827,8 +836,11 @@ unit_level_placeholder = "" title = "" [ui.admin.groups.members] +add_modal_title = "" +move_modal_title = "" [ui.admin.groups.members.table] +actions = "" email = "" name = "" remove = "" @@ -899,6 +911,8 @@ export_with_ids = "" export_without_ids = "" import = "" title = "" +view.hierarchy = "" +view.list = "" view_org_chart = "" [ui.admin.tenants.domain_conflict] @@ -985,8 +999,12 @@ search_placeholder = "" select_placeholder = "" [ui.admin.tenants.members] +add_existing = "" +create_new = "" descendants = "" direct = "" +remove = "" +view_profile = "" [msg.admin.apikeys.registry] count = "" @@ -1013,6 +1031,7 @@ total = "" total_label = "" [ui.admin.tenants.members.table] +actions = "" email = "" name = "" role = "" diff --git a/backend/check_aaa2.go b/backend/check_aaa2.go index 29a5df6c..55fe2c0c 100644 --- a/backend/check_aaa2.go +++ b/backend/check_aaa2.go @@ -2,9 +2,10 @@ package main import ( "fmt" + "log" + "gorm.io/driver/postgres" "gorm.io/gorm" - "log" ) type User struct { diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 8e841a6d..d921c33e 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -1620,54 +1620,55 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error { } } } else { - // Normal update (Move): replace primary company code and remove the old one from existingCodes - currentPrimary := extractTraitString(traits, "companyCode") - if currentPrimary != "" && currentPrimary != code { - // Remove old primary from existingCodes - var newCodes []string - for _, existing := range existingCodes { - if existing != currentPrimary { - newCodes = append(newCodes, existing) - } - } - existingCodes = newCodes + // Normal update (Move): replace primary company code and remove the old one from existingCodes + currentPrimary := extractTraitString(traits, "companyCode") + if currentPrimary != "" && currentPrimary != code { + // Remove old primary from existingCodes + var newCodes []string + for _, existing := range existingCodes { + if existing != currentPrimary { + newCodes = append(newCodes, existing) + } + } + existingCodes = newCodes - // [Keto Sync] Remove membership for the old tenant - if h.TenantService != nil && h.KetoOutboxRepo != nil { - go func(removedSlug string) { - bgCtx := context.Background() - if t, err := h.TenantService.GetTenantBySlug(bgCtx, removedSlug); err == nil && t != nil { - _ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{ - Namespace: "Tenant", - Object: t.ID, - Relation: "members", - Subject: "User:" + userID, - Action: domain.KetoOutboxActionDelete, - }) - } - }(currentPrimary) - } - } + // [Keto Sync] Remove membership for the old tenant + if h.TenantService != nil && h.KetoOutboxRepo != nil { + go func(removedSlug string) { + bgCtx := context.Background() + if t, err := h.TenantService.GetTenantBySlug(bgCtx, removedSlug); err == nil && t != nil { + _ = h.KetoOutboxRepo.Create(bgCtx, &domain.KetoOutbox{ + Namespace: "Tenant", + Object: t.ID, + Relation: "members", + Subject: "User:" + userID, + Action: domain.KetoOutboxActionDelete, + }) + } + }(currentPrimary) + } + } - traits["companyCode"] = code - // Resolve TenantID for Kratos Trait - if h.TenantService != nil && code != "" { - if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil { - traits["tenant_id"] = tenant.ID - } - } + traits["companyCode"] = code + // Resolve TenantID for Kratos Trait + if h.TenantService != nil && code != "" { + if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil { + traits["tenant_id"] = tenant.ID + } + } - found := false - for _, existing := range existingCodes { - if existing == code { - found = true - break - } - } - if !found && code != "" { - existingCodes = append(existingCodes, code) - } - } } + found := false + for _, existing := range existingCodes { + if existing == code { + found = true + break + } + } + if !found && code != "" { + existingCodes = append(existingCodes, code) + } + } + } // Deduplicate and save back companyCodes var codesToSave []string diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 2d1fd352..5657fb42 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -1,14 +1,6 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { - BookOpenText, - Filter, - Plus, - Search, - ServerCog, - ShieldHalf, - X, -} from "lucide-react"; +import { BookOpenText, Filter, Plus, Search, X } from "lucide-react"; import { useEffect, useState } from "react"; import { useAuth } from "react-oidc-context"; import { Link, useNavigate } from "react-router-dom"; @@ -50,6 +42,7 @@ import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; import { cn } from "../../lib/utils"; import { fetchMe } from "../auth/authApi"; +import { ClientLogo } from "./components/ClientLogo"; function ClientsPage() { const navigate = useNavigate(); @@ -498,13 +491,7 @@ function ClientsPage() { to={`/clients/${client.id}`} className="flex items-center gap-3 transition-colors hover:text-primary" > -
- {client.type === "private" ? ( - - ) : ( - - )} -
+

{client.name || diff --git a/devfront/src/features/clients/components/ClientLogo.tsx b/devfront/src/features/clients/components/ClientLogo.tsx new file mode 100644 index 00000000..397a1d99 --- /dev/null +++ b/devfront/src/features/clients/components/ClientLogo.tsx @@ -0,0 +1,59 @@ +import { ServerCog, ShieldHalf } from "lucide-react"; +import { useMemo, useState } from "react"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "../../../components/ui/avatar"; +import type { ClientSummary, ClientType } from "../../../lib/devApi"; +import { t } from "../../../lib/i18n"; + +type ClientLogoProps = { + client: Pick; +}; + +function readLogoUrl(metadata?: Record): string | undefined { + const logoUrl = metadata?.logo_url; + if (typeof logoUrl !== "string") { + return undefined; + } + + const trimmedLogoUrl = logoUrl.trim(); + return trimmedLogoUrl.length > 0 ? trimmedLogoUrl : undefined; +} + +function TypeFallbackIcon({ type }: { type: ClientType }) { + if (type === "private") { + return