Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### 新增

- Organization Phase 3(组织架构切片):宿主内 org chart。Dashboard `/organization/org` 使用共享 `dashboard/src/org-ui` 的 `OrgChartPage`;树与 CRUD 走 `/api/org-module/org`(`{FREEOS_HOME}/org/org_chart.sqlite`),不需要 Node sidecar。`freeos org export-standalone` 同时列出 Announcements 与 Org chart。未迁:Chat、员工目录页、汇报线/版本/导入导出图、其余 Open-12。
- Organization Phase 2:宿主内通知公告。Dashboard `/organization/announcements` 使用共享 `dashboard/src/org-ui` 页面;CRUD 走 `/api/org-module/announcements`(`{FREEOS_HOME}/org/announcements.sqlite`),不需要 Node sidecar。`freeos org export-standalone` 写出同一页面的独立站骨架(完整打包仍待 Phase 5)。

### 修复
Expand Down
6 changes: 5 additions & 1 deletion dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,13 @@
"packPreviewEmployees": "The preview now opens Employees so you can confirm the result.",
"navWorkbench": "Workbench",
"navAnnouncements": "Announcements",
"navOrgChart": "Org chart",
"announcementsTitle": "Announcements",
"announcementsBody": "Publish notices and track who has read them. This page lives in the host — no sidecar or second port.",
"openAnnouncements": "Open announcements"
"openAnnouncements": "Open announcements",
"orgChartTitle": "Organization chart",
"orgChartBody": "Departments and people on the host org tree. This page lives in the host — no sidecar or second port.",
"openOrgChart": "Open org chart"
},
"systemSettings": {
"subtitle": "Workbench, remote desktop, and host administration in one place."
Expand Down
6 changes: 5 additions & 1 deletion dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,13 @@
"packPreviewEmployees": "右侧预览已打开员工页,可直接核对。",
"navWorkbench": "工作台",
"navAnnouncements": "通知公告",
"navOrgChart": "组织架构",
"announcementsTitle": "通知公告",
"announcementsBody": "发布公告并跟踪已读。此页跑在宿主内,不需要边车或第二端口。",
"openAnnouncements": "打开通知公告"
"openAnnouncements": "打开通知公告",
"orgChartTitle": "组织架构图",
"orgChartBody": "宿主内的部门与人员树。此页跑在宿主内,不需要边车或第二端口。",
"openOrgChart": "打开组织架构"
},
"systemSettings": {
"subtitle": "工作台、远程桌面与主机管理集中在这里。"
Expand Down
7 changes: 7 additions & 0 deletions dashboard/src/org-ui/api/createClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ describe("createOrgApiClient", () => {
if (path === "/org-module/announcements/action/unread") {
return { success: true, data: { count: 2 } };
}
if (path === "/org-module/org/tree") {
return {
success: true,
data: [{ id: 1, name: "HQ", parent_id: null, sort_order: 0 }],
};
}
throw new Error(`unexpected ${path}`);
});
const client = createOrgApiClient({ fetchJson });
Expand All @@ -29,6 +35,7 @@ describe("createOrgApiClient", () => {
expect(listed.total).toBe(1);
expect(listed.list[0]?.title).toBe("Hi");
expect(await client.announcements.unread()).toEqual({ count: 2 });
expect((await client.org.tree())[0]?.name).toBe("HQ");
expect(fetchJson).toHaveBeenCalled();
});

Expand Down
141 changes: 140 additions & 1 deletion dashboard/src/org-ui/api/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,69 @@ export interface OrgAnnouncementsClient {
pinned(): Promise<Announcement[]>;
}

export interface OrgEmployee {
id: number;
name: string;
role: string;
description?: string;
employee_type: string;
agent_type?: string | null;
skills?: string;
avatar_emoji?: string;
department_id: number;
status: string;
is_online?: boolean;
}

export interface OrgDepartment {
id: number;
name: string;
parent_id: number | null;
sort_order: number;
description?: string;
department_code?: string | null;
function_type?: string;
level?: number;
children?: OrgDepartment[];
employees?: OrgEmployee[];
}

export interface DepartmentWrite {
name?: string;
parent_id?: number | null;
sort_order?: number;
description?: string;
department_code?: string | null;
function_type?: string;
level?: number;
}

export interface EmployeeWrite {
name?: string;
department_id?: number;
role?: string;
description?: string;
employee_type?: string;
agent_type?: string | null;
skills?: string;
avatar_emoji?: string;
status?: string;
}

export interface OrgChartClient {
tree(): Promise<OrgDepartment[]>;
listDepartments(): Promise<OrgDepartment[]>;
createDepartment(body: DepartmentWrite): Promise<OrgDepartment>;
updateDepartment(id: number, body: DepartmentWrite): Promise<OrgDepartment>;
removeDepartment(id: number): Promise<void>;
createEmployee(body: EmployeeWrite): Promise<OrgEmployee>;
updateEmployee(id: number, body: EmployeeWrite): Promise<OrgEmployee>;
removeEmployee(id: number): Promise<void>;
}

export interface OrgApiClient {
announcements: OrgAnnouncementsClient;
org: OrgChartClient;
}

async function unwrap<T>(payload: OrgEnvelope<T>): Promise<T> {
Expand All @@ -79,8 +140,10 @@ async function unwrap<T>(payload: OrgEnvelope<T>): Promise<T> {
export function createOrgApiClient(opts: {
fetchJson: OrgFetcher;
announcementsPrefix?: string;
orgPrefix?: string;
}): OrgApiClient {
const prefix = opts.announcementsPrefix ?? "/org-module/announcements";
const orgPrefix = opts.orgPrefix ?? "/org-module/org";
const fetchJson = opts.fetchJson;

const announcements: OrgAnnouncementsClient = {
Expand Down Expand Up @@ -163,5 +226,81 @@ export function createOrgApiClient(opts: {
},
};

return { announcements };
const org: OrgChartClient = {
async tree() {
return unwrap(
await fetchJson<OrgEnvelope<OrgDepartment[]>>(`${orgPrefix}/tree`),
);
},
async listDepartments() {
return unwrap(
await fetchJson<OrgEnvelope<OrgDepartment[]>>(
`${orgPrefix}/departments`,
),
);
},
async createDepartment(body) {
return unwrap(
await fetchJson<OrgEnvelope<OrgDepartment>>(
`${orgPrefix}/departments`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
),
);
},
async updateDepartment(id, body) {
return unwrap(
await fetchJson<OrgEnvelope<OrgDepartment>>(
`${orgPrefix}/departments/${id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
),
);
},
async removeDepartment(id) {
await unwrap(
await fetchJson<OrgEnvelope<undefined>>(
`${orgPrefix}/departments/${id}`,
{ method: "DELETE" },
),
);
},
async createEmployee(body) {
return unwrap(
await fetchJson<OrgEnvelope<OrgEmployee>>(`${orgPrefix}/employees`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}),
);
},
async updateEmployee(id, body) {
return unwrap(
await fetchJson<OrgEnvelope<OrgEmployee>>(
`${orgPrefix}/employees/${id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
),
);
},
async removeEmployee(id) {
await unwrap(
await fetchJson<OrgEnvelope<undefined>>(
`${orgPrefix}/employees/${id}`,
{ method: "DELETE" },
),
);
},
};

return { announcements, org };
}
2 changes: 1 addition & 1 deletion dashboard/src/org-ui/contract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** Catalog keys that have a shared org-ui page (keep in sync with contract.py). */
export const SHARED_ORG_UI_MODULES = ["announcements"] as const;
export const SHARED_ORG_UI_MODULES = ["announcements", "organization"] as const;

export type SharedOrgUiModule = (typeof SHARED_ORG_UI_MODULES)[number];
7 changes: 7 additions & 0 deletions dashboard/src/org-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ export type {
Announcement,
AnnouncementListData,
AnnouncementWrite,
DepartmentWrite,
EmployeeWrite,
OrgAnnouncementsClient,
OrgApiClient,
OrgChartClient,
OrgDepartment,
OrgEmployee,
OrgEnvelope,
} from "./api/createClient";
export { AnnouncementPage } from "./pages/announcements/AnnouncementPage";
export type { AnnouncementPageProps } from "./pages/announcements/AnnouncementPage";
export { OrgChartPage } from "./pages/org/OrgChartPage";
export type { OrgChartPageProps } from "./pages/org/OrgChartPage";
export type {
IdentityBridge,
OrgFetcher,
Expand Down
111 changes: 111 additions & 0 deletions dashboard/src/org-ui/pages/org/OrgChartPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
.page {
max-width: 960px;
margin: 0 auto;
padding: 8px 4px 32px;
display: flex;
flex-direction: column;
gap: 16px;
}

.header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}

.titleRow {
display: flex;
align-items: center;
gap: 10px;
}

.title {
margin: 0;
font-size: 20px;
font-weight: 650;
}

.filters {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}

.empty {
text-align: center;
padding: 48px 12px;
color: var(--fn-text-secondary, #667085);
}

.tree {
display: flex;
flex-direction: column;
gap: 12px;
}

.node {
border: 1px solid var(--fn-border, #e5e7eb);
border-radius: 10px;
padding: 14px;
background: var(--fn-bg-container, #fff);
}

.nodeHead {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}

.nodeTitle {
margin: 0;
font-size: 15px;
font-weight: 600;
}

.nodeMeta {
margin: 6px 0 0;
color: var(--fn-text-secondary, #667085);
font-size: 13px;
}

.people {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}

.person {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--fn-border, #e5e7eb);
border-radius: 999px;
padding: 4px 10px;
background: var(--fn-bg, #f9fafb);
cursor: pointer;
font-size: 13px;
}

.personHidden {
display: none;
}

.children {
margin-top: 12px;
margin-left: 16px;
padding-left: 12px;
border-left: 2px solid var(--fn-border, #e5e7eb);
display: flex;
flex-direction: column;
gap: 12px;
}

.actions {
margin-left: auto;
}
Loading
Loading