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>
......
This diff is collapsed.
...@@ -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>
)} )}
......
This diff is collapsed.
...@@ -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">
......
This diff is collapsed.
...@@ -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