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() {
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" />
<span className="truncate">{selectedCompany?.name ?? "Company"}</span>
<span className="truncate">{selectedCompany?.name ?? "Phòng ban"}</span>
</Link>
<div className="flex items-center gap-2 px-2 py-1">
<Settings className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="flex-1 truncate text-sm font-bold text-foreground">
Company Settings
Thiết lập phòng ban
</span>
</div>
</div>
<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">
<SidebarNavItem to="/company/settings" label="General" icon={SlidersHorizontal} end />
<SidebarNavItem to="/company/settings" label="Chung" icon={SlidersHorizontal} end />
<SidebarNavItem
to="/company/settings/environments"
label="Environments"
label="Môi trường"
icon={MonitorCog}
end
/>
<SidebarNavItem
to="/company/settings/access"
label="Access"
label="Truy cập"
icon={Shield}
badge={badges?.joinRequests ?? 0}
end
/>
<SidebarNavItem to="/company/settings/invites" label="Invites" icon={MailPlus} end />
<SidebarNavItem to="/company/settings/secrets" label="Secrets" icon={KeyRound} end />
<SidebarNavItem to="/company/settings/invites" label="Lời mời" icon={MailPlus} end />
<SidebarNavItem to="/company/settings/secrets" label="Bí mật" icon={KeyRound} end />
</div>
</nav>
</aside>
......
......@@ -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"
>
<SquarePen className="h-4 w-4 shrink-0" />
<span className="truncate">New Issue</span>
<span className="truncate">Vấn đề mới</span>
</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
to="/inbox"
label="Inbox"
label="Hộp thư"
icon={Inbox}
badge={inboxBadge.inbox}
badgeTone={inboxBadge.failedRuns > 0 ? "danger" : "default"}
......@@ -99,18 +99,18 @@ export function Sidebar() {
/>
</div>
<SidebarSection label="Work">
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} />
<SidebarSection label="Công việc">
<SidebarNavItem to="/issues" label="Vấn đề" icon={CircleDot} />
<SidebarNavItem to="/routines" label="Quy trình" icon={Repeat} />
<PluginLauncherOutlet
placementZones={["sidebar"]}
context={pluginContext}
className="flex flex-col gap-0.5"
itemClassName="text-[13px] font-medium"
/>
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
<SidebarNavItem to="/goals" label="Mục tiêu" icon={Target} />
{showWorkspacesLink ? (
<SidebarNavItem to="/workspaces" label="Workspaces" icon={GitBranch} />
<SidebarNavItem to="/workspaces" label="Không gian làm việc" icon={GitBranch} />
) : null}
</SidebarSection>
......@@ -118,12 +118,12 @@ export function Sidebar() {
<SidebarAgents />
<SidebarSection label="Company">
<SidebarNavItem to="/org" label="Org" icon={Network} />
<SidebarNavItem to="/skills" label="Skills" icon={Boxes} />
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Activity" icon={History} />
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} />
<SidebarSection label="Phòng ban">
<SidebarNavItem to="/org" label="Sơ đồ" icon={Network} />
<SidebarNavItem to="/skills" label="Kỹ năng" icon={Boxes} />
<SidebarNavItem to="/costs" label="Chi phí" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Hoạt động" icon={History} />
<SidebarNavItem to="/company/settings" label="Thiết lập" icon={Settings} />
</SidebarSection>
<PluginSlotOutlet
......
......@@ -225,12 +225,12 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
<Button
variant="ghost"
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">
{selectedCompany ? <WorkspaceIcon company={selectedCompany} /> : null}
<span className="truncate text-sm font-bold text-foreground">
{selectedCompany?.name ?? "Select workspace"}
{selectedCompany?.name ?? "Chọn phòng ban"}
</span>
</span>
<ChevronsUpDown className="size-3.5 shrink-0 text-muted-foreground" />
......@@ -239,7 +239,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
<DropdownMenuContent align="start" sideOffset={8} className="w-64 p-1">
<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">
Switch workspace
Chuyển phòng ban
</DropdownMenuLabel>
<button
type="button"
......@@ -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"
>
{isEditingOrder ? "Done" : "Edit"}
{isEditingOrder ? "Xong" : "Sắp xếp"}
</button>
</div>
<div className="max-h-96 overflow-y-auto">
......@@ -275,7 +275,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
</SortableContext>
</DndContext>
{orderedCompanies.length === 0 ? (
<DropdownMenuItem disabled>No workspaces</DropdownMenuItem>
<DropdownMenuItem disabled>Không có phòng ban</DropdownMenuItem>
) : null}
</div>
<DropdownMenuSeparator />
......@@ -285,7 +285,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
disabled={isEditingOrder}
>
<Plus className="size-4" />
<span>Add company...</span>
<span>Thêm phòng ban...</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild disabled={isEditingOrder}>
......@@ -301,7 +301,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
>
<UserPlus className="size-4" />
<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>
</Link>
</DropdownMenuItem>
......@@ -317,7 +317,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
}}
>
<Settings className="size-4" />
<span>Company settings</span>
<span>Thiết lập phòng ban</span>
</Link>
</DropdownMenuItem>
{session?.session ? (
......@@ -329,7 +329,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
disabled={isEditingOrder || signOutMutation.isPending}
>
<LogOut className="size-4" />
<span>{signOutMutation.isPending ? "Signing out..." : "Sign out"}</span>
<span>{signOutMutation.isPending ? "Đang đăng xuất..." : "Đăng xuất"}</span>
</DropdownMenuItem>
</>
) : null}
......
{
"app": {
"noCompanies": {
"title": "Create your first company",
"description": "Get started by creating a company.",
"newCompany": "New Company"
"title": "Create your first department",
"description": "Get started by creating a department.",
"newCompany": "New Department"
}
}
}
{
"app": {
"noCompanies": {
"title": "Tạo công ty đầu tiên của bạn",
"description": "Bắt đầu bằng cách tạo một công ty.",
"newCompany": "Công ty mới"
"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 phòng ban.",
"newCompany": "Phòng ban mới"
}
}
}
......@@ -38,45 +38,45 @@
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
}
:root {
color-scheme: light;
--radius: 0;
--radius: 0.5rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 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);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--accent: oklch(0.97 0.01 25.4);
--accent-foreground: oklch(0.205 0.01 25.4);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--ring: oklch(0.53 0.22 25.4);
--chart-1: oklch(0.53 0.22 25.4);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 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-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 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-fg: oklch(0.4 0.13 265);
--chip-match-title-border: oklch(0.85 0.05 265);
......@@ -92,38 +92,38 @@
}
.dark {
--background: oklch(0.145 0 0);
--background: oklch(0.13 0.01 25.4);
--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);
--popover: oklch(0.205 0 0);
--popover: oklch(0.17 0.01 25.4);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--primary: oklch(0.58 0.23 25.4);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.22 0.01 25.4);
--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);
--accent: oklch(0.269 0 0);
--accent: oklch(0.22 0.02 25.4);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--border: oklch(0.22 0.01 25.4);
--input: oklch(0.22 0.01 25.4);
--ring: oklch(0.58 0.23 25.4);
--chart-1: oklch(0.58 0.23 25.4);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--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-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-accent: oklch(0.269 0 0);
--sidebar-accent: oklch(0.22 0.02 25.4);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
--sidebar-border: oklch(0.22 0.01 25.4);
--sidebar-ring: oklch(0.58 0.23 25.4);
--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-border: oklch(0.4 0.06 265);
......
......@@ -49,7 +49,7 @@ export function Activity() {
const [filter, setFilter] = useState("all");
useEffect(() => {
setBreadcrumbs([{ label: "Activity" }]);
setBreadcrumbs([{ label: "Hoạt động" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -101,7 +101,7 @@ export function Activity() {
}, [data]);
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) {
......@@ -122,10 +122,10 @@ export function Activity() {
<div className="flex items-center justify-end">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-[140px] h-8 text-xs">
<SelectValue placeholder="Filter by type" />
<SelectValue placeholder="Lọc theo loại" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All types</SelectItem>
<SelectItem value="all">Tất cả</SelectItem>
{entityTypes.map((type) => (
<SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
......@@ -138,7 +138,7 @@ export function Activity() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{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 && (
......
......@@ -266,7 +266,7 @@ export function AdapterManager() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings", href: "/instance/settings/general" },
{ label: "Adapters" },
]);
......
......@@ -116,11 +116,11 @@ export function Agents() {
}, [agents]);
useEffect(() => {
setBreadcrumbs([{ label: "Agents" }]);
setBreadcrumbs([{ label: "Tác nhân" }]);
}, [setBreadcrumbs]);
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) {
......@@ -136,10 +136,10 @@ export function Agents() {
<Tabs value={tab} onValueChange={(v) => navigate(`/agents/${v}`)}>
<PageTabBar
items={[
{ value: "all", label: "All" },
{ value: "active", label: "Active" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "all", label: "Tất cả" },
{ value: "active", label: "Đang hoạt động" },
{ value: "paused", label: "Tạm dừng" },
{ value: "error", label: "Lỗi" },
]}
value={tab}
onValueChange={(v) => navigate(`/agents/${v}`)}
......@@ -171,7 +171,7 @@ export function Agents() {
)}>
{showTerminated && <span className="text-background text-[10px] leading-none">&#10003;</span>}
</span>
Show terminated
Hiển thị đã chấm dứt
</button>
</div>
)}
......@@ -201,13 +201,13 @@ export function Agents() {
)}
<Button size="sm" variant="outline" onClick={openNewAgent}>
<Plus className="h-3.5 w-3.5 mr-1.5" />
New Agent
Tác nhân mới
</Button>
</div>
</div>
{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>}
......@@ -215,8 +215,8 @@ export function Agents() {
{agents && agents.length === 0 && (
<EmptyState
icon={Bot}
message="Create your first agent to get started."
action="New Agent"
message="Hãy tạo tác nhân đầu tiên của bạn để bắt đầu."
action="Tác nhân mới"
onAction={openNewAgent}
/>
)}
......@@ -286,7 +286,7 @@ export function Agents() {
{effectiveView === "list" && agents && agents.length > 0 && filtered.length === 0 && (
<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>
)}
......@@ -301,13 +301,13 @@ export function Agents() {
{effectiveView === "org" && orgTree && orgTree.length > 0 && filteredOrg.length === 0 && (
<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>
)}
{effectiveView === "org" && orgTree && orgTree.length === 0 && (
<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>
)}
</div>
......
......@@ -26,7 +26,7 @@ export function Approvals() {
const [actionError, setActionError] = useState<string | null>(null);
useEffect(() => {
setBreadcrumbs([{ label: "Approvals" }]);
setBreadcrumbs([{ label: "Phê duyệt" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -75,7 +75,7 @@ export function Approvals() {
).length;
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) {
......@@ -87,7 +87,7 @@ export function Approvals() {
<div className="flex items-center justify-between">
<Tabs value={statusFilter} onValueChange={(v) => navigate(`/approvals/${v}`)}>
<PageTabBar items={[
{ value: "pending", label: <>Pending{pendingCount > 0 && (
{ value: "pending", label: <>Đang chờ{pendingCount > 0 && (
<span className={cn(
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
"bg-yellow-500/20 text-yellow-500"
......@@ -95,7 +95,7 @@ export function Approvals() {
{pendingCount}
</span>
)}</> },
{ value: "all", label: "All" },
{ value: "all", label: "Tất cả" },
]} />
</Tabs>
</div>
......@@ -107,7 +107,7 @@ export function Approvals() {
<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" />
<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>
</div>
)}
......
......@@ -69,7 +69,7 @@ export function Companies() {
});
useEffect(() => {
setBreadcrumbs([{ label: "Companies" }]);
setBreadcrumbs([{ label: "Phòng ban" }]);
}, [setBreadcrumbs]);
function startEdit(companyId: string, currentName: string) {
......@@ -92,12 +92,12 @@ export function Companies() {
<div className="flex items-center justify-end">
<Button size="sm" onClick={() => openOnboarding()}>
<Plus className="h-3.5 w-3.5 mr-1.5" />
New Company
Thêm phòng ban
</Button>
</div>
<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>}
</div>
......@@ -215,7 +215,7 @@ export function Companies() {
onClick={() => startEdit(company.id, company.name)}
>
<Pencil className="h-3.5 w-3.5" />
Rename
Đổi tên
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
......@@ -223,7 +223,7 @@ export function Companies() {
onClick={() => setConfirmDeleteId(company.id)}
>
<Trash2 className="h-3.5 w-3.5" />
Delete Company
Xóa phòng ban
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
......@@ -235,13 +235,13 @@ export function Companies() {
<div className="flex items-center gap-1.5">
<Users className="h-3.5 w-3.5" />
<span>
{agentCount} {agentCount === 1 ? "agent" : "agents"}
{agentCount} {agentCount === 1 ? "tác vụ/agent" : "tác vụ/agent"}
</span>
</div>
<div className="flex items-center gap-1.5">
<CircleDot className="h-3.5 w-3.5" />
<span>
{issueCount} {issueCount === 1 ? "issue" : "issues"}
{issueCount} vấn đề
</span>
</div>
<div className="flex items-center gap-1.5 tabular-nums">
......@@ -250,12 +250,12 @@ export function Companies() {
{formatCents(company.spentMonthlyCents)}
{company.budgetMonthlyCents > 0
? <> / {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>
</div>
<div className="flex items-center gap-1.5 ml-auto">
<Calendar className="h-3.5 w-3.5" />
<span>Created {relativeTime(company.createdAt)}</span>
<span>Đã tạo {relativeTime(company.createdAt)}</span>
</div>
</div>
......@@ -266,7 +266,7 @@ export function Companies() {
onClick={(e) => e.stopPropagation()}
>
<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>
<div className="flex items-center gap-2 ml-4 shrink-0">
<Button
......@@ -275,7 +275,7 @@ export function Companies() {
onClick={() => setConfirmDeleteId(null)}
disabled={deleteMutation.isPending}
>
Cancel
Hủy bỏ
</Button>
<Button
variant="destructive"
......@@ -283,7 +283,7 @@ export function Companies() {
onClick={() => deleteMutation.mutate(company.id)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting…" : "Delete"}
{deleteMutation.isPending ? "Đang xóa…" : "Xóa"}
</Button>
</div>
</div>
......
......@@ -71,9 +71,9 @@ export function CompanyAccess() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" },
{ label: "Access" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Thiết lập", href: "/company/settings" },
{ label: "Quyền truy cập" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -232,20 +232,20 @@ export function CompanyAccess() {
}, [removingMember]);
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) {
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) {
const message =
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.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>;
}
......@@ -271,16 +271,16 @@ export function CompanyAccess() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<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>
<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>
</div>
{access && !access.currentUserRole && (
<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>
)}
......@@ -288,10 +288,10 @@ export function CompanyAccess() {
<div className="space-y-1">
<div className="flex items-center gap-2">
<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>
<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>
</div>
......@@ -299,12 +299,12 @@ export function CompanyAccess() {
<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>
<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">
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>
</div>
<Badge variant="outline">{pendingHumanJoinRequests.length} pending</Badge>
<Badge variant="outline">{pendingHumanJoinRequests.length} đang chờ</Badge>
</div>
<div className="space-y-3">
{pendingHumanJoinRequests.map((request) => (
......@@ -314,22 +314,22 @@ export function CompanyAccess() {
request.requesterUser?.name ||
request.requestEmailSnapshot ||
request.requestingUserId ||
"Unknown human requester"
"Yêu cầu gia nhập không rõ danh tính"
}
subtitle={
request.requesterUser?.email ||
request.requestEmailSnapshot ||
request.requestingUserId ||
"No email available"
"Không có email"
}
context={
request.invite
? `${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()}`}
approveLabel="Approve human"
rejectLabel="Reject human"
detail={`Đã gửi lúc ${new Date(request.createdAt).toLocaleString()}`}
approveLabel="Phê duyệt"
rejectLabel="Từ chối"
disabled={joinRequestActionPending}
onApprove={() => approveJoinRequestMutation.mutate(request.id)}
onReject={() => rejectJoinRequestMutation.mutate(request.id)}
......@@ -341,14 +341,14 @@ export function CompanyAccess() {
<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>User account</div>
<div>Role</div>
<div>Status</div>
<div>Grants</div>
<div className="text-right">Action</div>
<div>Tài khoản</div>
<div>Vai trò</div>
<div>Trạng thái</div>
<div>Quyền hạn</div>
<div className="text-right">Hành động</div>
</div>
{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) => {
const removalReason = member.removal?.reason ?? null;
......@@ -403,16 +403,16 @@ export function CompanyAccess() {
<Dialog open={!!editingMember} onOpenChange={(open) => !open && setEditingMemberId(null)}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit member</DialogTitle>
<DialogTitle>Chỉnh sửa thành viên</DialogTitle>
<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>
</DialogHeader>
{editingMember && (
<div className="space-y-5">
<div className="grid gap-4 md:grid-cols-2">
<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
className="w-full rounded-md border border-border bg-background px-3 py-2"
value={draftRole ?? ""}
......@@ -420,7 +420,7 @@ export function CompanyAccess() {
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]) => (
<option key={value} value={value}>
{label}
......@@ -429,7 +429,7 @@ export function CompanyAccess() {
</select>
</label>
<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
className="w-full rounded-md border border-border bg-background px-3 py-2"
value={draftStatus}
......@@ -437,26 +437,26 @@ export function CompanyAccess() {
setDraftStatus(event.target.value as EditableMemberStatus)
}
>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="suspended">Suspended</option>
<option value="active">Hoạt động</option>
<option value="pending">Chờ duyệt</option>
<option value="suspended">Tạm ngưng</option>
</select>
</label>
</div>
<div className="space-y-3">
<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">
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>
</div>
<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">
{draftRole
? `${HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS[draftRole]} currently includes these permissions automatically.`
: "No role is selected, so this member has no implicit grants right now."}
? `Vai trò ${HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS[draftRole]} tự động bao gồm các quyền hạn này.`
: "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>
{implicitGrantKeys.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
......@@ -507,7 +507,7 @@ export function CompanyAccess() {
)}
<DialogFooter>
<Button variant="outline" onClick={() => setEditingMemberId(null)}>
Cancel
Hủy bỏ
</Button>
<Button
onClick={() => {
......@@ -521,7 +521,7 @@ export function CompanyAccess() {
}}
disabled={updateMemberMutation.isPending}
>
{updateMemberMutation.isPending ? "Saving…" : "Save access"}
{updateMemberMutation.isPending ? "Đang lưu…" : "Lưu phân quyền"}
</Button>
</DialogFooter>
</DialogContent>
......@@ -530,9 +530,9 @@ export function CompanyAccess() {
<Dialog open={!!removingMember} onOpenChange={(open) => !open && setRemovingMemberId(null)}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Remove member</DialogTitle>
<DialogTitle>Xóa thành viên</DialogTitle>
<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>
</DialogHeader>
{removingMember && (
......@@ -542,22 +542,22 @@ export function CompanyAccess() {
<div className="text-sm text-muted-foreground">{removingMember.user?.email || removingMember.principalId}</div>
<div className="mt-2 text-sm text-muted-foreground">
{assignedIssuesQuery.isLoading
? "Checking assigned issues..."
: `${assignedIssues.length} open assigned issue${assignedIssues.length === 1 ? "" : "s"}`}
? "Đang kiểm tra công việc đã gán..."
: `${assignedIssues.length} công việc đang xử lý.`}
</div>
</div>
{assignedIssues.length > 0 ? (
<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
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
value={reassignmentTarget}
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 ? (
<optgroup label="Humans">
<optgroup label="Nhân sự">
{activeReassignmentUsers.map((member) => (
<option key={member.id} value={`user:${member.principalId}`}>
{memberDisplayName(member)}
......@@ -566,7 +566,7 @@ export function CompanyAccess() {
</optgroup>
) : null}
{activeReassignmentAgents.length > 0 ? (
<optgroup label="Agents">
<optgroup label="AI Agents">
{activeReassignmentAgents.map((agent) => (
<option key={agent.id} value={`agent:${agent.id}`}>
{agent.name} ({agent.role})
......@@ -584,7 +584,7 @@ export function CompanyAccess() {
))}
{assignedIssues.length > 6 ? (
<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>
) : null}
</div>
......@@ -594,7 +594,7 @@ export function CompanyAccess() {
)}
<DialogFooter>
<Button variant="outline" onClick={() => setRemovingMemberId(null)}>
Cancel
Hủy bỏ
</Button>
<Button
variant="destructive"
......@@ -607,7 +607,7 @@ export function CompanyAccess() {
}}
disabled={archiveMemberMutation.isPending || assignedIssuesQuery.isLoading}
>
{archiveMemberMutation.isPending ? "Removing..." : "Remove member"}
{archiveMemberMutation.isPending ? "Đang xóa..." : "Xóa thành viên"}
</Button>
</DialogFooter>
</DialogContent>
......
......@@ -169,9 +169,9 @@ export function CompanyEnvironments() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" },
{ label: "Environments" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Thiết lập", href: "/company/settings" },
{ label: "Môi trường" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -397,7 +397,7 @@ export function CompanyEnvironments() {
Object.keys(sandboxConfigErrors).length === 0);
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) {
......@@ -405,10 +405,10 @@ export function CompanyEnvironments() {
<div className="max-w-3xl space-y-4">
<div className="flex items-center gap-2">
<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 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>
);
......@@ -419,10 +419,10 @@ export function CompanyEnvironments() {
<div className="space-y-2">
<div className="flex items-center gap-2">
<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>
<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>
</div>
......@@ -486,7 +486,7 @@ export function CompanyEnvironments() {
<div className="space-y-3">
{(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) => {
const probe = probeResults[environment.id] ?? null;
......
......@@ -500,7 +500,7 @@ function ExportPreviewPane({
}) {
if (!selectedFile || content === null) {
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({
</pre>
) : (
<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>
......@@ -911,7 +911,7 @@ export function CompanyExport() {
}
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) {
......@@ -919,7 +919,7 @@ export function CompanyExport() {
}
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
......@@ -935,14 +935,14 @@ export function CompanyExport() {
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-4 text-sm">
<span className="font-medium">
{selectedCompany?.name ?? "Company"} export
Xuất phòng ban {selectedCompany?.name ?? "phòng ban"}
</span>
<span className="text-muted-foreground">
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
Đã chọn {selectedCount} / {totalFiles} tệp
</span>
{warnings.length > 0 && (
<span className="text-amber-500">
{warnings.length} warning{warnings.length === 1 ? "" : "s"}
{warnings.length} cảnh báo
</span>
)}
</div>
......@@ -953,8 +953,8 @@ export function CompanyExport() {
>
<Download className="mr-1.5 h-3.5 w-3.5" />
{downloadMutation.isPending
? "Building export..."
: `Export ${selectedCount} file${selectedCount === 1 ? "" : "s"}`}
? "Đang tạo tệp xuất..."
: `Xuất ${selectedCount} tệp`}
</Button>
</div>
</div>
......@@ -972,7 +972,7 @@ export function CompanyExport() {
<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">
<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 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">
......@@ -981,7 +981,7 @@ export function CompanyExport() {
type="text"
value={treeSearch}
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"
data-page-search-target="true"
/>
......@@ -1005,7 +1005,7 @@ export function CompanyExport() {
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"
>
Show more issues ({visibleTaskChildren} of {totalTaskChildren})
Hiển thị thêm vấn đề ({visibleTaskChildren} trên {totalTaskChildren})
</button>
</div>
)}
......
......@@ -193,7 +193,7 @@ function ImportPreviewPane({
}) {
if (!selectedFile || content === null) {
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({
</pre>
) : (
<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>
......@@ -411,10 +411,10 @@ function ConflictResolutionList({
<div className="rounded-md border border-border">
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
<h3 className="text-sm font-medium">
Renames
Đổi tên
</h3>
<span className="text-xs text-muted-foreground">
{conflicts.length} item{conflicts.length === 1 ? "" : "s"}
{conflicts.length} mục
</span>
</div>
<div className="divide-y divide-border">
......@@ -442,7 +442,7 @@ function ConflictResolutionList({
)}
onClick={() => onToggleSkip(item.slug, item.filePath)}
>
{isSkipped ? "skipped" : "skip"}
{isSkipped ? "đã bỏ qua" : "bỏ qua"}
</button>
<span className={cn(
......@@ -495,10 +495,10 @@ function ConflictResolutionList({
{isConfirmed ? (
<>
<Check className="h-3 w-3" />
confirmed
đã xác nhận
</>
) : (
"confirm rename"
"xác nhận đổi tên"
)}
</button>
)}
......@@ -549,9 +549,9 @@ function AdapterPickerList({
<div className="mx-5 mt-3">
<div className="rounded-md border border-border">
<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">
{agents.length} agent{agents.length === 1 ? "" : "s"}
{agents.length} tác nhân
</span>
</div>
<div className="divide-y divide-border">
......@@ -567,7 +567,7 @@ function AdapterPickerList({
"shrink-0 rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-wide",
"text-blue-500 border-blue-500/30",
)}>
agent
tác nhân
</span>
<span className="shrink-0 font-mono text-xs text-muted-foreground">
{agent.name}
......@@ -595,7 +595,7 @@ function AdapterPickerList({
onClick={() => onToggleExpand(agent.slug)}
>
<ChevronRight className={cn("h-3 w-3 transition-transform", isExpanded && "rotate-90")} />
configure adapter
cấu hình bộ điều hợp
</button>
</div>
{isExpanded && (
......@@ -629,11 +629,11 @@ async function readLocalPackageZip(file: File): Promise<{
files: Record<string, CompanyPortabilityFileEntry>;
}> {
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());
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 {
name: file.name,
......@@ -704,7 +704,7 @@ export function CompanyImport() {
}, [companyAgents]);
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(() => {
setBreadcrumbs([
......@@ -1086,7 +1086,7 @@ export function CompanyImport() {
const selectedAction = selectedFile ? (actionMap.get(selectedFile) ?? null) : null;
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 (
......@@ -1094,17 +1094,17 @@ export function CompanyImport() {
{/* Source form section */}
<div className="border-b border-border px-5 py-5 space-y-4">
<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">
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>
</div>
<div className="grid gap-2 md:grid-cols-2">
{(
[
{ key: "github", icon: Github, label: "GitHub repo" },
{ key: "local", icon: Upload, label: "Local zip" },
{ key: "github", icon: Github, label: "Kho GitHub" },
{ key: "local", icon: Upload, label: "Tệp zip cục bộ" },
] as const
).map(({ key, icon: Icon, label }) => (
<button
......@@ -1163,7 +1163,7 @@ export function CompanyImport() {
) : (
<Field
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
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() {
</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
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
value={targetMode}
......@@ -1187,17 +1187,17 @@ export function CompanyImport() {
setImportPreview(null);
}}
>
<option value="new">Create new company</option>
<option value="new">Tạo phòng ban mới</option>
<option value="existing">
Existing company: {selectedCompany?.name}
Phòng ban hiện tại: {selectedCompany?.name}
</option>
</select>
</Field>
{targetMode === "new" && (
<Field
label="New company name"
hint="Optional override. Leave blank to use the package name."
label="Tên phòng ban mới"
hint="Tùy chọn ghi đè. Để trống để sử dụng tên gói mặc định."
>
<input
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() {
)}
<Field
label="Collision strategy"
hint="Board imports can rename, skip, or replace matching company content."
label="Xử lý trùng lặp"
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
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() {
setImportPreview(null);
}}
>
<option value="rename">Rename on conflict</option>
<option value="skip">Skip on conflict</option>
<option value="replace">Replace existing</option>
<option value="rename">Đổi tên khi trùng</option>
<option value="skip">Bỏ qua khi trùng</option>
<option value="replace">Thay thế hiện tại</option>
</select>
</Field>
......@@ -1234,7 +1234,7 @@ export function CompanyImport() {
onClick={() => previewMutation.mutate()}
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>
</div>
</div>
......@@ -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="flex flex-wrap items-center gap-4 text-sm">
<span className="font-medium">
Import preview
Xem trước nhập dữ liệu
</span>
<span className="text-muted-foreground">
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
Đã chọn {selectedCount} / {totalFiles} tệp
</span>
{conflicts.length > 0 && (
<span className="text-amber-500">
{conflicts.length} conflict{conflicts.length === 1 ? "" : "s"}
{conflicts.length} xung đột
</span>
)}
{importPreview.errors.length > 0 && (
......@@ -1295,8 +1295,8 @@ export function CompanyImport() {
>
<Download className="mr-1.5 h-3.5 w-3.5" />
{importMutation.isPending
? "Importing..."
: `Import ${selectedCount} file${selectedCount === 1 ? "" : "s"}`}
? "Đang nhập..."
: `Nhập ${selectedCount} tệp`}
</Button>
</div>
......@@ -1322,7 +1322,7 @@ export function CompanyImport() {
<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">
<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 className="flex-1 overflow-y-auto">
<FileTree
......
......@@ -13,9 +13,9 @@ import { queryKeys } from "@/lib/queryKeys";
const inviteRoleOptions = [
{
value: "viewer",
label: "Viewer",
description: "Can view company work and follow along without operational permissions.",
gets: "No built-in grants.",
label: "Người quan sát (Viewer)",
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: "Không có quyền mặc định.",
},
{
value: "operator",
......@@ -31,9 +31,9 @@ const inviteRoleOptions = [
},
{
value: "owner",
label: "Owner",
description: "Full company access, including membership and permission management.",
gets: "Everything in Admin, plus managing members and permission grants.",
label: "Chủ sở hữu (Owner)",
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: "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;
......@@ -81,9 +81,9 @@ export function CompanyInvites() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" },
{ label: "Invites" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Thiết lập", href: "/company/settings" },
{ label: "Lời mời" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -151,20 +151,20 @@ export function CompanyInvites() {
});
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) {
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) {
const message =
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.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>;
}
......@@ -173,10 +173,10 @@ export function CompanyInvites() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<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>
<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>
</div>
......@@ -288,7 +288,7 @@ export function CompanyInvites() {
{inviteHistory.length === 0 ? (
<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 className="border-t border-border">
......
......@@ -214,15 +214,15 @@ export function CompanySettings() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings" }
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Thiết lập" }
]);
}, [setBreadcrumbs, selectedCompany?.name]);
if (!selectedCompany) {
return (
<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>
);
}
......@@ -240,16 +240,16 @@ export function CompanySettings() {
<div className="max-w-2xl space-y-6">
<div className="flex items-center gap-2">
<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>
{/* General */}
<div className="space-y-4">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
General
Thiết lập chung
</div>
<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
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
type="text"
......@@ -258,14 +258,14 @@ export function CompanySettings() {
/>
</Field>
<Field
label="Description"
hint="Optional description shown in the company profile."
label="Mô tả"
hint="Mô tả ngắn gọn về phòng ban này."
>
<input
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
type="text"
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)}
/>
</Field>
......@@ -275,7 +275,7 @@ export function CompanySettings() {
{/* Appearance */}
<div className="space-y-4">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Appearance
Giao diện
</div>
<div className="space-y-3 rounded-md border border-border px-4 py-4">
<div className="flex items-start gap-4">
......@@ -289,8 +289,8 @@ export function CompanySettings() {
</div>
<div className="flex-1 space-y-3">
<Field
label="Logo"
hint="Upload a PNG, JPEG, WEBP, GIF, or SVG logo image."
label="Logo phòng ban"
hint="Tải lên tệp ảnh logo định dạng PNG, JPEG, WEBP, GIF hoặc SVG."
>
<div className="space-y-2">
<input
......@@ -307,7 +307,7 @@ export function CompanySettings() {
onClick={handleClearLogo}
disabled={clearLogoMutation.isPending}
>
{clearLogoMutation.isPending ? "Removing..." : "Remove logo"}
{clearLogoMutation.isPending ? "Đang xóa..." : "Xóa logo"}
</Button>
</div>
)}
......@@ -316,7 +316,7 @@ export function CompanySettings() {
{logoUploadError ??
(logoUploadMutation.error instanceof Error
? logoUploadMutation.error.message
: "Logo upload failed")}
: "Tải lên logo thất bại")}
</span>
)}
{clearLogoMutation.isError && (
......@@ -325,13 +325,13 @@ export function CompanySettings() {
</span>
)}
{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>
</Field>
<Field
label="Brand color"
hint="Sets the hue for the company icon. Leave empty for auto-generated color."
label="Màu chủ đạo"
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">
<input
......@@ -349,7 +349,7 @@ export function CompanySettings() {
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"
/>
{brandColor && (
......@@ -359,14 +359,14 @@ export function CompanySettings() {
onClick={() => setBrandColor("")}
className="text-xs text-muted-foreground"
>
Clear
Xóa
</Button>
)}
</div>
</Field>
<Field
label="Attachment size limit"
hint={`Accepted range: 1-${MAX_COMPANY_ATTACHMENT_MAX_MIB} MiB.`}
label="Giới hạn dung lượng tệp đính kèm"
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 items-center gap-2">
......@@ -383,7 +383,7 @@ export function CompanySettings() {
</div>
{!attachmentMaxValid && (
<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>
)}
</div>
......@@ -401,16 +401,16 @@ export function CompanySettings() {
onClick={handleSaveGeneral}
disabled={generalMutation.isPending || !companyName.trim() || !attachmentMaxValid}
>
{generalMutation.isPending ? "Saving..." : "Save changes"}
{generalMutation.isPending ? "Đang lưu..." : "Lưu thay đổi"}
</Button>
{generalMutation.isSuccess && (
<span className="text-xs text-muted-foreground">Saved</span>
<span className="text-xs text-muted-foreground">Đã lưu</span>
)}
{generalMutation.isError && (
<span className="text-xs text-destructive">
{generalMutation.error instanceof Error
? generalMutation.error.message
: "Failed to save"}
: "Lưu thất bại"}
</span>
)}
</div>
......@@ -419,12 +419,12 @@ export function CompanySettings() {
{/* Hiring */}
<div className="space-y-4" data-testid="company-settings-team-section">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Hiring
Nhân sự & Tuyển dụng
</div>
<div className="rounded-md border border-border px-4 py-3">
<ToggleField
label="Require board approval for new hires"
hint="New agent hires stay pending until approved by board."
label="Yêu cầu ban điều hành phê duyệt nhân sự mới"
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}
onChange={(v) => settingsMutation.mutate(v)}
toggleTestId="company-settings-team-approval-toggle"
......@@ -435,14 +435,14 @@ export function CompanySettings() {
{/* Invites */}
<div className="space-y-4" data-testid="company-settings-invites-section">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Invites
Lời mời
</div>
<div className="space-y-3 rounded-md border border-border px-4 py-4">
<div className="flex items-center gap-1.5">
<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>
<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 className="flex flex-wrap items-center gap-2">
<Button
......@@ -452,8 +452,8 @@ export function CompanySettings() {
disabled={inviteMutation.isPending}
>
{inviteMutation.isPending
? "Generating..."
: "Generate OpenClaw Invite Prompt"}
? "Đang tạo..."
: "Tạo lời nhắc mời OpenClaw"}
</Button>
</div>
{inviteError && (
......@@ -466,7 +466,7 @@ export function CompanySettings() {
>
<div className="flex items-center justify-between gap-2">
<div className="text-xs text-muted-foreground">
OpenClaw Invite Prompt
Lời nhắc mời OpenClaw
</div>
{snippetCopied && (
<span
......@@ -474,7 +474,7 @@ export function CompanySettings() {
className="flex items-center gap-1 text-xs text-green-600 animate-pulse"
>
<Check className="h-3 w-3" />
Copied
Đã sao chép
</span>
)}
</div>
......@@ -501,7 +501,7 @@ export function CompanySettings() {
}
}}
>
{snippetCopied ? "Copied snippet" : "Copy snippet"}
{snippetCopied ? "Đã sao chép mã" : "Sao chép mã"}
</Button>
</div>
</div>
......@@ -513,24 +513,24 @@ export function CompanySettings() {
{/* Import / Export */}
<div className="space-y-4">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Company Packages
Gói dữ liệu phòng ban
</div>
<div className="rounded-md border border-border px-4 py-4">
<p className="text-sm text-muted-foreground">
Import and export have moved to dedicated pages accessible from the{" "}
<a href="/org" className="underline hover:text-foreground">Org Chart</a> header.
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">Sơ đồ</a>.
</p>
<div className="mt-3 flex items-center gap-2">
<Button size="sm" variant="outline" asChild>
<a href="/company/export">
<Download className="mr-1.5 h-3.5 w-3.5" />
Export
Xuất dữ liệu
</a>
</Button>
<Button size="sm" variant="outline" asChild>
<a href="/company/import">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Import
Nhập dữ liệu
</a>
</Button>
</div>
......@@ -540,12 +540,11 @@ export function CompanySettings() {
{/* Danger Zone */}
<div className="space-y-4">
<div className="text-xs font-medium text-destructive uppercase tracking-wide">
Danger Zone
Khu vực nguy hiểm
</div>
<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">
Archive this company to hide it from the sidebar. This persists in
the database.
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.
</p>
<div className="flex items-center gap-2">
<Button
......@@ -558,7 +557,7 @@ export function CompanySettings() {
onClick={() => {
if (!selectedCompanyId) return;
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;
const nextCompanyId =
......@@ -574,16 +573,16 @@ export function CompanySettings() {
}}
>
{archiveMutation.isPending
? "Archiving..."
? "Đang lưu trữ..."
: selectedCompany.status === "archived"
? "Already archived"
: "Archive company"}
? "Đã lưu trữ"
: "Lưu trữ phòng ban"}
</Button>
{archiveMutation.isError && (
<span className="text-xs text-destructive">
{archiveMutation.error instanceof Error
? archiveMutation.error.message
: "Failed to archive company"}
: "Lưu trữ phòng ban thất bại"}
</span>
)}
</div>
......
......@@ -797,8 +797,8 @@ export function CompanySkills() {
useEffect(() => {
setBreadcrumbs([
{ label: "Skills", href: "/skills" },
...(routeSkillId ? [{ label: "Detail" }] : []),
{ label: "Kỹ năng", href: "/skills" },
...(routeSkillId ? [{ label: "Chi tiết" }] : []),
]);
}, [routeSkillId, setBreadcrumbs]);
......@@ -1080,7 +1080,7 @@ export function CompanySkills() {
});
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() {
......@@ -1097,9 +1097,9 @@ export function CompanySkills() {
<Dialog open={deleteOpen} onOpenChange={closeDeleteDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Remove skill</DialogTitle>
<DialogTitle>Xóa kỹ năng</DialogTitle>
<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>
</DialogHeader>
<div className="space-y-3 text-sm">
......@@ -1189,9 +1189,9 @@ export function CompanySkills() {
<div className="border-b border-border px-4 py-3">
<div className="flex items-center justify-between gap-2">
<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">
{skillsQuery.data?.length ?? 0} available
{skillsQuery.data?.length ?? 0} kỹ năng
</p>
</div>
<div className="flex items-center gap-1">
......@@ -1215,7 +1215,7 @@ export function CompanySkills() {
<input
value={skillFilter}
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"
/>
</div>
......
......@@ -168,7 +168,7 @@ export function Costs() {
} = useDateRange();
useEffect(() => {
setBreadcrumbs([{ label: "Costs" }]);
setBreadcrumbs([{ label: "Chi phí" }]);
}, [setBreadcrumbs]);
const [today, setToday] = useState(() => new Date().toDateString());
......@@ -529,7 +529,7 @@ export function Costs() {
}), [budgetPolicies]);
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;
......
......@@ -50,7 +50,7 @@ export function Dashboard() {
});
useEffect(() => {
setBreadcrumbs([{ label: "Dashboard" }]);
setBreadcrumbs([{ label: "Bảng điều khiển" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -176,14 +176,14 @@ export function Dashboard() {
return (
<EmptyState
icon={LayoutDashboard}
message="Welcome to Paperclip. Set up your first company and agent to get started."
action="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="Bắt đầu"
onAction={openOnboarding}
/>
);
}
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() {
<div className="flex items-center gap-2.5">
<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">
You have no agents.
Bạn chưa có tác nhân nào.
</p>
</div>
<button
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"
>
Create one here
Tạo tác nhân tại đây
</button>
</div>
)}
......@@ -241,7 +241,7 @@ export function Dashboard() {
<MetricCard
icon={Bot}
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"
description={
<span>
......@@ -254,7 +254,7 @@ export function Dashboard() {
<MetricCard
icon={CircleDot}
value={data.tasks.inProgress}
label="Tasks In Progress"
label="Nhiệm vụ đang thực hiện"
to="/issues"
description={
<span>
......@@ -266,7 +266,7 @@ export function Dashboard() {
<MetricCard
icon={DollarSign}
value={formatCents(data.costs.monthSpendCents)}
label="Month Spend"
label="Chi tiêu trong tháng"
to="/costs"
description={
<span>
......@@ -279,7 +279,7 @@ export function Dashboard() {
<MetricCard
icon={ShieldCheck}
value={data.pendingApprovals + data.budgets.pendingApprovals}
label="Pending Approvals"
label="Đang chờ duyệt"
to="/approvals"
description={
<span>
......@@ -318,7 +318,7 @@ export function Dashboard() {
{recentActivity.length > 0 && (
<div className="min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Recent Activity
Hoạt động gần đây
</h3>
<div className="border border-border divide-y divide-border overflow-hidden">
{recentActivity.map((event) => (
......@@ -339,7 +339,7 @@ export function Dashboard() {
{/* Recent Tasks */}
<div className="min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Recent Tasks
Nhiệm vụ gần đây
</h3>
{recentIssues.length === 0 ? (
<div className="border border-border p-4">
......
......@@ -14,8 +14,8 @@ export function DashboardLive() {
useEffect(() => {
setBreadcrumbs([
{ label: "Dashboard", href: "/dashboard" },
{ label: "Live runs" },
{ label: "Bảng điều khiển", href: "/dashboard" },
{ label: "Chạy trực tiếp" },
]);
}, [setBreadcrumbs]);
......@@ -23,7 +23,7 @@ export function DashboardLive() {
return (
<EmptyState
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() {
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" />
Dashboard
Bảng điều khiển
</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">
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>
</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>
<ActiveAgentsPanel
......@@ -55,7 +55,7 @@ export function DashboardLive() {
cardLimit={DASHBOARD_LIVE_RUN_LIMIT}
gridClassName="gap-3 md:grid-cols-2 2xl:grid-cols-3"
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"
showMoreLink={false}
/>
......
......@@ -17,7 +17,7 @@ export function Goals() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "Goals" }]);
setBreadcrumbs([{ label: "Mục tiêu" }]);
}, [setBreadcrumbs]);
const { data: goals, isLoading, error } = useQuery({
......@@ -27,7 +27,7 @@ export function Goals() {
});
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) {
......@@ -41,8 +41,8 @@ export function Goals() {
{goals && goals.length === 0 && (
<EmptyState
icon={Target}
message="No goals yet."
action="Add Goal"
message="Chưa có mục tiêu nào."
action="Thêm mục tiêu"
onAction={() => openNewGoal()}
/>
)}
......@@ -52,7 +52,7 @@ export function Goals() {
<div className="flex items-center justify-start">
<Button size="sm" variant="outline" onClick={() => openNewGoal()}>
<Plus className="h-3.5 w-3.5 mr-1.5" />
New Goal
Mục tiêu mới
</Button>
</div>
<GoalTree goals={goals} goalLink={(goal) => `/goals/${goal.id}`} />
......
......@@ -1866,7 +1866,7 @@ export function Inbox() {
}, [selectedIndex]);
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;
......@@ -1954,15 +1954,15 @@ export function Inbox() {
items={[
{
value: "mine",
label: "Mine",
label: "Của tôi",
},
{
value: "recent",
label: "Recent",
label: "Gần đây",
},
{ value: "unread", label: "Unread" },
{ value: "blocked", label: "Blocked" },
{ value: "all", label: "All" },
{ value: "unread", label: "Chưa đọc" },
{ value: "blocked", label: "Bị chặn" },
{ value: "all", label: "Tất cả" },
]}
/>
</Tabs>
......
......@@ -108,7 +108,7 @@ export function Issues() {
const issueLinkState = useMemo(
() =>
createIssueDetailLocationState(
"Issues",
"Vấn đề",
`${location.pathname}${location.search}${location.hash}`,
"issues",
),
......@@ -116,7 +116,7 @@ export function Issues() {
);
useEffect(() => {
setBreadcrumbs([{ label: "Issues" }]);
setBreadcrumbs([{ label: "Vấn đề" }]);
}, [setBreadcrumbs]);
const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE;
......@@ -173,7 +173,7 @@ export function Issues() {
});
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 (
......
......@@ -20,9 +20,9 @@ export function JoinRequestQueue() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Inbox", href: "/inbox" },
{ label: "Join Requests" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Hộp thư", href: "/inbox" },
{ label: "Yêu cầu tham gia" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -56,11 +56,11 @@ export function JoinRequestQueue() {
});
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) {
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) {
......@@ -78,16 +78,16 @@ export function JoinRequestQueue() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<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>
<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>
</div>
<div className="flex flex-wrap gap-3 rounded-xl border border-border bg-card p-4">
<label className="space-y-2 text-sm">
<span className="font-medium">Status</span>
<span className="font-medium">Trạng thái</span>
<select
className="rounded-md border border-border bg-background px-3 py-2"
value={status}
......@@ -95,13 +95,13 @@ export function JoinRequestQueue() {
setStatus(event.target.value as "pending_approval" | "approved" | "rejected")
}
>
<option value="pending_approval">Pending approval</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
<option value="pending_approval">Đang chờ duyệt</option>
<option value="approved">Đã duyệt</option>
<option value="rejected">Đã từ chối</option>
</select>
</label>
<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
className="rounded-md border border-border bg-background px-3 py-2"
value={requestType}
......@@ -109,9 +109,9 @@ export function JoinRequestQueue() {
setRequestType(event.target.value as "all" | "human" | "agent")
}
>
<option value="all">All</option>
<option value="human">Human</option>
<option value="agent">Agent</option>
<option value="all">Tất cả</option>
<option value="human">Người</option>
<option value="agent">Tác nhân</option>
</select>
</label>
</div>
......@@ -119,7 +119,7 @@ export function JoinRequestQueue() {
<div className="space-y-4">
{(requestsQuery.data ?? []).length === 0 ? (
<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>
) : (
requestsQuery.data!.map((request) => (
......@@ -154,13 +154,13 @@ export function JoinRequestQueue() {
onClick={() => rejectMutation.mutate(request.id)}
disabled={rejectMutation.isPending}
>
Reject
Từ chối
</Button>
<Button
onClick={() => approveMutation.mutate(request.id)}
disabled={approveMutation.isPending}
>
Approve
Phê duyệt
</Button>
</div>
) : null}
......
......@@ -17,7 +17,7 @@ export function MyIssues() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "My Issues" }]);
setBreadcrumbs([{ label: "Vấn đề của tôi" }]);
}, [setBreadcrumbs]);
const { data: issues, isLoading, error } = useQuery({
......@@ -27,7 +27,7 @@ export function MyIssues() {
});
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) {
......@@ -44,7 +44,7 @@ export function MyIssues() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{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 && (
......
......@@ -94,7 +94,7 @@ export function Org() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "Org Chart" }]);
setBreadcrumbs([{ label: "Sơ đồ tổ chức" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -104,7 +104,7 @@ export function Org() {
});
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) {
......@@ -118,7 +118,7 @@ export function Org() {
{data && data.length === 0 && (
<EmptyState
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() {
}, [agents]);
useEffect(() => {
setBreadcrumbs([{ label: "Org Chart" }]);
setBreadcrumbs([{ label: "Sơ đồ tổ chức" }]);
}, [setBreadcrumbs]);
// Layout computation
......@@ -429,7 +429,7 @@ export function OrgChart() {
}, [pan, zoom]);
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) {
......@@ -437,7 +437,7 @@ export function OrgChart() {
}
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 (
......@@ -446,13 +446,13 @@ export function OrgChart() {
<Link to="/company/import">
<Button variant="outline" size="sm">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Import company
Nhập phòng ban
</Button>
</Link>
<Link to="/company/export">
<Button variant="outline" size="sm">
<Download className="mr-1.5 h-3.5 w-3.5" />
Export company
Xuất phòng ban
</Button>
</Link>
</div>
......
......@@ -130,13 +130,13 @@ export function PluginPage() {
}
return (
<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>
);
}
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) {
......@@ -168,7 +168,7 @@ export function PluginPage() {
<Button variant="ghost" size="sm" asChild>
<Link to={companyPrefix ? `/${companyPrefix}/dashboard` : "/dashboard"}>
<ArrowLeft className="h-4 w-4 mr-1" />
Back
Quay lại
</Link>
</Button>
</div>
......
......@@ -19,7 +19,7 @@ export function Projects() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "Projects" }]);
setBreadcrumbs([{ label: "Dự án" }]);
}, [setBreadcrumbs]);
const { data: allProjects, isLoading, error } = useQuery({
......@@ -33,7 +33,7 @@ export function Projects() {
);
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) {
......@@ -45,7 +45,7 @@ export function Projects() {
<div className="flex items-center justify-end">
<Button size="sm" variant="outline" onClick={openNewProject}>
<Plus className="h-4 w-4 mr-1" />
Add Project
Thêm dự án
</Button>
</div>
......@@ -54,8 +54,8 @@ export function Projects() {
{!isLoading && projects.length === 0 && (
<EmptyState
icon={Hexagon}
message="No projects yet."
action="Add Project"
message="Chưa có dự án nào."
action="Thêm dự án"
onAction={openNewProject}
/>
)}
......
......@@ -241,7 +241,7 @@ export function Routines() {
const [routineViewState, setRoutineViewState] = useState<RoutineViewState>(() => getRoutineViewState(routineViewStateKey));
useEffect(() => {
setBreadcrumbs([{ label: "Routines" }]);
setBreadcrumbs([{ label: "Quy trình" }]);
}, [setBreadcrumbs]);
useEffect(() => {
......@@ -479,7 +479,7 @@ export function Routines() {
}
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) {
......@@ -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="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">
Routines
Quy trình
</h1>
<p className="text-sm text-muted-foreground">
Recurring work definitions that materialize into auditable execution issues.
......@@ -509,8 +509,8 @@ export function Routines() {
value={activeTab}
onValueChange={handleTabChange}
items={[
{ value: "routines", label: "Routines" },
{ value: "runs", label: "Recent Runs" },
{ value: "routines", label: "Quy trình" },
{ value: "runs", label: "Phiên chạy gần đây" },
]}
/>
<TabsContent value="routines" className="space-y-4">
......@@ -877,7 +877,7 @@ export function Routines() {
<div className="py-12">
<EmptyState
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>
) : (
......
......@@ -376,7 +376,7 @@ export function Secrets() {
const [vaultError, setVaultError] = useState<string | null>(null);
useEffect(() => {
setBreadcrumbs([{ label: "Secrets" }]);
setBreadcrumbs([{ label: "Bí mật" }]);
}, [setBreadcrumbs]);
const secretsQuery = useQuery({
......@@ -747,7 +747,7 @@ export function Secrets() {
if (!selectedCompanyId) {
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() {
<div className="flex h-full min-h-0 flex-col gap-4">
<div className="flex items-center gap-2">
<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>
<Tabs
......@@ -765,8 +765,8 @@ export function Secrets() {
>
<PageTabBar
items={[
{ value: "secrets", label: "Secrets" },
{ value: "vaults", label: "Provider vaults" },
{ value: "secrets", label: "Bí mật phòng ban" },
{ value: "vaults", label: "Kho khóa (Vaults)" },
]}
align="start"
value={activeTab}
......@@ -781,9 +781,9 @@ export function Secrets() {
<Input
value={search}
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"
aria-label="Search secrets"
aria-label="Tìm kiếm bí mật"
data-page-search-target="true"
/>
</div>
......@@ -802,32 +802,32 @@ export function Secrets() {
className="ml-auto"
/>
<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>
</div>
<div className="min-h-0 flex-1 overflow-y-auto">
{secretsQuery.isError ? (
<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}
<Button variant="ghost" size="sm" onClick={() => secretsQuery.refetch()}>
Retry
Thử lại
</Button>
</div>
) : secrets.length === 0 && !secretsQuery.isPending ? (
<EmptyState
icon={KeyRound}
message="No secrets yet. Create your first managed secret or link an external reference."
action="New secret"
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="Thêm bí mật"
onAction={() => setCreateOpen(true)}
/>
) : 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">
<thead className="bg-muted/40 text-xs uppercase tracking-wide text-muted-foreground">
<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">Provider</th>
<th className="px-2 py-2 text-left font-medium">Status</th>
......
......@@ -207,7 +207,7 @@ export function UserProfile() {
});
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]);
const allTime = data?.stats.find((entry) => entry.key === "all");
......@@ -243,7 +243,7 @@ export function UserProfile() {
);
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) {
......@@ -251,7 +251,7 @@ export function UserProfile() {
}
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;
......
......@@ -17,5 +17,6 @@
"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