Commit 9a412925 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: redesign UI to premium shadcn/ui with Canifa warm red/orange accents and...

feat: redesign UI to premium shadcn/ui with Canifa warm red/orange accents and translate company terms to Vietnamese departments (Phòng ban)
parent 5a7791d1
...@@ -41,34 +41,34 @@ export function CompanySettingsSidebar() { ...@@ -41,34 +41,34 @@ export function CompanySettingsSidebar() {
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground" className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
> >
<ChevronLeft className="h-3.5 w-3.5 shrink-0" /> <ChevronLeft className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{selectedCompany?.name ?? "Company"}</span> <span className="truncate">{selectedCompany?.name ?? "Phòng ban"}</span>
</Link> </Link>
<div className="flex items-center gap-2 px-2 py-1"> <div className="flex items-center gap-2 px-2 py-1">
<Settings className="h-4 w-4 text-muted-foreground shrink-0" /> <Settings className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="flex-1 truncate text-sm font-bold text-foreground"> <span className="flex-1 truncate text-sm font-bold text-foreground">
Company Settings Thiết lập phòng ban
</span> </span>
</div> </div>
</div> </div>
<nav className="flex-1 min-h-0 overflow-y-auto scrollbar-auto-hide px-3 py-2"> <nav className="flex-1 min-h-0 overflow-y-auto scrollbar-auto-hide px-3 py-2">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<SidebarNavItem to="/company/settings" label="General" icon={SlidersHorizontal} end /> <SidebarNavItem to="/company/settings" label="Chung" icon={SlidersHorizontal} end />
<SidebarNavItem <SidebarNavItem
to="/company/settings/environments" to="/company/settings/environments"
label="Environments" label="Môi trường"
icon={MonitorCog} icon={MonitorCog}
end end
/> />
<SidebarNavItem <SidebarNavItem
to="/company/settings/access" to="/company/settings/access"
label="Access" label="Truy cập"
icon={Shield} icon={Shield}
badge={badges?.joinRequests ?? 0} badge={badges?.joinRequests ?? 0}
end end
/> />
<SidebarNavItem to="/company/settings/invites" label="Invites" icon={MailPlus} end /> <SidebarNavItem to="/company/settings/invites" label="Lời mời" icon={MailPlus} end />
<SidebarNavItem to="/company/settings/secrets" label="Secrets" icon={KeyRound} end /> <SidebarNavItem to="/company/settings/secrets" label="Bí mật" icon={KeyRound} end />
</div> </div>
</nav> </nav>
</aside> </aside>
......
...@@ -79,12 +79,12 @@ export function Sidebar() { ...@@ -79,12 +79,12 @@ export function Sidebar() {
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors" className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
> >
<SquarePen className="h-4 w-4 shrink-0" /> <SquarePen className="h-4 w-4 shrink-0" />
<span className="truncate">New Issue</span> <span className="truncate">Vấn đề mới</span>
</button> </button>
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} liveCount={liveRunCount} /> <SidebarNavItem to="/dashboard" label="Bảng điều khiển" icon={LayoutDashboard} liveCount={liveRunCount} />
<SidebarNavItem <SidebarNavItem
to="/inbox" to="/inbox"
label="Inbox" label="Hộp thư"
icon={Inbox} icon={Inbox}
badge={inboxBadge.inbox} badge={inboxBadge.inbox}
badgeTone={inboxBadge.failedRuns > 0 ? "danger" : "default"} badgeTone={inboxBadge.failedRuns > 0 ? "danger" : "default"}
...@@ -99,18 +99,18 @@ export function Sidebar() { ...@@ -99,18 +99,18 @@ export function Sidebar() {
/> />
</div> </div>
<SidebarSection label="Work"> <SidebarSection label="Công việc">
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} /> <SidebarNavItem to="/issues" label="Vấn đề" icon={CircleDot} />
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} /> <SidebarNavItem to="/routines" label="Quy trình" icon={Repeat} />
<PluginLauncherOutlet <PluginLauncherOutlet
placementZones={["sidebar"]} placementZones={["sidebar"]}
context={pluginContext} context={pluginContext}
className="flex flex-col gap-0.5" className="flex flex-col gap-0.5"
itemClassName="text-[13px] font-medium" itemClassName="text-[13px] font-medium"
/> />
<SidebarNavItem to="/goals" label="Goals" icon={Target} /> <SidebarNavItem to="/goals" label="Mục tiêu" icon={Target} />
{showWorkspacesLink ? ( {showWorkspacesLink ? (
<SidebarNavItem to="/workspaces" label="Workspaces" icon={GitBranch} /> <SidebarNavItem to="/workspaces" label="Không gian làm việc" icon={GitBranch} />
) : null} ) : null}
</SidebarSection> </SidebarSection>
...@@ -118,12 +118,12 @@ export function Sidebar() { ...@@ -118,12 +118,12 @@ export function Sidebar() {
<SidebarAgents /> <SidebarAgents />
<SidebarSection label="Company"> <SidebarSection label="Phòng ban">
<SidebarNavItem to="/org" label="Org" icon={Network} /> <SidebarNavItem to="/org" label="Sơ đồ" icon={Network} />
<SidebarNavItem to="/skills" label="Skills" icon={Boxes} /> <SidebarNavItem to="/skills" label="Kỹ năng" icon={Boxes} />
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} /> <SidebarNavItem to="/costs" label="Chi phí" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Activity" icon={History} /> <SidebarNavItem to="/activity" label="Hoạt động" icon={History} />
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} /> <SidebarNavItem to="/company/settings" label="Thiết lập" icon={Settings} />
</SidebarSection> </SidebarSection>
<PluginSlotOutlet <PluginSlotOutlet
......
...@@ -225,12 +225,12 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -225,12 +225,12 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
<Button <Button
variant="ghost" variant="ghost"
className="h-9 flex-1 justify-start gap-2 px-2 text-left" className="h-9 flex-1 justify-start gap-2 px-2 text-left"
aria-label={selectedCompany ? `Open ${selectedCompany.name} workspace switcher` : "Open workspace switcher"} aria-label={selectedCompany ? `Mở menu chuyển phòng ban của ${selectedCompany.name}` : "Mở menu chuyển phòng ban"}
> >
<span className="flex min-w-0 flex-1 items-center gap-2"> <span className="flex min-w-0 flex-1 items-center gap-2">
{selectedCompany ? <WorkspaceIcon company={selectedCompany} /> : null} {selectedCompany ? <WorkspaceIcon company={selectedCompany} /> : null}
<span className="truncate text-sm font-bold text-foreground"> <span className="truncate text-sm font-bold text-foreground">
{selectedCompany?.name ?? "Select workspace"} {selectedCompany?.name ?? "Chọn phòng ban"}
</span> </span>
</span> </span>
<ChevronsUpDown className="size-3.5 shrink-0 text-muted-foreground" /> <ChevronsUpDown className="size-3.5 shrink-0 text-muted-foreground" />
...@@ -239,7 +239,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -239,7 +239,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
<DropdownMenuContent align="start" sideOffset={8} className="w-64 p-1"> <DropdownMenuContent align="start" sideOffset={8} className="w-64 p-1">
<div className="flex items-center justify-between gap-2 px-2 py-1.5"> <div className="flex items-center justify-between gap-2 px-2 py-1.5">
<DropdownMenuLabel className="p-0 text-[11px] font-semibold uppercase text-muted-foreground"> <DropdownMenuLabel className="p-0 text-[11px] font-semibold uppercase text-muted-foreground">
Switch workspace Chuyển phòng ban
</DropdownMenuLabel> </DropdownMenuLabel>
<button <button
type="button" type="button"
...@@ -250,7 +250,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -250,7 +250,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
}} }}
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" className="rounded px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
{isEditingOrder ? "Done" : "Edit"} {isEditingOrder ? "Xong" : "Sắp xếp"}
</button> </button>
</div> </div>
<div className="max-h-96 overflow-y-auto"> <div className="max-h-96 overflow-y-auto">
...@@ -275,7 +275,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -275,7 +275,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
</SortableContext> </SortableContext>
</DndContext> </DndContext>
{orderedCompanies.length === 0 ? ( {orderedCompanies.length === 0 ? (
<DropdownMenuItem disabled>No workspaces</DropdownMenuItem> <DropdownMenuItem disabled>Không có phòng ban</DropdownMenuItem>
) : null} ) : null}
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
...@@ -285,7 +285,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -285,7 +285,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
disabled={isEditingOrder} disabled={isEditingOrder}
> >
<Plus className="size-4" /> <Plus className="size-4" />
<span>Add company...</span> <span>Thêm phòng ban...</span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem asChild disabled={isEditingOrder}> <DropdownMenuItem asChild disabled={isEditingOrder}>
...@@ -301,7 +301,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -301,7 +301,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
> >
<UserPlus className="size-4" /> <UserPlus className="size-4" />
<span className="truncate"> <span className="truncate">
{selectedCompany ? `Invite people to ${selectedCompany.name}` : "Invite people"} {selectedCompany ? `Mời nhân sự vào ${selectedCompany.name}` : "Mời nhân sự"}
</span> </span>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
...@@ -317,7 +317,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -317,7 +317,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
}} }}
> >
<Settings className="size-4" /> <Settings className="size-4" />
<span>Company settings</span> <span>Thiết lập phòng ban</span>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
{session?.session ? ( {session?.session ? (
...@@ -329,7 +329,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb ...@@ -329,7 +329,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
disabled={isEditingOrder || signOutMutation.isPending} disabled={isEditingOrder || signOutMutation.isPending}
> >
<LogOut className="size-4" /> <LogOut className="size-4" />
<span>{signOutMutation.isPending ? "Signing out..." : "Sign out"}</span> <span>{signOutMutation.isPending ? "Đang đăng xuất..." : "Đăng xuất"}</span>
</DropdownMenuItem> </DropdownMenuItem>
</> </>
) : null} ) : null}
......
{ {
"app": { "app": {
"noCompanies": { "noCompanies": {
"title": "Create your first company", "title": "Create your first department",
"description": "Get started by creating a company.", "description": "Get started by creating a department.",
"newCompany": "New Company" "newCompany": "New Department"
} }
} }
} }
{ {
"app": { "app": {
"noCompanies": { "noCompanies": {
"title": "Tạo công ty đầu tiên của bạn", "title": "Tạo phòng ban đầu tiên của bạn",
"description": "Bắt đầu bằng cách tạo một công ty.", "description": "Bắt đầu bằng cách tạo một phòng ban.",
"newCompany": "Công ty mới" "newCompany": "Phòng ban mới"
} }
} }
} }
...@@ -38,45 +38,45 @@ ...@@ -38,45 +38,45 @@
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--radius-sm: 0.375rem; --radius-sm: 0.375rem;
--radius-md: 0.5rem; --radius-md: 0.5rem;
--radius-lg: 0px; --radius-lg: 0.5rem;
--radius-xl: 0px; --radius-xl: 0.75rem;
} }
:root { :root {
color-scheme: light; color-scheme: light;
--radius: 0; --radius: 0.5rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0); --primary: oklch(0.53 0.22 25.4);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0.01 25.4);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0.01 25.4);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.53 0.22 25.4);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.53 0.22 25.4);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429); --chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08); --chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0); --sidebar-primary: oklch(0.53 0.22 25.4);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0); --sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.53 0.22 25.4);
--chip-match-title-bg: oklch(0.97 0.02 265); --chip-match-title-bg: oklch(0.97 0.02 265);
--chip-match-title-fg: oklch(0.4 0.13 265); --chip-match-title-fg: oklch(0.4 0.13 265);
--chip-match-title-border: oklch(0.85 0.05 265); --chip-match-title-border: oklch(0.85 0.05 265);
...@@ -92,38 +92,38 @@ ...@@ -92,38 +92,38 @@
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: oklch(0.13 0.01 25.4);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0); --card: oklch(0.17 0.01 25.4);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0); --popover: oklch(0.17 0.01 25.4);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0); --primary: oklch(0.58 0.23 25.4);
--primary-foreground: oklch(0.205 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.269 0 0); --secondary: oklch(0.22 0.01 25.4);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0); --muted: oklch(0.22 0.01 25.4);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0); --accent: oklch(0.22 0.02 25.4);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.637 0.237 25.331); --destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.985 0 0); --destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.269 0 0); --border: oklch(0.22 0.01 25.4);
--input: oklch(0.269 0 0); --input: oklch(0.22 0.01 25.4);
--ring: oklch(0.439 0 0); --ring: oklch(0.58 0.23 25.4);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.58 0.23 25.4);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9); --chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439); --chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.145 0 0); --sidebar: oklch(0.13 0.01 25.4);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: oklch(0.58 0.23 25.4);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.22 0.02 25.4);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0); --sidebar-border: oklch(0.22 0.01 25.4);
--sidebar-ring: oklch(0.439 0 0); --sidebar-ring: oklch(0.58 0.23 25.4);
--chip-match-title-bg: oklch(0.27 0.04 265 / 0.5); --chip-match-title-bg: oklch(0.27 0.04 265 / 0.5);
--chip-match-title-fg: oklch(0.78 0.10 265); --chip-match-title-fg: oklch(0.78 0.10 265);
--chip-match-title-border: oklch(0.4 0.06 265); --chip-match-title-border: oklch(0.4 0.06 265);
......
...@@ -49,7 +49,7 @@ export function Activity() { ...@@ -49,7 +49,7 @@ export function Activity() {
const [filter, setFilter] = useState("all"); const [filter, setFilter] = useState("all");
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Activity" }]); setBreadcrumbs([{ label: "Hoạt động" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
...@@ -101,7 +101,7 @@ export function Activity() { ...@@ -101,7 +101,7 @@ export function Activity() {
}, [data]); }, [data]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={History} message="Select a company to view activity." />; return <EmptyState icon={History} message="Chọn phòng ban để xem hoạt động." />;
} }
if (isLoading) { if (isLoading) {
...@@ -122,10 +122,10 @@ export function Activity() { ...@@ -122,10 +122,10 @@ export function Activity() {
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<Select value={filter} onValueChange={setFilter}> <Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-[140px] h-8 text-xs"> <SelectTrigger className="w-[140px] h-8 text-xs">
<SelectValue placeholder="Filter by type" /> <SelectValue placeholder="Lọc theo loại" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All types</SelectItem> <SelectItem value="all">Tất cả</SelectItem>
{entityTypes.map((type) => ( {entityTypes.map((type) => (
<SelectItem key={type} value={type}> <SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)} {type.charAt(0).toUpperCase() + type.slice(1)}
...@@ -138,7 +138,7 @@ export function Activity() { ...@@ -138,7 +138,7 @@ export function Activity() {
{error && <p className="text-sm text-destructive">{error.message}</p>} {error && <p className="text-sm text-destructive">{error.message}</p>}
{filtered && filtered.length === 0 && ( {filtered && filtered.length === 0 && (
<EmptyState icon={History} message="No activity yet." /> <EmptyState icon={History} message="Chưa có hoạt động nào." />
)} )}
{filtered && filtered.length > 0 && ( {filtered && filtered.length > 0 && (
......
...@@ -266,7 +266,7 @@ export function AdapterManager() { ...@@ -266,7 +266,7 @@ export function AdapterManager() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings", href: "/instance/settings/general" }, { label: "Settings", href: "/instance/settings/general" },
{ label: "Adapters" }, { label: "Adapters" },
]); ]);
......
...@@ -116,11 +116,11 @@ export function Agents() { ...@@ -116,11 +116,11 @@ export function Agents() {
}, [agents]); }, [agents]);
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Agents" }]); setBreadcrumbs([{ label: "Tác nhân" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Bot} message="Select a company to view agents." />; return <EmptyState icon={Bot} message="Chọn phòng ban để xem danh sách tác nhân." />;
} }
if (isLoading) { if (isLoading) {
...@@ -136,10 +136,10 @@ export function Agents() { ...@@ -136,10 +136,10 @@ export function Agents() {
<Tabs value={tab} onValueChange={(v) => navigate(`/agents/${v}`)}> <Tabs value={tab} onValueChange={(v) => navigate(`/agents/${v}`)}>
<PageTabBar <PageTabBar
items={[ items={[
{ value: "all", label: "All" }, { value: "all", label: "Tất cả" },
{ value: "active", label: "Active" }, { value: "active", label: "Đang hoạt động" },
{ value: "paused", label: "Paused" }, { value: "paused", label: "Tạm dừng" },
{ value: "error", label: "Error" }, { value: "error", label: "Lỗi" },
]} ]}
value={tab} value={tab}
onValueChange={(v) => navigate(`/agents/${v}`)} onValueChange={(v) => navigate(`/agents/${v}`)}
...@@ -171,7 +171,7 @@ export function Agents() { ...@@ -171,7 +171,7 @@ export function Agents() {
)}> )}>
{showTerminated && <span className="text-background text-[10px] leading-none">&#10003;</span>} {showTerminated && <span className="text-background text-[10px] leading-none">&#10003;</span>}
</span> </span>
Show terminated Hiển thị đã chấm dứt
</button> </button>
</div> </div>
)} )}
...@@ -201,13 +201,13 @@ export function Agents() { ...@@ -201,13 +201,13 @@ export function Agents() {
)} )}
<Button size="sm" variant="outline" onClick={openNewAgent}> <Button size="sm" variant="outline" onClick={openNewAgent}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> <Plus className="h-3.5 w-3.5 mr-1.5" />
New Agent Tác nhân mới
</Button> </Button>
</div> </div>
</div> </div>
{filtered.length > 0 && ( {filtered.length > 0 && (
<p className="text-xs text-muted-foreground">{filtered.length} agent{filtered.length !== 1 ? "s" : ""}</p> <p className="text-xs text-muted-foreground">{filtered.length} tác nhân</p>
)} )}
{error && <p className="text-sm text-destructive">{error.message}</p>} {error && <p className="text-sm text-destructive">{error.message}</p>}
...@@ -215,8 +215,8 @@ export function Agents() { ...@@ -215,8 +215,8 @@ export function Agents() {
{agents && agents.length === 0 && ( {agents && agents.length === 0 && (
<EmptyState <EmptyState
icon={Bot} icon={Bot}
message="Create your first agent to get started." message="Hãy tạo tác nhân đầu tiên của bạn để bắt đầu."
action="New Agent" action="Tác nhân mới"
onAction={openNewAgent} onAction={openNewAgent}
/> />
)} )}
...@@ -286,7 +286,7 @@ export function Agents() { ...@@ -286,7 +286,7 @@ export function Agents() {
{effectiveView === "list" && agents && agents.length > 0 && filtered.length === 0 && ( {effectiveView === "list" && agents && agents.length > 0 && filtered.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8"> <p className="text-sm text-muted-foreground text-center py-8">
No agents match the selected filter. Không có tác nhân nào khớp với bộ lọc đã chọn.
</p> </p>
)} )}
...@@ -301,13 +301,13 @@ export function Agents() { ...@@ -301,13 +301,13 @@ export function Agents() {
{effectiveView === "org" && orgTree && orgTree.length > 0 && filteredOrg.length === 0 && ( {effectiveView === "org" && orgTree && orgTree.length > 0 && filteredOrg.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8"> <p className="text-sm text-muted-foreground text-center py-8">
No agents match the selected filter. Không có tác nhân nào khớp với bộ lọc đã chọn.
</p> </p>
)} )}
{effectiveView === "org" && orgTree && orgTree.length === 0 && ( {effectiveView === "org" && orgTree && orgTree.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8"> <p className="text-sm text-muted-foreground text-center py-8">
No organizational hierarchy defined. Chưa định nghĩa sơ đồ tổ chức.
</p> </p>
)} )}
</div> </div>
......
...@@ -26,7 +26,7 @@ export function Approvals() { ...@@ -26,7 +26,7 @@ export function Approvals() {
const [actionError, setActionError] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Approvals" }]); setBreadcrumbs([{ label: "Phê duyệt" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
...@@ -75,7 +75,7 @@ export function Approvals() { ...@@ -75,7 +75,7 @@ export function Approvals() {
).length; ).length;
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <p className="text-sm text-muted-foreground">Select a company first.</p>; return <p className="text-sm text-muted-foreground">Vui lòng chọn phòng ban trước.</p>;
} }
if (isLoading) { if (isLoading) {
...@@ -87,7 +87,7 @@ export function Approvals() { ...@@ -87,7 +87,7 @@ export function Approvals() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Tabs value={statusFilter} onValueChange={(v) => navigate(`/approvals/${v}`)}> <Tabs value={statusFilter} onValueChange={(v) => navigate(`/approvals/${v}`)}>
<PageTabBar items={[ <PageTabBar items={[
{ value: "pending", label: <>Pending{pendingCount > 0 && ( { value: "pending", label: <>Đang chờ{pendingCount > 0 && (
<span className={cn( <span className={cn(
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium", "ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
"bg-yellow-500/20 text-yellow-500" "bg-yellow-500/20 text-yellow-500"
...@@ -95,7 +95,7 @@ export function Approvals() { ...@@ -95,7 +95,7 @@ export function Approvals() {
{pendingCount} {pendingCount}
</span> </span>
)}</> }, )}</> },
{ value: "all", label: "All" }, { value: "all", label: "Tất cả" },
]} /> ]} />
</Tabs> </Tabs>
</div> </div>
...@@ -107,7 +107,7 @@ export function Approvals() { ...@@ -107,7 +107,7 @@ export function Approvals() {
<div className="flex flex-col items-center justify-center py-16 text-center"> <div className="flex flex-col items-center justify-center py-16 text-center">
<ShieldCheck className="h-8 w-8 text-muted-foreground/30 mb-3" /> <ShieldCheck className="h-8 w-8 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{statusFilter === "pending" ? "No pending approvals." : "No approvals yet."} {statusFilter === "pending" ? "Không có phê duyệt nào đang chờ." : "Chưa có phê duyệt nào."}
</p> </p>
</div> </div>
)} )}
......
...@@ -69,7 +69,7 @@ export function Companies() { ...@@ -69,7 +69,7 @@ export function Companies() {
}); });
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Companies" }]); setBreadcrumbs([{ label: "Phòng ban" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
function startEdit(companyId: string, currentName: string) { function startEdit(companyId: string, currentName: string) {
...@@ -92,12 +92,12 @@ export function Companies() { ...@@ -92,12 +92,12 @@ export function Companies() {
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<Button size="sm" onClick={() => openOnboarding()}> <Button size="sm" onClick={() => openOnboarding()}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> <Plus className="h-3.5 w-3.5 mr-1.5" />
New Company Thêm phòng ban
</Button> </Button>
</div> </div>
<div className="h-6"> <div className="h-6">
{loading && <p className="text-sm text-muted-foreground">Loading companies...</p>} {loading && <p className="text-sm text-muted-foreground">Đang tải danh sách phòng ban...</p>}
{error && <p className="text-sm text-destructive">{error.message}</p>} {error && <p className="text-sm text-destructive">{error.message}</p>}
</div> </div>
...@@ -215,7 +215,7 @@ export function Companies() { ...@@ -215,7 +215,7 @@ export function Companies() {
onClick={() => startEdit(company.id, company.name)} onClick={() => startEdit(company.id, company.name)}
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
Rename Đổi tên
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
...@@ -223,7 +223,7 @@ export function Companies() { ...@@ -223,7 +223,7 @@ export function Companies() {
onClick={() => setConfirmDeleteId(company.id)} onClick={() => setConfirmDeleteId(company.id)}
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" />
Delete Company Xóa phòng ban
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
...@@ -235,13 +235,13 @@ export function Companies() { ...@@ -235,13 +235,13 @@ export function Companies() {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Users className="h-3.5 w-3.5" /> <Users className="h-3.5 w-3.5" />
<span> <span>
{agentCount} {agentCount === 1 ? "agent" : "agents"} {agentCount} {agentCount === 1 ? "tác vụ/agent" : "tác vụ/agent"}
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<CircleDot className="h-3.5 w-3.5" /> <CircleDot className="h-3.5 w-3.5" />
<span> <span>
{issueCount} {issueCount === 1 ? "issue" : "issues"} {issueCount} vấn đề
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5 tabular-nums"> <div className="flex items-center gap-1.5 tabular-nums">
...@@ -250,12 +250,12 @@ export function Companies() { ...@@ -250,12 +250,12 @@ export function Companies() {
{formatCents(company.spentMonthlyCents)} {formatCents(company.spentMonthlyCents)}
{company.budgetMonthlyCents > 0 {company.budgetMonthlyCents > 0
? <> / {formatCents(company.budgetMonthlyCents)} <span className="text-xs">({budgetPct}%)</span></> ? <> / {formatCents(company.budgetMonthlyCents)} <span className="text-xs">({budgetPct}%)</span></>
: <span className="text-xs ml-1">Unlimited budget</span>} : <span className="text-xs ml-1">Không giới hạn ngân sách</span>}
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5 ml-auto"> <div className="flex items-center gap-1.5 ml-auto">
<Calendar className="h-3.5 w-3.5" /> <Calendar className="h-3.5 w-3.5" />
<span>Created {relativeTime(company.createdAt)}</span> <span>Đã tạo {relativeTime(company.createdAt)}</span>
</div> </div>
</div> </div>
...@@ -266,7 +266,7 @@ export function Companies() { ...@@ -266,7 +266,7 @@ export function Companies() {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<p className="text-sm text-destructive font-medium"> <p className="text-sm text-destructive font-medium">
Delete this company and all its data? This cannot be undone. Xóa phòng ban này và toàn bộ dữ liệu? Thao tác này không thể hoàn tác.
</p> </p>
<div className="flex items-center gap-2 ml-4 shrink-0"> <div className="flex items-center gap-2 ml-4 shrink-0">
<Button <Button
...@@ -275,7 +275,7 @@ export function Companies() { ...@@ -275,7 +275,7 @@ export function Companies() {
onClick={() => setConfirmDeleteId(null)} onClick={() => setConfirmDeleteId(null)}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
> >
Cancel Hủy bỏ
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
...@@ -283,7 +283,7 @@ export function Companies() { ...@@ -283,7 +283,7 @@ export function Companies() {
onClick={() => deleteMutation.mutate(company.id)} onClick={() => deleteMutation.mutate(company.id)}
disabled={deleteMutation.isPending} disabled={deleteMutation.isPending}
> >
{deleteMutation.isPending ? "Deleting…" : "Delete"} {deleteMutation.isPending ? "Đang xóa…" : "Xóa"}
</Button> </Button>
</div> </div>
</div> </div>
......
...@@ -71,9 +71,9 @@ export function CompanyAccess() { ...@@ -71,9 +71,9 @@ export function CompanyAccess() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" }, { label: "Thiết lập", href: "/company/settings" },
{ label: "Access" }, { label: "Quyền truy cập" },
]); ]);
}, [selectedCompany?.name, setBreadcrumbs]); }, [selectedCompany?.name, setBreadcrumbs]);
...@@ -232,20 +232,20 @@ export function CompanyAccess() { ...@@ -232,20 +232,20 @@ export function CompanyAccess() {
}, [removingMember]); }, [removingMember]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to manage access.</div>; return <div className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý quyền truy cập.</div>;
} }
if (membersQuery.isLoading) { if (membersQuery.isLoading) {
return <div className="text-sm text-muted-foreground">Loading company access…</div>; return <div className="text-sm text-muted-foreground">Đang tải thông tin truy cập phòng ban…</div>;
} }
if (membersQuery.error) { if (membersQuery.error) {
const message = const message =
membersQuery.error instanceof ApiError && membersQuery.error.status === 403 membersQuery.error instanceof ApiError && membersQuery.error.status === 403
? "You do not have permission to manage company members." ? "Bạn không có quyền quản lý thành viên phòng ban."
: membersQuery.error instanceof Error : membersQuery.error instanceof Error
? membersQuery.error.message ? membersQuery.error.message
: "Failed to load company members."; : "Không thể tải danh sách thành viên phòng ban.";
return <div className="text-sm text-destructive">{message}</div>; return <div className="text-sm text-destructive">{message}</div>;
} }
...@@ -271,16 +271,16 @@ export function CompanyAccess() { ...@@ -271,16 +271,16 @@ export function CompanyAccess() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShieldCheck className="h-5 w-5 text-muted-foreground" /> <ShieldCheck className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Access</h1> <h1 className="text-lg font-semibold">Quyền truy cập phòng ban</h1>
</div> </div>
<p className="max-w-3xl text-sm text-muted-foreground"> <p className="max-w-3xl text-sm text-muted-foreground">
Manage company user memberships, membership status, and explicit permission grants for {selectedCompany?.name}. Quản lý thành viên phòng ban, trạng thái thành viên và phân quyền chi tiết cho phòng ban {selectedCompany?.name}.
</p> </p>
</div> </div>
{access && !access.currentUserRole && ( {access && !access.currentUserRole && (
<div className="rounded-xl border border-amber-500/40 px-4 py-3 text-sm text-amber-200"> <div className="rounded-xl border border-amber-500/40 px-4 py-3 text-sm text-amber-200">
This account can manage access here through instance-admin privileges, but it does not currently hold an active company membership. Tài khoản này có quyền quản lý thông qua đặc quyền quản trị viên hệ thống, nhưng hiện tại không có tư cách thành viên chính thức trong phòng ban này.
</div> </div>
)} )}
...@@ -288,10 +288,10 @@ export function CompanyAccess() { ...@@ -288,10 +288,10 @@ export function CompanyAccess() {
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="h-4 w-4 text-muted-foreground" /> <Users className="h-4 w-4 text-muted-foreground" />
<h2 className="text-base font-semibold">Humans</h2> <h2 className="text-base font-semibold">Thành viên (Nhân sự)</h2>
</div> </div>
<p className="max-w-3xl text-sm text-muted-foreground"> <p className="max-w-3xl text-sm text-muted-foreground">
Manage human company memberships, status, and grants here. Quản lý thành viên phòng ban, vai trò và phân quyền tại đây.
</p> </p>
</div> </div>
...@@ -299,12 +299,12 @@ export function CompanyAccess() { ...@@ -299,12 +299,12 @@ export function CompanyAccess() {
<div className="space-y-3 rounded-xl border border-border px-4 py-4"> <div className="space-y-3 rounded-xl border border-border px-4 py-4">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div> <div>
<h3 className="text-sm font-semibold">Pending human joins</h3> <h3 className="text-sm font-semibold">Yêu cầu gia nhập phòng ban</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Review human join requests before they become active company members. Phê duyệt các yêu cầu gia nhập trước khi họ trở thành thành viên chính thức của phòng ban.
</p> </p>
</div> </div>
<Badge variant="outline">{pendingHumanJoinRequests.length} pending</Badge> <Badge variant="outline">{pendingHumanJoinRequests.length} đang chờ</Badge>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{pendingHumanJoinRequests.map((request) => ( {pendingHumanJoinRequests.map((request) => (
...@@ -314,22 +314,22 @@ export function CompanyAccess() { ...@@ -314,22 +314,22 @@ export function CompanyAccess() {
request.requesterUser?.name || request.requesterUser?.name ||
request.requestEmailSnapshot || request.requestEmailSnapshot ||
request.requestingUserId || request.requestingUserId ||
"Unknown human requester" "Yêu cầu gia nhập không rõ danh tính"
} }
subtitle={ subtitle={
request.requesterUser?.email || request.requesterUser?.email ||
request.requestEmailSnapshot || request.requestEmailSnapshot ||
request.requestingUserId || request.requestingUserId ||
"No email available" "Không có email"
} }
context={ context={
request.invite request.invite
? `${request.invite.allowedJoinTypes} join invite${request.invite.humanRole ? ` • default role ${request.invite.humanRole}` : ""}` ? `${request.invite.allowedJoinTypes} join invite${request.invite.humanRole ? ` • default role ${request.invite.humanRole}` : ""}`
: "Invite metadata unavailable" : "Không có thông tin lời mời"
} }
detail={`Submitted ${new Date(request.createdAt).toLocaleString()}`} detail={`Đã gửi lúc ${new Date(request.createdAt).toLocaleString()}`}
approveLabel="Approve human" approveLabel="Phê duyệt"
rejectLabel="Reject human" rejectLabel="Từ chối"
disabled={joinRequestActionPending} disabled={joinRequestActionPending}
onApprove={() => approveJoinRequestMutation.mutate(request.id)} onApprove={() => approveJoinRequestMutation.mutate(request.id)}
onReject={() => rejectJoinRequestMutation.mutate(request.id)} onReject={() => rejectJoinRequestMutation.mutate(request.id)}
...@@ -341,14 +341,14 @@ export function CompanyAccess() { ...@@ -341,14 +341,14 @@ export function CompanyAccess() {
<div className="overflow-hidden rounded-xl border border-border"> <div className="overflow-hidden rounded-xl border border-border">
<div className="grid grid-cols-[minmax(0,1.5fr)_120px_120px_minmax(0,1.2fr)_180px] gap-3 border-b border-border px-4 py-3 text-xs font-medium uppercase tracking-wide text-muted-foreground"> <div className="grid grid-cols-[minmax(0,1.5fr)_120px_120px_minmax(0,1.2fr)_180px] gap-3 border-b border-border px-4 py-3 text-xs font-medium uppercase tracking-wide text-muted-foreground">
<div>User account</div> <div>Tài khoản</div>
<div>Role</div> <div>Vai trò</div>
<div>Status</div> <div>Trạng thái</div>
<div>Grants</div> <div>Quyền hạn</div>
<div className="text-right">Action</div> <div className="text-right">Hành động</div>
</div> </div>
{members.length === 0 ? ( {members.length === 0 ? (
<div className="px-4 py-8 text-sm text-muted-foreground">No user memberships found for this company yet.</div> <div className="px-4 py-8 text-sm text-muted-foreground">Chưa có thành viên nào trong phòng ban này.</div>
) : ( ) : (
members.map((member) => { members.map((member) => {
const removalReason = member.removal?.reason ?? null; const removalReason = member.removal?.reason ?? null;
...@@ -403,16 +403,16 @@ export function CompanyAccess() { ...@@ -403,16 +403,16 @@ export function CompanyAccess() {
<Dialog open={!!editingMember} onOpenChange={(open) => !open && setEditingMemberId(null)}> <Dialog open={!!editingMember} onOpenChange={(open) => !open && setEditingMemberId(null)}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Edit member</DialogTitle> <DialogTitle>Chỉnh sửa thành viên</DialogTitle>
<DialogDescription> <DialogDescription>
Update company role, membership status, and explicit grants for {editingMember?.user?.name || editingMember?.user?.email || editingMember?.principalId}. Cập nhật vai trò, trạng thái thành viên và phân quyền chi tiết cho {editingMember?.user?.name || editingMember?.user?.email || editingMember?.principalId}.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{editingMember && ( {editingMember && (
<div className="space-y-5"> <div className="space-y-5">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2 text-sm"> <label className="space-y-2 text-sm">
<span className="font-medium">Company role</span> <span className="font-medium">Vai trò phòng ban</span>
<select <select
className="w-full rounded-md border border-border bg-background px-3 py-2" className="w-full rounded-md border border-border bg-background px-3 py-2"
value={draftRole ?? ""} value={draftRole ?? ""}
...@@ -420,7 +420,7 @@ export function CompanyAccess() { ...@@ -420,7 +420,7 @@ export function CompanyAccess() {
setDraftRole((event.target.value || null) as CompanyMember["membershipRole"]) setDraftRole((event.target.value || null) as CompanyMember["membershipRole"])
} }
> >
<option value="">Unset</option> <option value="">Chưa thiết lập</option>
{Object.entries(HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS).map(([value, label]) => ( {Object.entries(HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS).map(([value, label]) => (
<option key={value} value={value}> <option key={value} value={value}>
{label} {label}
...@@ -429,7 +429,7 @@ export function CompanyAccess() { ...@@ -429,7 +429,7 @@ export function CompanyAccess() {
</select> </select>
</label> </label>
<label className="space-y-2 text-sm"> <label className="space-y-2 text-sm">
<span className="font-medium">Membership status</span> <span className="font-medium">Trạng thái thành viên</span>
<select <select
className="w-full rounded-md border border-border bg-background px-3 py-2" className="w-full rounded-md border border-border bg-background px-3 py-2"
value={draftStatus} value={draftStatus}
...@@ -437,26 +437,26 @@ export function CompanyAccess() { ...@@ -437,26 +437,26 @@ export function CompanyAccess() {
setDraftStatus(event.target.value as EditableMemberStatus) setDraftStatus(event.target.value as EditableMemberStatus)
} }
> >
<option value="active">Active</option> <option value="active">Hoạt động</option>
<option value="pending">Pending</option> <option value="pending">Chờ duyệt</option>
<option value="suspended">Suspended</option> <option value="suspended">Tạm ngưng</option>
</select> </select>
</label> </label>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<h3 className="text-sm font-medium">Grants</h3> <h3 className="text-sm font-medium">Quyền hạn cụ thể</h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Roles provide implicit grants automatically. Explicit grants below are only for overrides and extra access that should persist even if the role changes. Vai trò cung cấp các quyền hạn mặc định. Phân quyền cụ thể dưới đây dùng để ghi đè hoặc bổ sung quyền hạn độc lập với vai trò.
</p> </p>
</div> </div>
<div className="rounded-lg border border-border px-3 py-3"> <div className="rounded-lg border border-border px-3 py-3">
<div className="text-sm font-medium">Implicit grants from role</div> <div className="text-sm font-medium">Quyền mặc định theo vai trò</div>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
{draftRole {draftRole
? `${HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS[draftRole]} currently includes these permissions automatically.` ? `Vai trò ${HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS[draftRole]} tự động bao gồm các quyền hạn này.`
: "No role is selected, so this member has no implicit grants right now."} : "Chưa chọn vai trò, thành viên này hiện chưa có quyền mặc định nào."}
</p> </p>
{implicitGrantKeys.length > 0 ? ( {implicitGrantKeys.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
...@@ -507,7 +507,7 @@ export function CompanyAccess() { ...@@ -507,7 +507,7 @@ export function CompanyAccess() {
)} )}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setEditingMemberId(null)}> <Button variant="outline" onClick={() => setEditingMemberId(null)}>
Cancel Hủy bỏ
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
...@@ -521,7 +521,7 @@ export function CompanyAccess() { ...@@ -521,7 +521,7 @@ export function CompanyAccess() {
}} }}
disabled={updateMemberMutation.isPending} disabled={updateMemberMutation.isPending}
> >
{updateMemberMutation.isPending ? "Saving…" : "Save access"} {updateMemberMutation.isPending ? "Đang lưu…" : "Lưu phân quyền"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
...@@ -530,9 +530,9 @@ export function CompanyAccess() { ...@@ -530,9 +530,9 @@ export function CompanyAccess() {
<Dialog open={!!removingMember} onOpenChange={(open) => !open && setRemovingMemberId(null)}> <Dialog open={!!removingMember} onOpenChange={(open) => !open && setRemovingMemberId(null)}>
<DialogContent className="max-w-xl"> <DialogContent className="max-w-xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Remove member</DialogTitle> <DialogTitle>Xóa thành viên</DialogTitle>
<DialogDescription> <DialogDescription>
Archive {memberDisplayName(removingMember)} and move active assignments before hiding this user from assignment fields. Lưu trữ {memberDisplayName(removingMember)} và bàn giao các công việc đang thực hiện trước khi xóa thành viên này.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{removingMember && ( {removingMember && (
...@@ -542,22 +542,22 @@ export function CompanyAccess() { ...@@ -542,22 +542,22 @@ export function CompanyAccess() {
<div className="text-sm text-muted-foreground">{removingMember.user?.email || removingMember.principalId}</div> <div className="text-sm text-muted-foreground">{removingMember.user?.email || removingMember.principalId}</div>
<div className="mt-2 text-sm text-muted-foreground"> <div className="mt-2 text-sm text-muted-foreground">
{assignedIssuesQuery.isLoading {assignedIssuesQuery.isLoading
? "Checking assigned issues..." ? "Đang kiểm tra công việc đã gán..."
: `${assignedIssues.length} open assigned issue${assignedIssues.length === 1 ? "" : "s"}`} : `${assignedIssues.length} công việc đang xử lý.`}
</div> </div>
</div> </div>
{assignedIssues.length > 0 ? ( {assignedIssues.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-medium">Issue reassignment</div> <div className="text-sm font-medium">Bàn giao công việc</div>
<select <select
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm" className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
value={reassignmentTarget} value={reassignmentTarget}
onChange={(event) => setReassignmentTarget(event.target.value)} onChange={(event) => setReassignmentTarget(event.target.value)}
> >
<option value="__unassigned">Leave unassigned</option> <option value="__unassigned">Không phân công (để trống)</option>
{activeReassignmentUsers.length > 0 ? ( {activeReassignmentUsers.length > 0 ? (
<optgroup label="Humans"> <optgroup label="Nhân sự">
{activeReassignmentUsers.map((member) => ( {activeReassignmentUsers.map((member) => (
<option key={member.id} value={`user:${member.principalId}`}> <option key={member.id} value={`user:${member.principalId}`}>
{memberDisplayName(member)} {memberDisplayName(member)}
...@@ -566,7 +566,7 @@ export function CompanyAccess() { ...@@ -566,7 +566,7 @@ export function CompanyAccess() {
</optgroup> </optgroup>
) : null} ) : null}
{activeReassignmentAgents.length > 0 ? ( {activeReassignmentAgents.length > 0 ? (
<optgroup label="Agents"> <optgroup label="AI Agents">
{activeReassignmentAgents.map((agent) => ( {activeReassignmentAgents.map((agent) => (
<option key={agent.id} value={`agent:${agent.id}`}> <option key={agent.id} value={`agent:${agent.id}`}>
{agent.name} ({agent.role}) {agent.name} ({agent.role})
...@@ -584,7 +584,7 @@ export function CompanyAccess() { ...@@ -584,7 +584,7 @@ export function CompanyAccess() {
))} ))}
{assignedIssues.length > 6 ? ( {assignedIssues.length > 6 ? (
<div className="px-3 py-2 text-sm text-muted-foreground"> <div className="px-3 py-2 text-sm text-muted-foreground">
{assignedIssues.length - 6} more issue{assignedIssues.length - 6 === 1 ? "" : "s"} và {assignedIssues.length - 6} công việc khác.
</div> </div>
) : null} ) : null}
</div> </div>
...@@ -594,7 +594,7 @@ export function CompanyAccess() { ...@@ -594,7 +594,7 @@ export function CompanyAccess() {
)} )}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setRemovingMemberId(null)}> <Button variant="outline" onClick={() => setRemovingMemberId(null)}>
Cancel Hủy bỏ
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
...@@ -607,7 +607,7 @@ export function CompanyAccess() { ...@@ -607,7 +607,7 @@ export function CompanyAccess() {
}} }}
disabled={archiveMemberMutation.isPending || assignedIssuesQuery.isLoading} disabled={archiveMemberMutation.isPending || assignedIssuesQuery.isLoading}
> >
{archiveMemberMutation.isPending ? "Removing..." : "Remove member"} {archiveMemberMutation.isPending ? "Đang xóa..." : "Xóa thành viên"}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
......
...@@ -169,9 +169,9 @@ export function CompanyEnvironments() { ...@@ -169,9 +169,9 @@ export function CompanyEnvironments() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" }, { label: "Thiết lập", href: "/company/settings" },
{ label: "Environments" }, { label: "Môi trường" },
]); ]);
}, [selectedCompany?.name, setBreadcrumbs]); }, [selectedCompany?.name, setBreadcrumbs]);
...@@ -397,7 +397,7 @@ export function CompanyEnvironments() { ...@@ -397,7 +397,7 @@ export function CompanyEnvironments() {
Object.keys(sandboxConfigErrors).length === 0); Object.keys(sandboxConfigErrors).length === 0);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to manage environments.</div>; return <div className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý môi trường.</div>;
} }
if (!environmentsEnabled) { if (!environmentsEnabled) {
...@@ -405,10 +405,10 @@ export function CompanyEnvironments() { ...@@ -405,10 +405,10 @@ export function CompanyEnvironments() {
<div className="max-w-3xl space-y-4"> <div className="max-w-3xl space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" /> <Settings className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Environments</h1> <h1 className="text-lg font-semibold">Môi trường phòng ban</h1>
</div> </div>
<div className="rounded-md border border-border px-4 py-4 text-sm text-muted-foreground"> <div className="rounded-md border border-border px-4 py-4 text-sm text-muted-foreground">
Enable Environments in instance experimental settings to manage company execution targets. Kích hoạt môi trường trong thiết lập thử nghiệm của hệ thống để quản lý các mục tiêu thực thi của phòng ban.
</div> </div>
</div> </div>
); );
...@@ -419,10 +419,10 @@ export function CompanyEnvironments() { ...@@ -419,10 +419,10 @@ export function CompanyEnvironments() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" /> <Settings className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Environments</h1> <h1 className="text-lg font-semibold">Môi trường phòng ban</h1>
</div> </div>
<p className="max-w-3xl text-sm text-muted-foreground"> <p className="max-w-3xl text-sm text-muted-foreground">
Define reusable execution targets for projects, issue workspaces, and remote-capable adapters. Định nghĩa các mục tiêu thực thi tái sử dụng cho dự án, không gian làm việc và các adapter hỗ trợ điều khiển từ xa.
</p> </p>
</div> </div>
...@@ -486,7 +486,7 @@ export function CompanyEnvironments() { ...@@ -486,7 +486,7 @@ export function CompanyEnvironments() {
<div className="space-y-3"> <div className="space-y-3">
{(environments ?? []).length === 0 ? ( {(environments ?? []).length === 0 ? (
<div className="text-sm text-muted-foreground">No environments saved for this company yet.</div> <div className="text-sm text-muted-foreground">Chưa có môi trường nào được lưu cho phòng ban này.</div>
) : ( ) : (
(environments ?? []).map((environment) => { (environments ?? []).map((environment) => {
const probe = probeResults[environment.id] ?? null; const probe = probeResults[environment.id] ?? null;
......
...@@ -500,7 +500,7 @@ function ExportPreviewPane({ ...@@ -500,7 +500,7 @@ function ExportPreviewPane({
}) { }) {
if (!selectedFile || content === null) { if (!selectedFile || content === null) {
return ( return (
<EmptyState icon={Package} message="Select a file to preview its contents." /> <EmptyState icon={Package} message="Chọn một tệp để xem trước nội dung." />
); );
} }
...@@ -546,7 +546,7 @@ function ExportPreviewPane({ ...@@ -546,7 +546,7 @@ function ExportPreviewPane({
</pre> </pre>
) : ( ) : (
<div className="rounded-lg border border-border bg-accent/10 px-4 py-3 text-sm text-muted-foreground"> <div className="rounded-lg border border-border bg-accent/10 px-4 py-3 text-sm text-muted-foreground">
Binary asset preview is not available for this file type. Không hỗ trợ xem trước tệp nhị phân loại này.
</div> </div>
)} )}
</div> </div>
...@@ -911,7 +911,7 @@ export function CompanyExport() { ...@@ -911,7 +911,7 @@ export function CompanyExport() {
} }
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Package} message="Select a company to export." />; return <EmptyState icon={Package} message="Chọn phòng ban để xuất dữ liệu." />;
} }
if (exportPreviewMutation.isPending && !exportData) { if (exportPreviewMutation.isPending && !exportData) {
...@@ -919,7 +919,7 @@ export function CompanyExport() { ...@@ -919,7 +919,7 @@ export function CompanyExport() {
} }
if (!exportData) { if (!exportData) {
return <EmptyState icon={Package} message="Loading export data..." />; return <EmptyState icon={Package} message="Đang tải dữ liệu xuất..." />;
} }
const previewContent = selectedFile const previewContent = selectedFile
...@@ -935,14 +935,14 @@ export function CompanyExport() { ...@@ -935,14 +935,14 @@ export function CompanyExport() {
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-4 text-sm">
<span className="font-medium"> <span className="font-medium">
{selectedCompany?.name ?? "Company"} export Xuất phòng ban {selectedCompany?.name ?? "phòng ban"}
</span> </span>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected Đã chọn {selectedCount} / {totalFiles} tệp
</span> </span>
{warnings.length > 0 && ( {warnings.length > 0 && (
<span className="text-amber-500"> <span className="text-amber-500">
{warnings.length} warning{warnings.length === 1 ? "" : "s"} {warnings.length} cảnh báo
</span> </span>
)} )}
</div> </div>
...@@ -953,8 +953,8 @@ export function CompanyExport() { ...@@ -953,8 +953,8 @@ export function CompanyExport() {
> >
<Download className="mr-1.5 h-3.5 w-3.5" /> <Download className="mr-1.5 h-3.5 w-3.5" />
{downloadMutation.isPending {downloadMutation.isPending
? "Building export..." ? "Đang tạo tệp xuất..."
: `Export ${selectedCount} file${selectedCount === 1 ? "" : "s"}`} : `Xuất ${selectedCount} tệp`}
</Button> </Button>
</div> </div>
</div> </div>
...@@ -972,7 +972,7 @@ export function CompanyExport() { ...@@ -972,7 +972,7 @@ export function CompanyExport() {
<div className="grid h-[calc(100vh-12rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]"> <div className="grid h-[calc(100vh-12rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="flex flex-col border-r border-border overflow-hidden"> <aside className="flex flex-col border-r border-border overflow-hidden">
<div className="border-b border-border px-4 py-3 shrink-0"> <div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2> <h2 className="text-base font-semibold">Các tệp trong gói</h2>
</div> </div>
<div className="border-b border-border px-3 py-2 shrink-0"> <div className="border-b border-border px-3 py-2 shrink-0">
<div className="flex items-center gap-2 rounded-md border border-border px-2 py-1"> <div className="flex items-center gap-2 rounded-md border border-border px-2 py-1">
...@@ -981,7 +981,7 @@ export function CompanyExport() { ...@@ -981,7 +981,7 @@ export function CompanyExport() {
type="text" type="text"
value={treeSearch} value={treeSearch}
onChange={(e) => handleSearchChange(e.target.value)} onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Search files..." placeholder="Tìm kiếm tệp..."
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground" className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
data-page-search-target="true" data-page-search-target="true"
/> />
...@@ -1005,7 +1005,7 @@ export function CompanyExport() { ...@@ -1005,7 +1005,7 @@ export function CompanyExport() {
onClick={() => setTaskLimit((prev) => prev + TASKS_PAGE_SIZE)} onClick={() => setTaskLimit((prev) => prev + TASKS_PAGE_SIZE)}
className="w-full rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent/30 hover:text-foreground transition-colors" className="w-full rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent/30 hover:text-foreground transition-colors"
> >
Show more issues ({visibleTaskChildren} of {totalTaskChildren}) Hiển thị thêm vấn đề ({visibleTaskChildren} trên {totalTaskChildren})
</button> </button>
</div> </div>
)} )}
......
...@@ -193,7 +193,7 @@ function ImportPreviewPane({ ...@@ -193,7 +193,7 @@ function ImportPreviewPane({
}) { }) {
if (!selectedFile || content === null) { if (!selectedFile || content === null) {
return ( return (
<EmptyState icon={Package} message="Select a file to preview its contents." /> <EmptyState icon={Package} message="Chọn một tệp để xem trước nội dung." />
); );
} }
...@@ -255,7 +255,7 @@ function ImportPreviewPane({ ...@@ -255,7 +255,7 @@ function ImportPreviewPane({
</pre> </pre>
) : ( ) : (
<div className="rounded-lg border border-border bg-accent/10 px-4 py-3 text-sm text-muted-foreground"> <div className="rounded-lg border border-border bg-accent/10 px-4 py-3 text-sm text-muted-foreground">
Binary asset preview is not available for this file type. Không hỗ trợ xem trước tệp nhị phân loại này.
</div> </div>
)} )}
</div> </div>
...@@ -411,10 +411,10 @@ function ConflictResolutionList({ ...@@ -411,10 +411,10 @@ function ConflictResolutionList({
<div className="rounded-md border border-border"> <div className="rounded-md border border-border">
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5"> <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
<h3 className="text-sm font-medium"> <h3 className="text-sm font-medium">
Renames Đổi tên
</h3> </h3>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{conflicts.length} item{conflicts.length === 1 ? "" : "s"} {conflicts.length} mục
</span> </span>
</div> </div>
<div className="divide-y divide-border"> <div className="divide-y divide-border">
...@@ -442,7 +442,7 @@ function ConflictResolutionList({ ...@@ -442,7 +442,7 @@ function ConflictResolutionList({
)} )}
onClick={() => onToggleSkip(item.slug, item.filePath)} onClick={() => onToggleSkip(item.slug, item.filePath)}
> >
{isSkipped ? "skipped" : "skip"} {isSkipped ? "đã bỏ qua" : "bỏ qua"}
</button> </button>
<span className={cn( <span className={cn(
...@@ -495,10 +495,10 @@ function ConflictResolutionList({ ...@@ -495,10 +495,10 @@ function ConflictResolutionList({
{isConfirmed ? ( {isConfirmed ? (
<> <>
<Check className="h-3 w-3" /> <Check className="h-3 w-3" />
confirmed đã xác nhận
</> </>
) : ( ) : (
"confirm rename" "xác nhận đổi tên"
)} )}
</button> </button>
)} )}
...@@ -549,9 +549,9 @@ function AdapterPickerList({ ...@@ -549,9 +549,9 @@ function AdapterPickerList({
<div className="mx-5 mt-3"> <div className="mx-5 mt-3">
<div className="rounded-md border border-border"> <div className="rounded-md border border-border">
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5"> <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
<h3 className="text-sm font-medium">Adapters</h3> <h3 className="text-sm font-medium">Bộ điều hợp (Adapters)</h3>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{agents.length} agent{agents.length === 1 ? "" : "s"} {agents.length} tác nhân
</span> </span>
</div> </div>
<div className="divide-y divide-border"> <div className="divide-y divide-border">
...@@ -567,7 +567,7 @@ function AdapterPickerList({ ...@@ -567,7 +567,7 @@ function AdapterPickerList({
"shrink-0 rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-wide", "shrink-0 rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-wide",
"text-blue-500 border-blue-500/30", "text-blue-500 border-blue-500/30",
)}> )}>
agent tác nhân
</span> </span>
<span className="shrink-0 font-mono text-xs text-muted-foreground"> <span className="shrink-0 font-mono text-xs text-muted-foreground">
{agent.name} {agent.name}
...@@ -595,7 +595,7 @@ function AdapterPickerList({ ...@@ -595,7 +595,7 @@ function AdapterPickerList({
onClick={() => onToggleExpand(agent.slug)} onClick={() => onToggleExpand(agent.slug)}
> >
<ChevronRight className={cn("h-3 w-3 transition-transform", isExpanded && "rotate-90")} /> <ChevronRight className={cn("h-3 w-3 transition-transform", isExpanded && "rotate-90")} />
configure adapter cấu hình bộ điều hợp
</button> </button>
</div> </div>
{isExpanded && ( {isExpanded && (
...@@ -629,11 +629,11 @@ async function readLocalPackageZip(file: File): Promise<{ ...@@ -629,11 +629,11 @@ async function readLocalPackageZip(file: File): Promise<{
files: Record<string, CompanyPortabilityFileEntry>; files: Record<string, CompanyPortabilityFileEntry>;
}> { }> {
if (!/\.zip$/i.test(file.name)) { if (!/\.zip$/i.test(file.name)) {
throw new Error("Select a .zip company package."); throw new Error("Vui lòng chọn một gói phòng ban dạng .zip.");
} }
const archive = await readZipArchive(await file.arrayBuffer()); const archive = await readZipArchive(await file.arrayBuffer());
if (Object.keys(archive.files).length === 0) { if (Object.keys(archive.files).length === 0) {
throw new Error("No package files were found in the selected zip archive."); throw new Error("Không tìm thấy tệp cấu hình nào trong tệp zip đã chọn.");
} }
return { return {
name: file.name, name: file.name,
...@@ -704,7 +704,7 @@ export function CompanyImport() { ...@@ -704,7 +704,7 @@ export function CompanyImport() {
}, [companyAgents]); }, [companyAgents]);
const localZipHelpText = const localZipHelpText =
"Upload a .zip exported directly from Paperclip. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly."; "Tải lên tệp .zip được xuất trực tiếp từ Paperclip. Các tệp zip được nén lại bằng Finder, Explorer hoặc các công cụ khác có thể không hoạt động chính xác.";
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
...@@ -1086,7 +1086,7 @@ export function CompanyImport() { ...@@ -1086,7 +1086,7 @@ export function CompanyImport() {
const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null; const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null;
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Download} message="Select a company to import into." />; return <EmptyState icon={Download} message="Chọn phòng ban để nhập dữ liệu." />;
} }
return ( return (
...@@ -1094,17 +1094,17 @@ export function CompanyImport() { ...@@ -1094,17 +1094,17 @@ export function CompanyImport() {
{/* Source form section */} {/* Source form section */}
<div className="border-b border-border px-5 py-5 space-y-4"> <div className="border-b border-border px-5 py-5 space-y-4">
<div> <div>
<h2 className="text-base font-semibold">Import source</h2> <h2 className="text-base font-semibold">Nguồn nhập</h2>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
Choose a GitHub repo or upload a local Paperclip zip package. Chọn một kho lưu trữ GitHub hoặc tải lên tệp zip phòng ban của Paperclip.
</p> </p>
</div> </div>
<div className="grid gap-2 md:grid-cols-2"> <div className="grid gap-2 md:grid-cols-2">
{( {(
[ [
{ key: "github", icon: Github, label: "GitHub repo" }, { key: "github", icon: Github, label: "Kho GitHub" },
{ key: "local", icon: Upload, label: "Local zip" }, { key: "local", icon: Upload, label: "Tệp zip cục bộ" },
] as const ] as const
).map(({ key, icon: Icon, label }) => ( ).map(({ key, icon: Icon, label }) => (
<button <button
...@@ -1163,7 +1163,7 @@ export function CompanyImport() { ...@@ -1163,7 +1163,7 @@ export function CompanyImport() {
) : ( ) : (
<Field <Field
label="GitHub URL" label="GitHub URL"
hint="Repo tree path or blob URL to COMPANY.md (e.g. github.com/owner/repo/tree/main/company)." hint="Đường dẫn nhánh/thư mục hoặc URL blob tới COMPANY.md (ví dụ: github.com/owner/repo/tree/main/company)."
> >
<input <input
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
...@@ -1178,7 +1178,7 @@ export function CompanyImport() { ...@@ -1178,7 +1178,7 @@ export function CompanyImport() {
</Field> </Field>
)} )}
<Field label="Target" hint="Import into this company or create a new one."> <Field label="Đích đến" hint="Nhập vào phòng ban này hoặc tạo một phòng ban mới.">
<select <select
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
value={targetMode} value={targetMode}
...@@ -1187,17 +1187,17 @@ export function CompanyImport() { ...@@ -1187,17 +1187,17 @@ export function CompanyImport() {
setImportPreview(null); setImportPreview(null);
}} }}
> >
<option value="new">Create new company</option> <option value="new">Tạo phòng ban mới</option>
<option value="existing"> <option value="existing">
Existing company: {selectedCompany?.name} Phòng ban hiện tại: {selectedCompany?.name}
</option> </option>
</select> </select>
</Field> </Field>
{targetMode === "new" && ( {targetMode === "new" && (
<Field <Field
label="New company name" label="Tên phòng ban mới"
hint="Optional override. Leave blank to use the package name." hint="Tùy chọn ghi đè. Để trống để sử dụng tên gói mặc định."
> >
<input <input
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
...@@ -1210,8 +1210,8 @@ export function CompanyImport() { ...@@ -1210,8 +1210,8 @@ export function CompanyImport() {
)} )}
<Field <Field
label="Collision strategy" label="Xử lý trùng lặp"
hint="Board imports can rename, skip, or replace matching company content." hint="Việc nhập dữ liệu có thể đổi tên, bỏ qua hoặc thay thế nội dung phòng ban trùng khớp."
> >
<select <select
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
...@@ -1221,9 +1221,9 @@ export function CompanyImport() { ...@@ -1221,9 +1221,9 @@ export function CompanyImport() {
setImportPreview(null); setImportPreview(null);
}} }}
> >
<option value="rename">Rename on conflict</option> <option value="rename">Đổi tên khi trùng</option>
<option value="skip">Skip on conflict</option> <option value="skip">Bỏ qua khi trùng</option>
<option value="replace">Replace existing</option> <option value="replace">Thay thế hiện tại</option>
</select> </select>
</Field> </Field>
...@@ -1234,7 +1234,7 @@ export function CompanyImport() { ...@@ -1234,7 +1234,7 @@ export function CompanyImport() {
onClick={() => previewMutation.mutate()} onClick={() => previewMutation.mutate()}
disabled={previewMutation.isPending || !hasSource} disabled={previewMutation.isPending || !hasSource}
> >
{previewMutation.isPending ? "Previewing..." : "Preview import"} {previewMutation.isPending ? "Đang tải bản xem trước..." : "Xem trước nhập dữ liệu"}
</Button> </Button>
</div> </div>
</div> </div>
...@@ -1246,14 +1246,14 @@ export function CompanyImport() { ...@@ -1246,14 +1246,14 @@ export function CompanyImport() {
<div className="sticky top-0 z-10 border-b border-border bg-background px-5 py-3"> <div className="sticky top-0 z-10 border-b border-border bg-background px-5 py-3">
<div className="flex flex-wrap items-center gap-4 text-sm"> <div className="flex flex-wrap items-center gap-4 text-sm">
<span className="font-medium"> <span className="font-medium">
Import preview Xem trước nhập dữ liệu
</span> </span>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected Đã chọn {selectedCount} / {totalFiles} tệp
</span> </span>
{conflicts.length > 0 && ( {conflicts.length > 0 && (
<span className="text-amber-500"> <span className="text-amber-500">
{conflicts.length} conflict{conflicts.length === 1 ? "" : "s"} {conflicts.length} xung đột
</span> </span>
)} )}
{importPreview.errors.length > 0 && ( {importPreview.errors.length > 0 && (
...@@ -1295,8 +1295,8 @@ export function CompanyImport() { ...@@ -1295,8 +1295,8 @@ export function CompanyImport() {
> >
<Download className="mr-1.5 h-3.5 w-3.5" /> <Download className="mr-1.5 h-3.5 w-3.5" />
{importMutation.isPending {importMutation.isPending
? "Importing..." ? "Đang nhập..."
: `Import ${selectedCount} file${selectedCount === 1 ? "" : "s"}`} : `Nhập ${selectedCount} tệp`}
</Button> </Button>
</div> </div>
...@@ -1322,7 +1322,7 @@ export function CompanyImport() { ...@@ -1322,7 +1322,7 @@ export function CompanyImport() {
<div className="grid h-[calc(100vh-16rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]"> <div className="grid h-[calc(100vh-16rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="flex flex-col border-r border-border overflow-hidden"> <aside className="flex flex-col border-r border-border overflow-hidden">
<div className="border-b border-border px-4 py-3 shrink-0"> <div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2> <h2 className="text-base font-semibold">Tệp trong gói</h2>
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<FileTree <FileTree
......
...@@ -13,9 +13,9 @@ import { queryKeys } from "@/lib/queryKeys"; ...@@ -13,9 +13,9 @@ import { queryKeys } from "@/lib/queryKeys";
const inviteRoleOptions = [ const inviteRoleOptions = [
{ {
value: "viewer", value: "viewer",
label: "Viewer", label: "Người quan sát (Viewer)",
description: "Can view company work and follow along without operational permissions.", description: "Có thể xem công việc và theo dõi hoạt động mà không có quyền vận hành.",
gets: "No built-in grants.", gets: "Không có quyền mặc định.",
}, },
{ {
value: "operator", value: "operator",
...@@ -31,9 +31,9 @@ const inviteRoleOptions = [ ...@@ -31,9 +31,9 @@ const inviteRoleOptions = [
}, },
{ {
value: "owner", value: "owner",
label: "Owner", label: "Chủ sở hữu (Owner)",
description: "Full company access, including membership and permission management.", description: "Toàn quyền truy cập phòng ban, bao gồm quản lý thành viên và phân quyền.",
gets: "Everything in Admin, plus managing members and permission grants.", gets: "Mọi quyền của Quản trị viên, cộng thêm quyền quản lý thành viên và phân quyền cụ thể.",
}, },
] as const; ] as const;
...@@ -81,9 +81,9 @@ export function CompanyInvites() { ...@@ -81,9 +81,9 @@ export function CompanyInvites() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" }, { label: "Thiết lập", href: "/company/settings" },
{ label: "Invites" }, { label: "Lời mời" },
]); ]);
}, [selectedCompany?.name, setBreadcrumbs]); }, [selectedCompany?.name, setBreadcrumbs]);
...@@ -151,20 +151,20 @@ export function CompanyInvites() { ...@@ -151,20 +151,20 @@ export function CompanyInvites() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to manage invites.</div>; return <div className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý lời mời.</div>;
} }
if (invitesQuery.isLoading) { if (invitesQuery.isLoading) {
return <div className="text-sm text-muted-foreground">Loading invites…</div>; return <div className="text-sm text-muted-foreground">Đang tải danh sách lời mời…</div>;
} }
if (invitesQuery.error) { if (invitesQuery.error) {
const message = const message =
invitesQuery.error instanceof ApiError && invitesQuery.error.status === 403 invitesQuery.error instanceof ApiError && invitesQuery.error.status === 403
? "You do not have permission to manage company invites." ? "Bạn không có quyền quản lý lời mời gia nhập phòng ban."
: invitesQuery.error instanceof Error : invitesQuery.error instanceof Error
? invitesQuery.error.message ? invitesQuery.error.message
: "Failed to load invites."; : "Không thể tải danh sách lời mời.";
return <div className="text-sm text-destructive">{message}</div>; return <div className="text-sm text-destructive">{message}</div>;
} }
...@@ -173,10 +173,10 @@ export function CompanyInvites() { ...@@ -173,10 +173,10 @@ export function CompanyInvites() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MailPlus className="h-5 w-5 text-muted-foreground" /> <MailPlus className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Invites</h1> <h1 className="text-lg font-semibold">Lời mời gia nhập phòng ban</h1>
</div> </div>
<p className="max-w-3xl text-sm text-muted-foreground"> <p className="max-w-3xl text-sm text-muted-foreground">
Create human invite links for company access. New invite links are copied to your clipboard when they are generated. Tạo liên kết mời người dùng tham gia phòng ban. Liên kết mời mới sẽ được tự động sao chép sau khi được tạo.
</p> </p>
</div> </div>
...@@ -288,7 +288,7 @@ export function CompanyInvites() { ...@@ -288,7 +288,7 @@ export function CompanyInvites() {
{inviteHistory.length === 0 ? ( {inviteHistory.length === 0 ? (
<div className="border-t border-border px-5 py-8 text-sm text-muted-foreground"> <div className="border-t border-border px-5 py-8 text-sm text-muted-foreground">
No invites have been created for this company yet. Chưa có lời mời nào được tạo cho phòng ban này.
</div> </div>
) : ( ) : (
<div className="border-t border-border"> <div className="border-t border-border">
......
...@@ -214,15 +214,15 @@ export function CompanySettings() { ...@@ -214,15 +214,15 @@ export function CompanySettings() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings" } { label: "Thiết lập" }
]); ]);
}, [setBreadcrumbs, selectedCompany?.name]); }, [setBreadcrumbs, selectedCompany?.name]);
if (!selectedCompany) { if (!selectedCompany) {
return ( return (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
No company selected. Select a company from the switcher above. Chưa chọn phòng ban. Vui lòng chọn một phòng ban ở menu chuyển đổi phía trên.
</div> </div>
); );
} }
...@@ -240,16 +240,16 @@ export function CompanySettings() { ...@@ -240,16 +240,16 @@ export function CompanySettings() {
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" /> <Settings className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Settings</h1> <h1 className="text-lg font-semibold">Thiết lập phòng ban</h1>
</div> </div>
{/* General */} {/* General */}
<div className="space-y-4"> <div className="space-y-4">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
General Thiết lập chung
</div> </div>
<div className="space-y-3 rounded-md border border-border px-4 py-4"> <div className="space-y-3 rounded-md border border-border px-4 py-4">
<Field label="Company name" hint="The display name for your company."> <Field label="Tên phòng ban" hint="Tên hiển thị cho phòng ban của bạn.">
<input <input
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
type="text" type="text"
...@@ -258,14 +258,14 @@ export function CompanySettings() { ...@@ -258,14 +258,14 @@ export function CompanySettings() {
/> />
</Field> </Field>
<Field <Field
label="Description" label="Mô tả"
hint="Optional description shown in the company profile." hint="Mô tả ngắn gọn về phòng ban này."
> >
<input <input
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none" className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
type="text" type="text"
value={description} value={description}
placeholder="Optional company description" placeholder="Nhập mô tả phòng ban (không bắt buộc)"
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
/> />
</Field> </Field>
...@@ -275,7 +275,7 @@ export function CompanySettings() { ...@@ -275,7 +275,7 @@ export function CompanySettings() {
{/* Appearance */} {/* Appearance */}
<div className="space-y-4"> <div className="space-y-4">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Appearance Giao diện
</div> </div>
<div className="space-y-3 rounded-md border border-border px-4 py-4"> <div className="space-y-3 rounded-md border border-border px-4 py-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
...@@ -289,8 +289,8 @@ export function CompanySettings() { ...@@ -289,8 +289,8 @@ export function CompanySettings() {
</div> </div>
<div className="flex-1 space-y-3"> <div className="flex-1 space-y-3">
<Field <Field
label="Logo" label="Logo phòng ban"
hint="Upload a PNG, JPEG, WEBP, GIF, or SVG logo image." hint="Tải lên tệp ảnh logo định dạng PNG, JPEG, WEBP, GIF hoặc SVG."
> >
<div className="space-y-2"> <div className="space-y-2">
<input <input
...@@ -307,7 +307,7 @@ export function CompanySettings() { ...@@ -307,7 +307,7 @@ export function CompanySettings() {
onClick={handleClearLogo} onClick={handleClearLogo}
disabled={clearLogoMutation.isPending} disabled={clearLogoMutation.isPending}
> >
{clearLogoMutation.isPending ? "Removing..." : "Remove logo"} {clearLogoMutation.isPending ? "Đang xóa..." : "Xóa logo"}
</Button> </Button>
</div> </div>
)} )}
...@@ -316,7 +316,7 @@ export function CompanySettings() { ...@@ -316,7 +316,7 @@ export function CompanySettings() {
{logoUploadError ?? {logoUploadError ??
(logoUploadMutation.error instanceof Error (logoUploadMutation.error instanceof Error
? logoUploadMutation.error.message ? logoUploadMutation.error.message
: "Logo upload failed")} : "Tải lên logo thất bại")}
</span> </span>
)} )}
{clearLogoMutation.isError && ( {clearLogoMutation.isError && (
...@@ -325,13 +325,13 @@ export function CompanySettings() { ...@@ -325,13 +325,13 @@ export function CompanySettings() {
</span> </span>
)} )}
{logoUploadMutation.isPending && ( {logoUploadMutation.isPending && (
<span className="text-xs text-muted-foreground">Uploading logo...</span> <span className="text-xs text-muted-foreground">Đang tải lên logo...</span>
)} )}
</div> </div>
</Field> </Field>
<Field <Field
label="Brand color" label="Màu chủ đạo"
hint="Sets the hue for the company icon. Leave empty for auto-generated color." hint="Thiết lập màu sắc cho biểu tượng phòng ban. Để trống để tự động tạo."
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
...@@ -349,7 +349,7 @@ export function CompanySettings() { ...@@ -349,7 +349,7 @@ export function CompanySettings() {
setBrandColor(v); setBrandColor(v);
} }
}} }}
placeholder="Auto" placeholder="Tự động"
className="w-28 rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm font-mono outline-none" className="w-28 rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm font-mono outline-none"
/> />
{brandColor && ( {brandColor && (
...@@ -359,14 +359,14 @@ export function CompanySettings() { ...@@ -359,14 +359,14 @@ export function CompanySettings() {
onClick={() => setBrandColor("")} onClick={() => setBrandColor("")}
className="text-xs text-muted-foreground" className="text-xs text-muted-foreground"
> >
Clear Xóa
</Button> </Button>
)} )}
</div> </div>
</Field> </Field>
<Field <Field
label="Attachment size limit" label="Giới hạn dung lượng tệp đính kèm"
hint={`Accepted range: 1-${MAX_COMPANY_ATTACHMENT_MAX_MIB} MiB.`} hint={`Phạm vi cho phép: từ 1 đến ${MAX_COMPANY_ATTACHMENT_MAX_MIB} MiB.`}
> >
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
...@@ -383,7 +383,7 @@ export function CompanySettings() { ...@@ -383,7 +383,7 @@ export function CompanySettings() {
</div> </div>
{!attachmentMaxValid && ( {!attachmentMaxValid && (
<span className="text-xs text-destructive"> <span className="text-xs text-destructive">
Enter a whole number from 1 to {MAX_COMPANY_ATTACHMENT_MAX_MIB}. Vui lòng nhập một số nguyên từ 1 đến {MAX_COMPANY_ATTACHMENT_MAX_MIB}.
</span> </span>
)} )}
</div> </div>
...@@ -401,16 +401,16 @@ export function CompanySettings() { ...@@ -401,16 +401,16 @@ export function CompanySettings() {
onClick={handleSaveGeneral} onClick={handleSaveGeneral}
disabled={generalMutation.isPending || !companyName.trim() || !attachmentMaxValid} disabled={generalMutation.isPending || !companyName.trim() || !attachmentMaxValid}
> >
{generalMutation.isPending ? "Saving..." : "Save changes"} {generalMutation.isPending ? "Đang lưu..." : "Lưu thay đổi"}
</Button> </Button>
{generalMutation.isSuccess && ( {generalMutation.isSuccess && (
<span className="text-xs text-muted-foreground">Saved</span> <span className="text-xs text-muted-foreground">Đã lưu</span>
)} )}
{generalMutation.isError && ( {generalMutation.isError && (
<span className="text-xs text-destructive"> <span className="text-xs text-destructive">
{generalMutation.error instanceof Error {generalMutation.error instanceof Error
? generalMutation.error.message ? generalMutation.error.message
: "Failed to save"} : "Lưu thất bại"}
</span> </span>
)} )}
</div> </div>
...@@ -419,12 +419,12 @@ export function CompanySettings() { ...@@ -419,12 +419,12 @@ export function CompanySettings() {
{/* Hiring */} {/* Hiring */}
<div className="space-y-4" data-testid="company-settings-team-section"> <div className="space-y-4" data-testid="company-settings-team-section">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Hiring Nhân sự & Tuyển dụng
</div> </div>
<div className="rounded-md border border-border px-4 py-3"> <div className="rounded-md border border-border px-4 py-3">
<ToggleField <ToggleField
label="Require board approval for new hires" label="Yêu cầu ban điều hành phê duyệt nhân sự mới"
hint="New agent hires stay pending until approved by board." hint="Nhân sự/agent mới sẽ ở trạng thái chờ duyệt cho đến khi được ban điều hành đồng ý."
checked={!!selectedCompany.requireBoardApprovalForNewAgents} checked={!!selectedCompany.requireBoardApprovalForNewAgents}
onChange={(v) => settingsMutation.mutate(v)} onChange={(v) => settingsMutation.mutate(v)}
toggleTestId="company-settings-team-approval-toggle" toggleTestId="company-settings-team-approval-toggle"
...@@ -435,14 +435,14 @@ export function CompanySettings() { ...@@ -435,14 +435,14 @@ export function CompanySettings() {
{/* Invites */} {/* Invites */}
<div className="space-y-4" data-testid="company-settings-invites-section"> <div className="space-y-4" data-testid="company-settings-invites-section">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Invites Lời mời
</div> </div>
<div className="space-y-3 rounded-md border border-border px-4 py-4"> <div className="space-y-3 rounded-md border border-border px-4 py-4">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Generate an OpenClaw agent invite snippet. Tạo đoạn mã lời mời cho OpenClaw agent.
</span> </span>
<HintIcon text="Creates a short-lived OpenClaw agent invite and renders a copy-ready prompt." /> <HintIcon text="Tạo mã lời mời OpenClaw agent ngắn hạn và hiển thị lời nhắc sẵn sàng sao chép." />
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Button <Button
...@@ -452,8 +452,8 @@ export function CompanySettings() { ...@@ -452,8 +452,8 @@ export function CompanySettings() {
disabled={inviteMutation.isPending} disabled={inviteMutation.isPending}
> >
{inviteMutation.isPending {inviteMutation.isPending
? "Generating..." ? "Đang tạo..."
: "Generate OpenClaw Invite Prompt"} : "Tạo lời nhắc mời OpenClaw"}
</Button> </Button>
</div> </div>
{inviteError && ( {inviteError && (
...@@ -466,7 +466,7 @@ export function CompanySettings() { ...@@ -466,7 +466,7 @@ export function CompanySettings() {
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
OpenClaw Invite Prompt Lời nhắc mời OpenClaw
</div> </div>
{snippetCopied && ( {snippetCopied && (
<span <span
...@@ -474,7 +474,7 @@ export function CompanySettings() { ...@@ -474,7 +474,7 @@ export function CompanySettings() {
className="flex items-center gap-1 text-xs text-green-600 animate-pulse" className="flex items-center gap-1 text-xs text-green-600 animate-pulse"
> >
<Check className="h-3 w-3" /> <Check className="h-3 w-3" />
Copied Đã sao chép
</span> </span>
)} )}
</div> </div>
...@@ -501,7 +501,7 @@ export function CompanySettings() { ...@@ -501,7 +501,7 @@ export function CompanySettings() {
} }
}} }}
> >
{snippetCopied ? "Copied snippet" : "Copy snippet"} {snippetCopied ? "Đã sao chép mã" : "Sao chép mã"}
</Button> </Button>
</div> </div>
</div> </div>
...@@ -513,24 +513,24 @@ export function CompanySettings() { ...@@ -513,24 +513,24 @@ export function CompanySettings() {
{/* Import / Export */} {/* Import / Export */}
<div className="space-y-4"> <div className="space-y-4">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Company Packages Gói dữ liệu phòng ban
</div> </div>
<div className="rounded-md border border-border px-4 py-4"> <div className="rounded-md border border-border px-4 py-4">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Import and export have moved to dedicated pages accessible from the{" "} Chức năng Nhập và Xuất dữ liệu đã được di chuyển sang các trang chuyên biệt, truy cập từ thanh tiêu đề{" "}
<a href="/org" className="underline hover:text-foreground">Org Chart</a> header. <a href="/org" className="underline hover:text-foreground">Sơ đồ</a>.
</p> </p>
<div className="mt-3 flex items-center gap-2"> <div className="mt-3 flex items-center gap-2">
<Button size="sm" variant="outline" asChild> <Button size="sm" variant="outline" asChild>
<a href="/company/export"> <a href="/company/export">
<Download className="mr-1.5 h-3.5 w-3.5" /> <Download className="mr-1.5 h-3.5 w-3.5" />
Export Xuất dữ liệu
</a> </a>
</Button> </Button>
<Button size="sm" variant="outline" asChild> <Button size="sm" variant="outline" asChild>
<a href="/company/import"> <a href="/company/import">
<Upload className="mr-1.5 h-3.5 w-3.5" /> <Upload className="mr-1.5 h-3.5 w-3.5" />
Import Nhập dữ liệu
</a> </a>
</Button> </Button>
</div> </div>
...@@ -540,12 +540,11 @@ export function CompanySettings() { ...@@ -540,12 +540,11 @@ export function CompanySettings() {
{/* Danger Zone */} {/* Danger Zone */}
<div className="space-y-4"> <div className="space-y-4">
<div className="text-xs font-medium text-destructive uppercase tracking-wide"> <div className="text-xs font-medium text-destructive uppercase tracking-wide">
Danger Zone Khu vực nguy hiểm
</div> </div>
<div className="space-y-3 rounded-md border border-destructive/40 bg-destructive/5 px-4 py-4"> <div className="space-y-3 rounded-md border border-destructive/40 bg-destructive/5 px-4 py-4">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Archive this company to hide it from the sidebar. This persists in Lưu trữ phòng ban này để ẩn khỏi thanh bên. Thao tác này sẽ cập nhật vào cơ sở dữ liệu.
the database.
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
...@@ -558,7 +557,7 @@ export function CompanySettings() { ...@@ -558,7 +557,7 @@ export function CompanySettings() {
onClick={() => { onClick={() => {
if (!selectedCompanyId) return; if (!selectedCompanyId) return;
const confirmed = window.confirm( const confirmed = window.confirm(
`Archive company "${selectedCompany.name}"? It will be hidden from the sidebar.` `Lưu trữ phòng ban "${selectedCompany.name}"? Nó sẽ bị ẩn khỏi thanh bên.`
); );
if (!confirmed) return; if (!confirmed) return;
const nextCompanyId = const nextCompanyId =
...@@ -574,16 +573,16 @@ export function CompanySettings() { ...@@ -574,16 +573,16 @@ export function CompanySettings() {
}} }}
> >
{archiveMutation.isPending {archiveMutation.isPending
? "Archiving..." ? "Đang lưu trữ..."
: selectedCompany.status === "archived" : selectedCompany.status === "archived"
? "Already archived" ? "Đã lưu trữ"
: "Archive company"} : "Lưu trữ phòng ban"}
</Button> </Button>
{archiveMutation.isError && ( {archiveMutation.isError && (
<span className="text-xs text-destructive"> <span className="text-xs text-destructive">
{archiveMutation.error instanceof Error {archiveMutation.error instanceof Error
? archiveMutation.error.message ? archiveMutation.error.message
: "Failed to archive company"} : "Lưu trữ phòng ban thất bại"}
</span> </span>
)} )}
</div> </div>
......
...@@ -797,8 +797,8 @@ export function CompanySkills() { ...@@ -797,8 +797,8 @@ export function CompanySkills() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: "Skills", href: "/skills" }, { label: "Kỹ năng", href: "/skills" },
...(routeSkillId ? [{ label: "Detail" }] : []), ...(routeSkillId ? [{ label: "Chi tiết" }] : []),
]); ]);
}, [routeSkillId, setBreadcrumbs]); }, [routeSkillId, setBreadcrumbs]);
...@@ -1080,7 +1080,7 @@ export function CompanySkills() { ...@@ -1080,7 +1080,7 @@ export function CompanySkills() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Boxes} message="Select a company to manage skills." />; return <EmptyState icon={Boxes} message="Vui lòng chọn phòng ban để quản lý kỹ năng." />;
} }
function handleAddSkillSource() { function handleAddSkillSource() {
...@@ -1097,9 +1097,9 @@ export function CompanySkills() { ...@@ -1097,9 +1097,9 @@ export function CompanySkills() {
<Dialog open={deleteOpen} onOpenChange={closeDeleteDialog}> <Dialog open={deleteOpen} onOpenChange={closeDeleteDialog}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Remove skill</DialogTitle> <DialogTitle>Xóa kỹ năng</DialogTitle>
<DialogDescription> <DialogDescription>
Remove this skill from the company library. If any agents still use it, removal will be blocked until it is detached. Xóa kỹ năng này khỏi thư viện của phòng ban. Nếu có agent nào đang sử dụng kỹ năng này, việc xóa sẽ bị chặn cho đến khi được gỡ liên kết.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
...@@ -1189,9 +1189,9 @@ export function CompanySkills() { ...@@ -1189,9 +1189,9 @@ export function CompanySkills() {
<div className="border-b border-border px-4 py-3"> <div className="border-b border-border px-4 py-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div> <div>
<h1 className="text-base font-semibold">Skills</h1> <h1 className="text-base font-semibold">Kỹ năng</h1>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{skillsQuery.data?.length ?? 0} available {skillsQuery.data?.length ?? 0} kỹ năng
</p> </p>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
...@@ -1215,7 +1215,7 @@ export function CompanySkills() { ...@@ -1215,7 +1215,7 @@ export function CompanySkills() {
<input <input
value={skillFilter} value={skillFilter}
onChange={(event) => setSkillFilter(event.target.value)} onChange={(event) => setSkillFilter(event.target.value)}
placeholder="Filter skills" placeholder="Lọc kỹ năng..."
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground" className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/> />
</div> </div>
......
...@@ -168,7 +168,7 @@ export function Costs() { ...@@ -168,7 +168,7 @@ export function Costs() {
} = useDateRange(); } = useDateRange();
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Costs" }]); setBreadcrumbs([{ label: "Chi phí" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const [today, setToday] = useState(() => new Date().toDateString()); const [today, setToday] = useState(() => new Date().toDateString());
...@@ -529,7 +529,7 @@ export function Costs() { ...@@ -529,7 +529,7 @@ export function Costs() {
}), [budgetPolicies]); }), [budgetPolicies]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={DollarSign} message="Select a company to view costs." />; return <EmptyState icon={DollarSign} message="Chọn phòng ban để xem chi phí." />;
} }
const showCustomPrompt = preset === "custom" && !customReady; const showCustomPrompt = preset === "custom" && !customReady;
......
...@@ -50,7 +50,7 @@ export function Dashboard() { ...@@ -50,7 +50,7 @@ export function Dashboard() {
}); });
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Dashboard" }]); setBreadcrumbs([{ label: "Bảng điều khiển" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
...@@ -176,14 +176,14 @@ export function Dashboard() { ...@@ -176,14 +176,14 @@ export function Dashboard() {
return ( return (
<EmptyState <EmptyState
icon={LayoutDashboard} icon={LayoutDashboard}
message="Welcome to Paperclip. Set up your first company and agent to get started." message="Chào mừng đến với Canifa Agent. Hãy thiết lập phòng ban và tác nhân đầu tiên của bạn để bắt đầu."
action="Get Started" action="Bắt đầu"
onAction={openOnboarding} onAction={openOnboarding}
/> />
); );
} }
return ( return (
<EmptyState icon={LayoutDashboard} message="Create or select a company to view the dashboard." /> <EmptyState icon={LayoutDashboard} message="Vui lòng tạo hoặc chọn phòng ban để xem bảng điều khiển." />
); );
} }
...@@ -202,14 +202,14 @@ export function Dashboard() { ...@@ -202,14 +202,14 @@ export function Dashboard() {
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Bot className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" /> <Bot className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />
<p className="text-sm text-amber-900 dark:text-amber-100"> <p className="text-sm text-amber-900 dark:text-amber-100">
You have no agents. Bạn chưa có tác nhân nào.
</p> </p>
</div> </div>
<button <button
onClick={() => openOnboarding({ initialStep: 2, companyId: selectedCompanyId! })} onClick={() => openOnboarding({ initialStep: 2, companyId: selectedCompanyId! })}
className="text-sm font-medium text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100 underline underline-offset-2 shrink-0" className="text-sm font-medium text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100 underline underline-offset-2 shrink-0"
> >
Create one here Tạo tác nhân tại đây
</button> </button>
</div> </div>
)} )}
...@@ -241,7 +241,7 @@ export function Dashboard() { ...@@ -241,7 +241,7 @@ export function Dashboard() {
<MetricCard <MetricCard
icon={Bot} icon={Bot}
value={data.agents.active + data.agents.running + data.agents.paused + data.agents.error} value={data.agents.active + data.agents.running + data.agents.paused + data.agents.error}
label="Agents Enabled" label="Tác nhân hoạt động"
to="/agents" to="/agents"
description={ description={
<span> <span>
...@@ -254,7 +254,7 @@ export function Dashboard() { ...@@ -254,7 +254,7 @@ export function Dashboard() {
<MetricCard <MetricCard
icon={CircleDot} icon={CircleDot}
value={data.tasks.inProgress} value={data.tasks.inProgress}
label="Tasks In Progress" label="Nhiệm vụ đang thực hiện"
to="/issues" to="/issues"
description={ description={
<span> <span>
...@@ -266,7 +266,7 @@ export function Dashboard() { ...@@ -266,7 +266,7 @@ export function Dashboard() {
<MetricCard <MetricCard
icon={DollarSign} icon={DollarSign}
value={formatCents(data.costs.monthSpendCents)} value={formatCents(data.costs.monthSpendCents)}
label="Month Spend" label="Chi tiêu trong tháng"
to="/costs" to="/costs"
description={ description={
<span> <span>
...@@ -279,7 +279,7 @@ export function Dashboard() { ...@@ -279,7 +279,7 @@ export function Dashboard() {
<MetricCard <MetricCard
icon={ShieldCheck} icon={ShieldCheck}
value={data.pendingApprovals + data.budgets.pendingApprovals} value={data.pendingApprovals + data.budgets.pendingApprovals}
label="Pending Approvals" label="Đang chờ duyệt"
to="/approvals" to="/approvals"
description={ description={
<span> <span>
...@@ -318,7 +318,7 @@ export function Dashboard() { ...@@ -318,7 +318,7 @@ export function Dashboard() {
{recentActivity.length > 0 && ( {recentActivity.length > 0 && (
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3"> <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Recent Activity Hoạt động gần đây
</h3> </h3>
<div className="border border-border divide-y divide-border overflow-hidden"> <div className="border border-border divide-y divide-border overflow-hidden">
{recentActivity.map((event) => ( {recentActivity.map((event) => (
...@@ -339,7 +339,7 @@ export function Dashboard() { ...@@ -339,7 +339,7 @@ export function Dashboard() {
{/* Recent Tasks */} {/* Recent Tasks */}
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3"> <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Recent Tasks Nhiệm vụ gần đây
</h3> </h3>
{recentIssues.length === 0 ? ( {recentIssues.length === 0 ? (
<div className="border border-border p-4"> <div className="border border-border p-4">
......
...@@ -14,8 +14,8 @@ export function DashboardLive() { ...@@ -14,8 +14,8 @@ export function DashboardLive() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: "Dashboard", href: "/dashboard" }, { label: "Bảng điều khiển", href: "/dashboard" },
{ label: "Live runs" }, { label: "Chạy trực tiếp" },
]); ]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
...@@ -23,7 +23,7 @@ export function DashboardLive() { ...@@ -23,7 +23,7 @@ export function DashboardLive() {
return ( return (
<EmptyState <EmptyState
icon={RadioTower} icon={RadioTower}
message={companies.length === 0 ? "Create a company to view live runs." : "Select a company to view live runs."} message={companies.length === 0 ? "Vui lòng tạo phòng ban để xem các phiên chạy trực tiếp." : "Chọn phòng ban để xem các phiên chạy trực tiếp."}
/> />
); );
} }
...@@ -37,14 +37,14 @@ export function DashboardLive() { ...@@ -37,14 +37,14 @@ export function DashboardLive() {
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
> >
<ArrowLeft className="h-3.5 w-3.5" /> <ArrowLeft className="h-3.5 w-3.5" />
Dashboard Bảng điều khiển
</Link> </Link>
<h1 className="mt-2 text-2xl font-semibold tracking-normal text-foreground">Live agent runs</h1> <h1 className="mt-2 text-2xl font-semibold tracking-normal text-foreground">Tác nhân chạy trực tiếp</h1>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
Active runs first, followed by the most recent completed runs. Các phiên đang hoạt động hiển thị trước, theo sau là các phiên hoàn thành gần đây nhất.
</p> </p>
</div> </div>
<div className="text-sm text-muted-foreground">Showing up to {DASHBOARD_LIVE_RUN_LIMIT}</div> <div className="text-sm text-muted-foreground">Hiển thị tối đa {DASHBOARD_LIVE_RUN_LIMIT}</div>
</div> </div>
<ActiveAgentsPanel <ActiveAgentsPanel
...@@ -55,7 +55,7 @@ export function DashboardLive() { ...@@ -55,7 +55,7 @@ export function DashboardLive() {
cardLimit={DASHBOARD_LIVE_RUN_LIMIT} cardLimit={DASHBOARD_LIVE_RUN_LIMIT}
gridClassName="gap-3 md:grid-cols-2 2xl:grid-cols-3" gridClassName="gap-3 md:grid-cols-2 2xl:grid-cols-3"
cardClassName="h-[420px]" cardClassName="h-[420px]"
emptyMessage="No active or recent agent runs." emptyMessage="Không có phiên chạy tác nhân nào đang hoạt động hoặc gần đây."
queryScope="dashboard-live" queryScope="dashboard-live"
showMoreLink={false} showMoreLink={false}
/> />
......
...@@ -17,7 +17,7 @@ export function Goals() { ...@@ -17,7 +17,7 @@ export function Goals() {
const { setBreadcrumbs } = useBreadcrumbs(); const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Goals" }]); setBreadcrumbs([{ label: "Mục tiêu" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data: goals, isLoading, error } = useQuery({ const { data: goals, isLoading, error } = useQuery({
...@@ -27,7 +27,7 @@ export function Goals() { ...@@ -27,7 +27,7 @@ export function Goals() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Target} message="Select a company to view goals." />; return <EmptyState icon={Target} message="Chọn phòng ban để xem mục tiêu." />;
} }
if (isLoading) { if (isLoading) {
...@@ -41,8 +41,8 @@ export function Goals() { ...@@ -41,8 +41,8 @@ export function Goals() {
{goals && goals.length === 0 && ( {goals && goals.length === 0 && (
<EmptyState <EmptyState
icon={Target} icon={Target}
message="No goals yet." message="Chưa có mục tiêu nào."
action="Add Goal" action="Thêm mục tiêu"
onAction={() => openNewGoal()} onAction={() => openNewGoal()}
/> />
)} )}
...@@ -52,7 +52,7 @@ export function Goals() { ...@@ -52,7 +52,7 @@ export function Goals() {
<div className="flex items-center justify-start"> <div className="flex items-center justify-start">
<Button size="sm" variant="outline" onClick={() => openNewGoal()}> <Button size="sm" variant="outline" onClick={() => openNewGoal()}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> <Plus className="h-3.5 w-3.5 mr-1.5" />
New Goal Mục tiêu mới
</Button> </Button>
</div> </div>
<GoalTree goals={goals} goalLink={(goal) => `/goals/${goal.id}`} /> <GoalTree goals={goals} goalLink={(goal) => `/goals/${goal.id}`} />
......
...@@ -1866,7 +1866,7 @@ export function Inbox() { ...@@ -1866,7 +1866,7 @@ export function Inbox() {
}, [selectedIndex]); }, [selectedIndex]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />; return <EmptyState icon={InboxIcon} message="Chọn phòng ban để xem hộp thư." />;
} }
const hasRunFailures = failedRuns.length > 0; const hasRunFailures = failedRuns.length > 0;
...@@ -1954,15 +1954,15 @@ export function Inbox() { ...@@ -1954,15 +1954,15 @@ export function Inbox() {
items={[ items={[
{ {
value: "mine", value: "mine",
label: "Mine", label: "Của tôi",
}, },
{ {
value: "recent", value: "recent",
label: "Recent", label: "Gần đây",
}, },
{ value: "unread", label: "Unread" }, { value: "unread", label: "Chưa đọc" },
{ value: "blocked", label: "Blocked" }, { value: "blocked", label: "Bị chặn" },
{ value: "all", label: "All" }, { value: "all", label: "Tất cả" },
]} ]}
/> />
</Tabs> </Tabs>
......
...@@ -108,7 +108,7 @@ export function Issues() { ...@@ -108,7 +108,7 @@ export function Issues() {
const issueLinkState = useMemo( const issueLinkState = useMemo(
() => () =>
createIssueDetailLocationState( createIssueDetailLocationState(
"Issues", "Vấn đề",
`${location.pathname}${location.search}${location.hash}`, `${location.pathname}${location.search}${location.hash}`,
"issues", "issues",
), ),
...@@ -116,7 +116,7 @@ export function Issues() { ...@@ -116,7 +116,7 @@ export function Issues() {
); );
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Issues" }]); setBreadcrumbs([{ label: "Vấn đề" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE; const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE;
...@@ -173,7 +173,7 @@ export function Issues() { ...@@ -173,7 +173,7 @@ export function Issues() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={CircleDot} message="Select a company to view issues." />; return <EmptyState icon={CircleDot} message="Chọn phòng ban để xem danh sách vấn đề." />;
} }
return ( return (
......
...@@ -20,9 +20,9 @@ export function JoinRequestQueue() { ...@@ -20,9 +20,9 @@ export function JoinRequestQueue() {
useEffect(() => { useEffect(() => {
setBreadcrumbs([ setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Inbox", href: "/inbox" }, { label: "Hộp thư", href: "/inbox" },
{ label: "Join Requests" }, { label: "Yêu cầu tham gia" },
]); ]);
}, [selectedCompany?.name, setBreadcrumbs]); }, [selectedCompany?.name, setBreadcrumbs]);
...@@ -56,11 +56,11 @@ export function JoinRequestQueue() { ...@@ -56,11 +56,11 @@ export function JoinRequestQueue() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to review join requests.</div>; return <div className="text-sm text-muted-foreground">Chọn phòng ban để xem yêu cầu tham gia.</div>;
} }
if (requestsQuery.isLoading) { if (requestsQuery.isLoading) {
return <div className="text-sm text-muted-foreground">Loading join requests…</div>; return <div className="text-sm text-muted-foreground">Đang tải yêu cầu tham gia...</div>;
} }
if (requestsQuery.error) { if (requestsQuery.error) {
...@@ -78,16 +78,16 @@ export function JoinRequestQueue() { ...@@ -78,16 +78,16 @@ export function JoinRequestQueue() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserPlus2 className="h-5 w-5 text-muted-foreground" /> <UserPlus2 className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Join Request Queue</h1> <h1 className="text-lg font-semibold">Hàng đợi yêu cầu tham gia</h1>
</div> </div>
<p className="max-w-3xl text-sm text-muted-foreground"> <p className="max-w-3xl text-sm text-muted-foreground">
Review human and agent join requests outside the mixed inbox feed. This queue uses the same approval mutations as the inline inbox cards. Xem xét các yêu cầu tham gia của con người và tác nhân bên ngoài nguồn cấp dữ liệu hộp thư hỗn hợp. Hàng đợi này sử dụng các đột biến phê duyệt giống như các thẻ hộp thư nội dòng.
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-3 rounded-xl border border-border bg-card p-4"> <div className="flex flex-wrap gap-3 rounded-xl border border-border bg-card p-4">
<label className="space-y-2 text-sm"> <label className="space-y-2 text-sm">
<span className="font-medium">Status</span> <span className="font-medium">Trạng thái</span>
<select <select
className="rounded-md border border-border bg-background px-3 py-2" className="rounded-md border border-border bg-background px-3 py-2"
value={status} value={status}
...@@ -95,13 +95,13 @@ export function JoinRequestQueue() { ...@@ -95,13 +95,13 @@ export function JoinRequestQueue() {
setStatus(event.target.value as "pending_approval" | "approved" | "rejected") setStatus(event.target.value as "pending_approval" | "approved" | "rejected")
} }
> >
<option value="pending_approval">Pending approval</option> <option value="pending_approval">Đang chờ duyệt</option>
<option value="approved">Approved</option> <option value="approved">Đã duyệt</option>
<option value="rejected">Rejected</option> <option value="rejected">Đã từ chối</option>
</select> </select>
</label> </label>
<label className="space-y-2 text-sm"> <label className="space-y-2 text-sm">
<span className="font-medium">Request type</span> <span className="font-medium">Loại yêu cầu</span>
<select <select
className="rounded-md border border-border bg-background px-3 py-2" className="rounded-md border border-border bg-background px-3 py-2"
value={requestType} value={requestType}
...@@ -109,9 +109,9 @@ export function JoinRequestQueue() { ...@@ -109,9 +109,9 @@ export function JoinRequestQueue() {
setRequestType(event.target.value as "all" | "human" | "agent") setRequestType(event.target.value as "all" | "human" | "agent")
} }
> >
<option value="all">All</option> <option value="all">Tất cả</option>
<option value="human">Human</option> <option value="human">Người</option>
<option value="agent">Agent</option> <option value="agent">Tác nhân</option>
</select> </select>
</label> </label>
</div> </div>
...@@ -119,7 +119,7 @@ export function JoinRequestQueue() { ...@@ -119,7 +119,7 @@ export function JoinRequestQueue() {
<div className="space-y-4"> <div className="space-y-4">
{(requestsQuery.data ?? []).length === 0 ? ( {(requestsQuery.data ?? []).length === 0 ? (
<div className="rounded-xl border border-dashed border-border px-4 py-8 text-sm text-muted-foreground"> <div className="rounded-xl border border-dashed border-border px-4 py-8 text-sm text-muted-foreground">
No join requests match the current filters. Không có yêu cầu tham gia nào khớp với bộ lọc hiện tại.
</div> </div>
) : ( ) : (
requestsQuery.data!.map((request) => ( requestsQuery.data!.map((request) => (
...@@ -154,13 +154,13 @@ export function JoinRequestQueue() { ...@@ -154,13 +154,13 @@ export function JoinRequestQueue() {
onClick={() => rejectMutation.mutate(request.id)} onClick={() => rejectMutation.mutate(request.id)}
disabled={rejectMutation.isPending} disabled={rejectMutation.isPending}
> >
Reject Từ chối
</Button> </Button>
<Button <Button
onClick={() => approveMutation.mutate(request.id)} onClick={() => approveMutation.mutate(request.id)}
disabled={approveMutation.isPending} disabled={approveMutation.isPending}
> >
Approve Phê duyệt
</Button> </Button>
</div> </div>
) : null} ) : null}
......
...@@ -17,7 +17,7 @@ export function MyIssues() { ...@@ -17,7 +17,7 @@ export function MyIssues() {
const { setBreadcrumbs } = useBreadcrumbs(); const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "My Issues" }]); setBreadcrumbs([{ label: "Vấn đề của tôi" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data: issues, isLoading, error } = useQuery({ const { data: issues, isLoading, error } = useQuery({
...@@ -27,7 +27,7 @@ export function MyIssues() { ...@@ -27,7 +27,7 @@ export function MyIssues() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />; return <EmptyState icon={ListTodo} message="Chọn phòng ban để xem vấn đề của bạn." />;
} }
if (isLoading) { if (isLoading) {
...@@ -44,7 +44,7 @@ export function MyIssues() { ...@@ -44,7 +44,7 @@ export function MyIssues() {
{error && <p className="text-sm text-destructive">{error.message}</p>} {error && <p className="text-sm text-destructive">{error.message}</p>}
{myIssues.length === 0 && ( {myIssues.length === 0 && (
<EmptyState icon={ListTodo} message="No issues assigned to you." /> <EmptyState icon={ListTodo} message="Không có vấn đề nào được giao cho bạn." />
)} )}
{myIssues.length > 0 && ( {myIssues.length > 0 && (
......
...@@ -94,7 +94,7 @@ export function Org() { ...@@ -94,7 +94,7 @@ export function Org() {
const { setBreadcrumbs } = useBreadcrumbs(); const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Org Chart" }]); setBreadcrumbs([{ label: "Sơ đồ tổ chức" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
...@@ -104,7 +104,7 @@ export function Org() { ...@@ -104,7 +104,7 @@ export function Org() {
}); });
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={GitBranch} message="Select a company to view org chart." />; return <EmptyState icon={GitBranch} message="Chọn phòng ban để xem sơ đồ tổ chức." />;
} }
if (isLoading) { if (isLoading) {
...@@ -118,7 +118,7 @@ export function Org() { ...@@ -118,7 +118,7 @@ export function Org() {
{data && data.length === 0 && ( {data && data.length === 0 && (
<EmptyState <EmptyState
icon={GitBranch} icon={GitBranch}
message="No agents in the organization. Create agents to build your org chart." message="Không có tác nhân nào trong phòng ban. Hãy tạo các tác nhân để xây dựng sơ đồ tổ chức."
/> />
)} )}
......
...@@ -194,7 +194,7 @@ export function OrgChart() { ...@@ -194,7 +194,7 @@ export function OrgChart() {
}, [agents]); }, [agents]);
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Org Chart" }]); setBreadcrumbs([{ label: "Sơ đồ tổ chức" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
// Layout computation // Layout computation
...@@ -429,7 +429,7 @@ export function OrgChart() { ...@@ -429,7 +429,7 @@ export function OrgChart() {
}, [pan, zoom]); }, [pan, zoom]);
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Network} message="Select a company to view the org chart." />; return <EmptyState icon={Network} message="Chọn phòng ban để xem sơ đồ tổ chức." />;
} }
if (isLoading) { if (isLoading) {
...@@ -437,7 +437,7 @@ export function OrgChart() { ...@@ -437,7 +437,7 @@ export function OrgChart() {
} }
if (orgTree && orgTree.length === 0) { if (orgTree && orgTree.length === 0) {
return <EmptyState icon={Network} message="No organizational hierarchy defined." />; return <EmptyState icon={Network} message="Chưa định nghĩa sơ đồ tổ chức." />;
} }
return ( return (
...@@ -446,13 +446,13 @@ export function OrgChart() { ...@@ -446,13 +446,13 @@ export function OrgChart() {
<Link to="/company/import"> <Link to="/company/import">
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Upload className="mr-1.5 h-3.5 w-3.5" /> <Upload className="mr-1.5 h-3.5 w-3.5" />
Import company Nhập phòng ban
</Button> </Button>
</Link> </Link>
<Link to="/company/export"> <Link to="/company/export">
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Download className="mr-1.5 h-3.5 w-3.5" /> <Download className="mr-1.5 h-3.5 w-3.5" />
Export company Xuất phòng ban
</Button> </Button>
</Link> </Link>
</div> </div>
......
...@@ -130,13 +130,13 @@ export function PluginPage() { ...@@ -130,13 +130,13 @@ export function PluginPage() {
} }
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-muted-foreground">Select a company to view this page.</p> <p className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để xem trang này.</p>
</div> </div>
); );
} }
if (!contributions) { if (!contributions) {
return <div className="text-sm text-muted-foreground">Loading…</div>; return <div className="text-sm text-muted-foreground">Đang tải...</div>;
} }
if (!pluginId && pluginRoutePath) { if (!pluginId && pluginRoutePath) {
...@@ -168,7 +168,7 @@ export function PluginPage() { ...@@ -168,7 +168,7 @@ export function PluginPage() {
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" asChild>
<Link to={companyPrefix ? `/${companyPrefix}/dashboard` : "/dashboard"}> <Link to={companyPrefix ? `/${companyPrefix}/dashboard` : "/dashboard"}>
<ArrowLeft className="h-4 w-4 mr-1" /> <ArrowLeft className="h-4 w-4 mr-1" />
Back Quay lại
</Link> </Link>
</Button> </Button>
</div> </div>
......
...@@ -19,7 +19,7 @@ export function Projects() { ...@@ -19,7 +19,7 @@ export function Projects() {
const { setBreadcrumbs } = useBreadcrumbs(); const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Projects" }]); setBreadcrumbs([{ label: "Dự án" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const { data: allProjects, isLoading, error } = useQuery({ const { data: allProjects, isLoading, error } = useQuery({
...@@ -33,7 +33,7 @@ export function Projects() { ...@@ -33,7 +33,7 @@ export function Projects() {
); );
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Hexagon} message="Select a company to view projects." />; return <EmptyState icon={Hexagon} message="Chọn phòng ban để xem danh sách dự án." />;
} }
if (isLoading) { if (isLoading) {
...@@ -45,7 +45,7 @@ export function Projects() { ...@@ -45,7 +45,7 @@ export function Projects() {
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<Button size="sm" variant="outline" onClick={openNewProject}> <Button size="sm" variant="outline" onClick={openNewProject}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Project Thêm dự án
</Button> </Button>
</div> </div>
...@@ -54,8 +54,8 @@ export function Projects() { ...@@ -54,8 +54,8 @@ export function Projects() {
{!isLoading && projects.length === 0 && ( {!isLoading && projects.length === 0 && (
<EmptyState <EmptyState
icon={Hexagon} icon={Hexagon}
message="No projects yet." message="Chưa có dự án nào."
action="Add Project" action="Thêm dự án"
onAction={openNewProject} onAction={openNewProject}
/> />
)} )}
......
...@@ -241,7 +241,7 @@ export function Routines() { ...@@ -241,7 +241,7 @@ export function Routines() {
const [routineViewState, setRoutineViewState] = useState<RoutineViewState>(() => getRoutineViewState(routineViewStateKey)); const [routineViewState, setRoutineViewState] = useState<RoutineViewState>(() => getRoutineViewState(routineViewStateKey));
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Routines" }]); setBreadcrumbs([{ label: "Quy trình" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
useEffect(() => { useEffect(() => {
...@@ -479,7 +479,7 @@ export function Routines() { ...@@ -479,7 +479,7 @@ export function Routines() {
} }
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={Repeat} message="Select a company to view routines." />; return <EmptyState icon={Repeat} message="Chọn phòng ban để xem các quy trình." />;
} }
if (isLoading) { if (isLoading) {
...@@ -491,7 +491,7 @@ export function Routines() { ...@@ -491,7 +491,7 @@ export function Routines() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-1"> <div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight"> <h1 className="text-2xl font-semibold tracking-tight">
Routines Quy trình
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Recurring work definitions that materialize into auditable execution issues. Recurring work definitions that materialize into auditable execution issues.
...@@ -509,8 +509,8 @@ export function Routines() { ...@@ -509,8 +509,8 @@ export function Routines() {
value={activeTab} value={activeTab}
onValueChange={handleTabChange} onValueChange={handleTabChange}
items={[ items={[
{ value: "routines", label: "Routines" }, { value: "routines", label: "Quy trình" },
{ value: "runs", label: "Recent Runs" }, { value: "runs", label: "Phiên chạy gần đây" },
]} ]}
/> />
<TabsContent value="routines" className="space-y-4"> <TabsContent value="routines" className="space-y-4">
...@@ -877,7 +877,7 @@ export function Routines() { ...@@ -877,7 +877,7 @@ export function Routines() {
<div className="py-12"> <div className="py-12">
<EmptyState <EmptyState
icon={Repeat} icon={Repeat}
message="No routines yet. Use Create routine to define the first recurring workflow." message="Chưa có quy trình nào. Hãy sử dụng Tạo quy trình để tạo quy trình làm việc định kỳ đầu tiên."
/> />
</div> </div>
) : ( ) : (
......
...@@ -376,7 +376,7 @@ export function Secrets() { ...@@ -376,7 +376,7 @@ export function Secrets() {
const [vaultError, setVaultError] = useState<string | null>(null); const [vaultError, setVaultError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Secrets" }]); setBreadcrumbs([{ label: "Bí mật" }]);
}, [setBreadcrumbs]); }, [setBreadcrumbs]);
const secretsQuery = useQuery({ const secretsQuery = useQuery({
...@@ -747,7 +747,7 @@ export function Secrets() { ...@@ -747,7 +747,7 @@ export function Secrets() {
if (!selectedCompanyId) { if (!selectedCompanyId) {
return ( return (
<div className="p-6 text-sm text-muted-foreground">Select a company to manage secrets.</div> <div className="p-6 text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý bí mật.</div>
); );
} }
...@@ -755,7 +755,7 @@ export function Secrets() { ...@@ -755,7 +755,7 @@ export function Secrets() {
<div className="flex h-full min-h-0 flex-col gap-4"> <div className="flex h-full min-h-0 flex-col gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<KeyRound className="h-5 w-5 text-muted-foreground" /> <KeyRound className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Secrets</h1> <h1 className="text-lg font-semibold">Bí mật</h1>
</div> </div>
<Tabs <Tabs
...@@ -765,8 +765,8 @@ export function Secrets() { ...@@ -765,8 +765,8 @@ export function Secrets() {
> >
<PageTabBar <PageTabBar
items={[ items={[
{ value: "secrets", label: "Secrets" }, { value: "secrets", label: "Bí mật phòng ban" },
{ value: "vaults", label: "Provider vaults" }, { value: "vaults", label: "Kho khóa (Vaults)" },
]} ]}
align="start" align="start"
value={activeTab} value={activeTab}
...@@ -781,9 +781,9 @@ export function Secrets() { ...@@ -781,9 +781,9 @@ export function Secrets() {
<Input <Input
value={search} value={search}
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
placeholder="Search by name, key, ref" placeholder="Tìm kiếm theo tên, khóa, tham chiếu..."
className="pl-7 text-xs sm:text-sm" className="pl-7 text-xs sm:text-sm"
aria-label="Search secrets" aria-label="Tìm kiếm bí mật"
data-page-search-target="true" data-page-search-target="true"
/> />
</div> </div>
...@@ -802,32 +802,32 @@ export function Secrets() { ...@@ -802,32 +802,32 @@ export function Secrets() {
className="ml-auto" className="ml-auto"
/> />
<Button onClick={() => setCreateOpen(true)} size="sm"> <Button onClick={() => setCreateOpen(true)} size="sm">
<Plus className="h-3.5 w-3.5 mr-1" /> New secret <Plus className="h-3.5 w-3.5 mr-1" /> Thêm bí mật
</Button> </Button>
</div> </div>
<div className="min-h-0 flex-1 overflow-y-auto"> <div className="min-h-0 flex-1 overflow-y-auto">
{secretsQuery.isError ? ( {secretsQuery.isError ? (
<div className="text-sm text-destructive flex items-center gap-2 py-4"> <div className="text-sm text-destructive flex items-center gap-2 py-4">
<AlertCircle className="h-4 w-4" /> Failed to load secrets:{" "} <AlertCircle className="h-4 w-4" /> Không thể tải bí mật:{" "}
{(secretsQuery.error as Error).message} {(secretsQuery.error as Error).message}
<Button variant="ghost" size="sm" onClick={() => secretsQuery.refetch()}> <Button variant="ghost" size="sm" onClick={() => secretsQuery.refetch()}>
Retry Thử lại
</Button> </Button>
</div> </div>
) : secrets.length === 0 && !secretsQuery.isPending ? ( ) : secrets.length === 0 && !secretsQuery.isPending ? (
<EmptyState <EmptyState
icon={KeyRound} icon={KeyRound}
message="No secrets yet. Create your first managed secret or link an external reference." message="Chưa có bí mật nào. Hãy tạo bí mật được quản lý đầu tiên hoặc liên kết tới một tham chiếu bên ngoài."
action="New secret" action="Thêm bí mật"
onAction={() => setCreateOpen(true)} onAction={() => setCreateOpen(true)}
/> />
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<EmptyState icon={Search} message="No secrets match your filters." /> <EmptyState icon={Search} message="Không tìm thấy bí mật nào khớp với bộ lọc." />
) : ( ) : (
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="bg-muted/40 text-xs uppercase tracking-wide text-muted-foreground"> <thead className="bg-muted/40 text-xs uppercase tracking-wide text-muted-foreground">
<tr> <tr>
<th className="px-3 py-2 text-left font-medium">Name</th> <th className="px-3 py-2 text-left font-medium">Tên</th>
<th className="px-2 py-2 text-left font-medium">Mode</th> <th className="px-2 py-2 text-left font-medium">Mode</th>
<th className="px-2 py-2 text-left font-medium">Provider</th> <th className="px-2 py-2 text-left font-medium">Provider</th>
<th className="px-2 py-2 text-left font-medium">Status</th> <th className="px-2 py-2 text-left font-medium">Status</th>
......
...@@ -207,7 +207,7 @@ export function UserProfile() { ...@@ -207,7 +207,7 @@ export function UserProfile() {
}); });
useEffect(() => { useEffect(() => {
setBreadcrumbs([{ label: "Users" }, { label: data?.user.name ?? userSlug }]); setBreadcrumbs([{ label: "Thành viên" }, { label: data?.user.name ?? userSlug }]);
}, [data?.user.name, setBreadcrumbs, userSlug]); }, [data?.user.name, setBreadcrumbs, userSlug]);
const allTime = data?.stats.find((entry) => entry.key === "all"); const allTime = data?.stats.find((entry) => entry.key === "all");
...@@ -243,7 +243,7 @@ export function UserProfile() { ...@@ -243,7 +243,7 @@ export function UserProfile() {
); );
if (!selectedCompanyId) { if (!selectedCompanyId) {
return <EmptyState icon={UserRound} message="Select a company to view user profiles." />; return <EmptyState icon={UserRound} message="Chọn phòng ban để xem thông tin cá nhân." />;
} }
if (isLoading) { if (isLoading) {
...@@ -251,7 +251,7 @@ export function UserProfile() { ...@@ -251,7 +251,7 @@ export function UserProfile() {
} }
if (error || !data) { if (error || !data) {
return <EmptyState icon={AlertCircle} message="User profile not found for this company." />; return <EmptyState icon={AlertCircle} message="Không tìm thấy thông tin thành viên trong phòng ban này." />;
} }
const allTimeTokens = allTime ? totalTokens(allTime) : 0; const allTimeTokens = allTime ? totalTokens(allTime) : 0;
......
...@@ -17,5 +17,6 @@ ...@@ -17,5 +17,6 @@
"lexical": ["./node_modules/lexical/index.d.ts"] "lexical": ["./node_modules/lexical/index.d.ts"]
} }
}, },
"include": ["src", "storybook"] "include": ["src", "storybook"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.spec.ts", "src/**/*.spec.tsx"]
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment