Skip to content

Commit 2bdea6b

Browse files
committed
initial: KG Planning standalone extension for Y-app
0 parents  commit 2bdea6b

47 files changed

Lines changed: 3966 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pages.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: true
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v5
22+
- uses: actions/setup-node@v5
23+
with:
24+
node-version: 20
25+
cache: npm
26+
- run: npm ci
27+
- run: npm run build
28+
- uses: actions/configure-pages@v5
29+
- uses: actions/upload-pages-artifact@v3
30+
with:
31+
path: dist
32+
33+
deploy:
34+
needs: build
35+
runs-on: ubuntu-latest
36+
environment:
37+
name: github-pages
38+
url: ${{ steps.deployment.outputs.page_url }}
39+
steps:
40+
- id: deployment
41+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
dist/
3+
*.log
4+
.DS_Store
5+
.vscode/
6+
.idea/

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Y-app Extension — KG Planning
2+
3+
Standalone iframe extension for [Y-app](https://github.com/OpenAEC-Foundation/Y-app).
4+
Hosts a planning grid + Financieel/Projecten/HR dashboards for KG.
5+
6+
This repo builds into static HTML/JS + CSS, gets deployed to GitHub Pages,
7+
and is loaded inside Y-app through the Extension Manager (a host-side
8+
iframe + postMessage bridge).
9+
10+
## How it plugs into Y-app
11+
12+
1. This repo publishes to `https://<owner>.github.io/Y_App-extension-kg-planning/`.
13+
2. In Y-app → **Settings → Extensions → Add**, paste that URL.
14+
3. Y-app's Extension Manager stores the URL in the instance's SQLite row.
15+
4. When a user opens the extension, Y-app renders:
16+
```html
17+
<iframe sandbox="allow-scripts allow-same-origin"
18+
src="https://…/?host=<origin>&instance=<id>&erpUrl=<url>&lang=<nl|en>" />
19+
```
20+
5. The iframe calls ERPNext via `postMessage` → parent Y-app proxies the
21+
request using the user's ERPNext session. Credentials never reach the
22+
iframe.
23+
24+
## Development
25+
26+
```bash
27+
npm install
28+
npm run dev # http://localhost:5174
29+
npm run build # → dist/
30+
```
31+
32+
Running outside Y-app (`npm run dev` directly) will get "Extension loaded
33+
outside Y-app iframe" errors from the bridge — that's expected. To test
34+
it end-to-end, run Y-app locally and point its Extension Manager at
35+
`http://localhost:5174/`.
36+
37+
## Files that talk to the host
38+
39+
- `src/bridge.ts` — the postMessage RPC. Exports `fetchList`,
40+
`fetchDocument`, `updateDocument`, `callMethod`, `getErpNextAppUrl`,
41+
`getActiveInstanceId`. Replaces what used to be Y-app's
42+
`lib/erpnext.ts` and `lib/instances.ts`.
43+
44+
Everything else is pure React and doesn't know the host exists.

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="nl">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>KG Planning</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "y-app-extension-kg-planning",
3+
"private": true,
4+
"version": "0.1.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"i18next": "^23.11.5",
13+
"lucide-react": "^0.469.0",
14+
"react": "^19.0.0",
15+
"react-dom": "^19.0.0",
16+
"react-i18next": "^14.1.2",
17+
"react-router-dom": "^7.1.1"
18+
},
19+
"devDependencies": {
20+
"@types/react": "^19.0.2",
21+
"@types/react-dom": "^19.0.2",
22+
"@vitejs/plugin-react": "^4.3.4",
23+
"autoprefixer": "^10.4.20",
24+
"postcss": "^8.4.49",
25+
"tailwindcss": "^3.4.17",
26+
"typescript": "~5.6.2",
27+
"vite": "^6.0.5"
28+
}
29+
}

postcss.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
};

src/Page.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Suspense, lazy, useCallback } from "react";
2+
import { useNavigate, useParams } from "react-router-dom";
3+
import { useTranslation } from "react-i18next";
4+
import { RefreshCw } from "lucide-react";
5+
import { KgPlanningProvider, useKgPlanning } from "./store";
6+
import TabBar, { type KgTab } from "./TabBar";
7+
import FilterBar, { EmployeeInfoBar } from "./views/FilterBar";
8+
import TeamView from "./views/TeamView";
9+
import GanttView from "./views/GanttView";
10+
import GridView from "./views/GridView";
11+
12+
const DashboardsRoot = lazy(() => import("./dashboards/Root"));
13+
14+
const VALID_KG_TABS: readonly KgTab[] = ["planning", "financial", "projects", "hr"];
15+
function isValidKgTab(v: string | undefined): v is KgTab {
16+
return !!v && (VALID_KG_TABS as readonly string[]).includes(v);
17+
}
18+
19+
export default function KgPlanningPage() {
20+
const navigate = useNavigate();
21+
const { tab: tabParam } = useParams<{ tab?: string }>();
22+
const tab: KgTab = isValidKgTab(tabParam) ? tabParam : "planning";
23+
const setTab = useCallback((next: KgTab) => {
24+
navigate(next === "planning" ? "/" : `/${next}`);
25+
}, [navigate]);
26+
27+
return (
28+
<div className="flex flex-col h-full bg-white">
29+
<TabBar active={tab} onChange={setTab} />
30+
<div className="flex-1 overflow-hidden">
31+
{tab === "planning" ? (
32+
<KgPlanningProvider>
33+
<PlanningShell />
34+
</KgPlanningProvider>
35+
) : (
36+
<Suspense fallback={<LoadingPanel />}>
37+
<DashboardsRoot tab={tab} />
38+
</Suspense>
39+
)}
40+
</div>
41+
</div>
42+
);
43+
}
44+
45+
function LoadingPanel() {
46+
const { t } = useTranslation();
47+
return (
48+
<div className="flex items-center justify-center h-full text-sm text-slate-500">
49+
{t("extensions.kg_planning.loading")}
50+
</div>
51+
);
52+
}
53+
54+
function PlanningShell() {
55+
const { t } = useTranslation();
56+
const { status, errorMessage, missingFields, reload, activeView, hydration } = useKgPlanning();
57+
58+
if (status === "loading") return <LoadingPanel />;
59+
60+
if (status === "not_configured") {
61+
return (
62+
<div className="flex flex-col items-center justify-center h-full gap-3 p-8 text-center">
63+
<h2 className="text-lg font-semibold text-slate-800">
64+
{t("extensions.kg_planning.not_configured_title")}
65+
</h2>
66+
<p className="max-w-md text-sm text-slate-600">
67+
{t("extensions.kg_planning.not_configured_body")}
68+
</p>
69+
{missingFields.length > 0 && (
70+
<code className="px-3 py-1.5 rounded bg-slate-100 text-xs text-slate-700">
71+
{missingFields.join(", ")}
72+
</code>
73+
)}
74+
</div>
75+
);
76+
}
77+
78+
if (status === "error") {
79+
return (
80+
<div className="m-6 p-4 bg-red-50 border border-red-200 rounded-lg">
81+
<p className="text-sm font-medium text-red-700">{t("extensions.kg_planning.load_failed")}</p>
82+
<p className="mt-1 text-xs text-red-600 break-words">{errorMessage}</p>
83+
<button
84+
onClick={() => void reload()}
85+
className="mt-3 inline-flex items-center gap-1.5 px-3 py-1.5 bg-white border border-red-300 text-red-700 rounded-lg text-xs font-medium hover:bg-red-100 cursor-pointer"
86+
>
87+
<RefreshCw size={12} /> {t("webmail.retry")}
88+
</button>
89+
</div>
90+
);
91+
}
92+
93+
return (
94+
<div className="flex flex-col h-full bg-slate-50">
95+
<FilterBar />
96+
<EmployeeInfoBar />
97+
{hydration && hydration.loaded < hydration.total && (
98+
<div className="relative h-1 bg-slate-200 overflow-hidden" title={`${hydration.loaded} / ${hydration.total}`}>
99+
<div
100+
className="absolute inset-y-0 left-0 bg-teal-500 transition-[width] duration-200 ease-out"
101+
style={{ width: `${Math.round((hydration.loaded / hydration.total) * 100)}%` }}
102+
/>
103+
</div>
104+
)}
105+
<div className="flex-1 overflow-auto">
106+
{activeView === "grid" && <GridView />}
107+
{activeView === "gantt" && <GanttView />}
108+
{activeView === "team" && <TeamView />}
109+
</div>
110+
</div>
111+
);
112+
}

src/TabBar.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useTranslation } from "react-i18next";
2+
3+
export type KgTab = "planning" | "financial" | "projects" | "hr";
4+
5+
interface TabBarProps {
6+
active: KgTab;
7+
onChange: (tab: KgTab) => void;
8+
}
9+
10+
const TABS: { id: KgTab; labelKey: string }[] = [
11+
{ id: "planning", labelKey: "extensions.kg_planning.tab_planning" },
12+
{ id: "financial", labelKey: "extensions.kg_planning.dashboards.tab_financieel" },
13+
{ id: "projects", labelKey: "extensions.kg_planning.dashboards.tab_projecten" },
14+
{ id: "hr", labelKey: "extensions.kg_planning.dashboards.tab_hr" },
15+
];
16+
17+
export default function TabBar({ active, onChange }: TabBarProps) {
18+
const { t } = useTranslation();
19+
return (
20+
<div className="flex gap-1 px-4 pt-3 border-b border-slate-200 bg-white overflow-x-auto">
21+
{TABS.map((tab) => {
22+
const isActive = tab.id === active;
23+
return (
24+
<button
25+
key={tab.id}
26+
onClick={() => onChange(tab.id)}
27+
className={`shrink-0 whitespace-nowrap px-4 py-2 text-sm font-medium rounded-t-lg border-b-2 transition-colors cursor-pointer ${
28+
isActive
29+
? "border-teal-600 text-teal-700 bg-slate-50"
30+
: "border-transparent text-slate-600 hover:text-slate-900 hover:bg-slate-50"
31+
}`}
32+
>
33+
{t(tab.labelKey)}
34+
</button>
35+
);
36+
})}
37+
</div>
38+
);
39+
}

0 commit comments

Comments
 (0)