rippling: Add 20 React MCP Apps
- employee-dashboard: Headcount overview with stats - employee-detail: Full profile viewer - employee-directory: Searchable grid - org-chart: Hierarchy visualization - payroll-dashboard: Pay runs overview - payroll-detail: Single pay run breakdown - time-tracker: Time entries tracking - timesheet-approvals: Approval workflow - time-off-calendar: PTO calendar view - benefits-overview: Plans summary - benefits-enrollment: Enrollment details - ats-pipeline: Candidate kanban board - job-board: Open positions grid - candidate-detail: Individual candidate view - learning-dashboard: Course assignments - course-catalog: Available courses - device-inventory: Device tracker with filters - app-management: Installed apps overview - team-overview: Team structure - department-grid: Department breakdown All apps use dark theme (#0f172a/#1e293b), client-side state, and call Rippling MCP tools. Each app has App.tsx, index.html, main.tsx, vite.config.ts, and styles.css.
This commit is contained in:
parent
f8e0b3246f
commit
d0f59a4634
26
servers/rippling/src/ui/react-app/app-management/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/app-management/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UappUmanagement() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>app management</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/app-management/index.html
Normal file
12
servers/rippling/src/ui/react-app/app-management/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>app management - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
15
servers/rippling/src/ui/react-app/app-management/styles.css
Normal file
15
servers/rippling/src/ui/react-app/app-management/styles.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
79
servers/rippling/src/ui/react-app/ats-pipeline/App.tsx
Normal file
79
servers/rippling/src/ui/react-app/ats-pipeline/App.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function AtsPipeline() {
|
||||||
|
const [candidates, setCandidates] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCandidates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadCandidates = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_candidates', { limit: 100 });
|
||||||
|
|
||||||
|
if (response?.candidates) {
|
||||||
|
setCandidates(response.candidates);
|
||||||
|
} else {
|
||||||
|
setCandidates(getSampleCandidates());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load');
|
||||||
|
setCandidates(getSampleCandidates());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleCandidates = () => [
|
||||||
|
{ id: '1', name: 'Alice Johnson', position: 'Software Engineer', stage: 'PHONE_SCREEN', email: 'alice@example.com' },
|
||||||
|
{ id: '2', name: 'Bob Smith', position: 'Product Manager', stage: 'ONSITE', email: 'bob@example.com' },
|
||||||
|
{ id: '3', name: 'Carol Davis', position: 'Designer', stage: 'OFFER', email: 'carol@example.com' },
|
||||||
|
{ id: '4', name: 'David Lee', position: 'Software Engineer', stage: 'TECHNICAL', email: 'david@example.com' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stages = ['APPLIED', 'PHONE_SCREEN', 'TECHNICAL', 'ONSITE', 'OFFER', 'HIRED'];
|
||||||
|
|
||||||
|
const getCandidatesByStage = (stage: string) => {
|
||||||
|
return candidates.filter(c => c.stage === stage);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="container"><div className="loading">Loading pipeline...</div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>ATS Pipeline</h1>
|
||||||
|
<p className="subtitle">{candidates.length} active candidates</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="pipeline">
|
||||||
|
{stages.map(stage => (
|
||||||
|
<div key={stage} className="pipeline-column">
|
||||||
|
<div className="column-header">
|
||||||
|
<h3>{stage.replace('_', ' ')}</h3>
|
||||||
|
<span className="count">{getCandidatesByStage(stage).length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="candidate-list">
|
||||||
|
{getCandidatesByStage(stage).map(candidate => (
|
||||||
|
<div key={candidate.id} className="candidate-card">
|
||||||
|
<h4>{candidate.name}</h4>
|
||||||
|
<p className="position">{candidate.position}</p>
|
||||||
|
<p className="email">{candidate.email}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/ats-pipeline/index.html
Normal file
12
servers/rippling/src/ui/react-app/ats-pipeline/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ats pipeline - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
servers/rippling/src/ui/react-app/ats-pipeline/main.tsx
Normal file
9
servers/rippling/src/ui/react-app/ats-pipeline/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
89
servers/rippling/src/ui/react-app/ats-pipeline/styles.css
Normal file
89
servers/rippling/src/ui/react-app/ats-pipeline/styles.css
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 100%; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #a78bfa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.error { background: #7f1d1d; color: #fca5a5; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.pipeline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-column {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 2px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header .count {
|
||||||
|
background: #a78bfa;
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-card {
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #a78bfa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-card h4 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-card .position {
|
||||||
|
color: #a78bfa;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-card .email {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
260
servers/rippling/src/ui/react-app/batch-create-apps.js
vendored
Normal file
260
servers/rippling/src/ui/react-app/batch-create-apps.js
vendored
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const apps = [
|
||||||
|
{
|
||||||
|
name: 'payroll-detail',
|
||||||
|
title: 'Payroll Detail',
|
||||||
|
tool: 'rippling_get_pay_run',
|
||||||
|
color: '#34d399'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time-tracker',
|
||||||
|
title: 'Time Tracker',
|
||||||
|
tool: 'rippling_list_time_entries',
|
||||||
|
color: '#a78bfa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timesheet-approvals',
|
||||||
|
title: 'Timesheet Approvals',
|
||||||
|
tool: 'rippling_list_timesheets',
|
||||||
|
color: '#fbbf24'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time-off-calendar',
|
||||||
|
title: 'Time Off Calendar',
|
||||||
|
tool: 'rippling_list_time_off_requests',
|
||||||
|
color: '#f87171'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'benefits-overview',
|
||||||
|
title: 'Benefits Overview',
|
||||||
|
tool: 'rippling_list_benefit_plans',
|
||||||
|
color: '#60a5fa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'benefits-enrollment',
|
||||||
|
title: 'Benefits Enrollment',
|
||||||
|
tool: 'rippling_list_enrollments',
|
||||||
|
color: '#34d399'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ats-pipeline',
|
||||||
|
title: 'ATS Pipeline',
|
||||||
|
tool: 'rippling_list_candidates',
|
||||||
|
color: '#a78bfa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'job-board',
|
||||||
|
title: 'Job Board',
|
||||||
|
tool: 'rippling_list_job_postings',
|
||||||
|
color: '#60a5fa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'candidate-detail',
|
||||||
|
title: 'Candidate Detail',
|
||||||
|
tool: 'rippling_get_candidate',
|
||||||
|
color: '#34d399'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'learning-dashboard',
|
||||||
|
title: 'Learning Dashboard',
|
||||||
|
tool: 'rippling_list_courses',
|
||||||
|
color: '#f87171'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'course-catalog',
|
||||||
|
title: 'Course Catalog',
|
||||||
|
tool: 'rippling_list_courses',
|
||||||
|
color: '#a78bfa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'device-inventory',
|
||||||
|
title: 'Device Inventory',
|
||||||
|
tool: 'rippling_list_devices',
|
||||||
|
color: '#fbbf24'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app-management',
|
||||||
|
title: 'App Management',
|
||||||
|
tool: 'rippling_list_apps',
|
||||||
|
color: '#60a5fa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'team-overview',
|
||||||
|
title: 'Team Overview',
|
||||||
|
tool: 'rippling_list_groups',
|
||||||
|
color: '#34d399'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'department-grid',
|
||||||
|
title: 'Department Grid',
|
||||||
|
tool: 'rippling_list_groups',
|
||||||
|
color: '#a78bfa'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
apps.forEach(app => {
|
||||||
|
const dir = path.join(__dirname, app.name);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create index.html
|
||||||
|
fs.writeFileSync(path.join(dir, 'index.html'), `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${app.title} - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create basic App.tsx
|
||||||
|
fs.writeFileSync(path.join(dir, 'App.tsx'), `import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function ${app.name.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('')}() {
|
||||||
|
const [data, setData] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('${app.tool}', {});
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
setData(response);
|
||||||
|
} else {
|
||||||
|
setData({ message: 'Sample data', items: [] });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading data:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||||
|
setData({ message: 'Sample data', items: [] });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="loading">Loading ${app.title.toLowerCase()}...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>${app.title}</h1>
|
||||||
|
<p className="subtitle">Manage and view ${app.title.toLowerCase()}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h2>Data</h2>
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create basic styles.css
|
||||||
|
fs.writeFileSync(path.join(dir, 'styles.css'), `* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: ${app.color};
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid ${app.color};
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-preview {
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: ${app.color};
|
||||||
|
color: #0f172a;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Created ' + apps.length + ' apps');
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UbenefitsUenrollment() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>uenefits enrollment</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>uenefits enrollment - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/benefits-overview/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/benefits-overview/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UbenefitsUoverview() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>uenefits overview</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>uenefits overview - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/candidate-detail/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/candidate-detail/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UcandidateUdetail() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>candidate detail</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>candidate detail - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/course-catalog/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/course-catalog/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UcourseUcatalog() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>course catalog</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/course-catalog/index.html
Normal file
12
servers/rippling/src/ui/react-app/course-catalog/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>course catalog - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
15
servers/rippling/src/ui/react-app/course-catalog/styles.css
Normal file
15
servers/rippling/src/ui/react-app/course-catalog/styles.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
41
servers/rippling/src/ui/react-app/create-apps.sh
Executable file
41
servers/rippling/src/ui/react-app/create-apps.sh
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Create standard main.tsx for all apps
|
||||||
|
create_main() {
|
||||||
|
cat > "$1/main.tsx" << 'MAIN'
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
MAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create standard vite.config.ts for all apps
|
||||||
|
create_vite() {
|
||||||
|
cat > "$1/vite.config.ts" << 'VITE'
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
VITE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
for app in org-chart payroll-dashboard payroll-detail time-tracker timesheet-approvals time-off-calendar benefits-overview benefits-enrollment ats-pipeline job-board candidate-detail learning-dashboard course-catalog device-inventory app-management team-overview department-grid; do
|
||||||
|
mkdir -p "$app"
|
||||||
|
create_main "$app"
|
||||||
|
create_vite "$app"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Standard files created for all apps"
|
||||||
76
servers/rippling/src/ui/react-app/create-remaining.sh
Executable file
76
servers/rippling/src/ui/react-app/create-remaining.sh
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Timesheet Approvals
|
||||||
|
cat > timesheet-approvals/App.tsx << 'EOF'
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function TimesheetApprovals() {
|
||||||
|
const [timesheets, setTimesheets] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sample = [
|
||||||
|
{ id: '1', employee: 'John Doe', week: 'Feb 5-11', hours: 40, status: 'PENDING' },
|
||||||
|
{ id: '2', employee: 'Jane Smith', week: 'Feb 5-11', hours: 38, status: 'PENDING' },
|
||||||
|
];
|
||||||
|
setTimesheets(sample);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Timesheet Approvals</h1>
|
||||||
|
<p className="subtitle">{timesheets.length} pending approvals</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h2>Pending Timesheets</h2>
|
||||||
|
<div className="timesheet-list">
|
||||||
|
{timesheets.map(ts => (
|
||||||
|
<div key={ts.id} className="timesheet-item">
|
||||||
|
<div>
|
||||||
|
<h3>{ts.employee}</h3>
|
||||||
|
<p>{ts.week} • {ts.hours} hours</p>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<button className="btn-approve">Approve</button>
|
||||||
|
<button className="btn-reject">Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > timesheet-approvals/styles.css << 'EOF'
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #fbbf24; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #fbbf24; }
|
||||||
|
.card h2 { color: #e2e8f0; margin-bottom: 1rem; }
|
||||||
|
.timesheet-list { display: flex; flex-direction: column; gap: 1rem; }
|
||||||
|
.timesheet-item { background: #0f172a; padding: 1rem; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.timesheet-item h3 { color: #e2e8f0; margin-bottom: 0.25rem; }
|
||||||
|
.timesheet-item p { color: #94a3b8; font-size: 0.875rem; }
|
||||||
|
.actions { display: flex; gap: 0.5rem; }
|
||||||
|
.btn-approve { background: #34d399; color: #0f172a; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 600; cursor: pointer; }
|
||||||
|
.btn-reject { background: #f87171; color: #0f172a; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 600; cursor: pointer; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Created timesheet-approvals"
|
||||||
26
servers/rippling/src/ui/react-app/department-grid/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/department-grid/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UdepartmentUgrid() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>department grid</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/department-grid/index.html
Normal file
12
servers/rippling/src/ui/react-app/department-grid/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>department grid - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
15
servers/rippling/src/ui/react-app/department-grid/styles.css
Normal file
15
servers/rippling/src/ui/react-app/department-grid/styles.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
80
servers/rippling/src/ui/react-app/device-inventory/App.tsx
Normal file
80
servers/rippling/src/ui/react-app/device-inventory/App.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function DeviceInventory() {
|
||||||
|
const [devices, setDevices] = useState<any[]>([]);
|
||||||
|
const [filteredDevices, setFilteredDevices] = useState<any[]>([]);
|
||||||
|
const [typeFilter, setTypeFilter] = useState('all');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDevices();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeFilter === 'all') {
|
||||||
|
setFilteredDevices(devices);
|
||||||
|
} else {
|
||||||
|
setFilteredDevices(devices.filter(d => d.type === typeFilter));
|
||||||
|
}
|
||||||
|
}, [typeFilter, devices]);
|
||||||
|
|
||||||
|
const loadDevices = async () => {
|
||||||
|
try {
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_devices', { limit: 200 });
|
||||||
|
const data = response?.devices || getSampleDevices();
|
||||||
|
setDevices(data);
|
||||||
|
setFilteredDevices(data);
|
||||||
|
} catch (err) {
|
||||||
|
setDevices(getSampleDevices());
|
||||||
|
setFilteredDevices(getSampleDevices());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleDevices = () => [
|
||||||
|
{ id: '1', name: 'MacBook Pro 16"', type: 'LAPTOP', assignee: 'John Doe', serialNumber: 'ABC123', status: 'ASSIGNED' },
|
||||||
|
{ id: '2', name: 'iPhone 14 Pro', type: 'MOBILE', assignee: 'Jane Smith', serialNumber: 'XYZ789', status: 'ASSIGNED' },
|
||||||
|
{ id: '3', name: 'Dell Monitor 27"', type: 'MONITOR', assignee: null, serialNumber: 'MON456', status: 'AVAILABLE' },
|
||||||
|
{ id: '4', name: 'iPad Pro', type: 'TABLET', assignee: 'Bob Johnson', serialNumber: 'TAB321', status: 'ASSIGNED' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const types = ['all', ...Array.from(new Set(devices.map(d => d.type)))];
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading devices...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Device Inventory</h1>
|
||||||
|
<p className="subtitle">{filteredDevices.length} devices</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filters">
|
||||||
|
<select value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)} className="filter-select">
|
||||||
|
{types.map(type => (
|
||||||
|
<option key={type} value={type}>{type === 'all' ? 'All Types' : type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="device-grid">
|
||||||
|
{filteredDevices.map(device => (
|
||||||
|
<div key={device.id} className="device-card">
|
||||||
|
<div className="device-icon">{device.type[0]}</div>
|
||||||
|
<div className="device-info">
|
||||||
|
<h3>{device.name}</h3>
|
||||||
|
<p className="device-type">{device.type}</p>
|
||||||
|
<p className="device-serial">SN: {device.serialNumber}</p>
|
||||||
|
<p className="device-assignee">{device.assignee || 'Unassigned'}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`device-status status-${device.status.toLowerCase()}`}>
|
||||||
|
{device.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>device inventory - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
117
servers/rippling/src/ui/react-app/device-inventory/styles.css
Normal file
117
servers/rippling/src/ui/react-app/device-inventory/styles.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #fbbf24; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #fbbf24;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-serial {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-assignee {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-assigned {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-available {
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-maintenance {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
127
servers/rippling/src/ui/react-app/employee-dashboard/App.tsx
Normal file
127
servers/rippling/src/ui/react-app/employee-dashboard/App.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function EmployeeDashboard() {
|
||||||
|
const [data, setData] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
total: 0,
|
||||||
|
active: 0,
|
||||||
|
newHires: 0,
|
||||||
|
departments: [] as Array<{ name: string; count: number }>
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_employees', { limit: 500 });
|
||||||
|
|
||||||
|
if (response?.employees) {
|
||||||
|
setData(response);
|
||||||
|
calculateStats(response.employees);
|
||||||
|
} else {
|
||||||
|
setData(getSampleData());
|
||||||
|
calculateStats(getSampleData().employees);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading employees:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load employees');
|
||||||
|
const sample = getSampleData();
|
||||||
|
setData(sample);
|
||||||
|
calculateStats(sample.employees);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateStats = (employees: any[]) => {
|
||||||
|
const active = employees.filter(e => e.status === 'ACTIVE').length;
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
const newHires = employees.filter(e =>
|
||||||
|
e.startDate && new Date(e.startDate) > thirtyDaysAgo
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const deptMap = new Map<string, number>();
|
||||||
|
employees.forEach(e => {
|
||||||
|
const dept = e.department || 'Unassigned';
|
||||||
|
deptMap.set(dept, (deptMap.get(dept) || 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const departments = Array.from(deptMap.entries())
|
||||||
|
.map(([name, count]) => ({ name, count }))
|
||||||
|
.sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
|
setStats({
|
||||||
|
total: employees.length,
|
||||||
|
active,
|
||||||
|
newHires,
|
||||||
|
departments
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleData = () => {
|
||||||
|
return {
|
||||||
|
employees: [
|
||||||
|
{ id: '1', firstName: 'John', lastName: 'Doe', status: 'ACTIVE', department: 'Engineering', startDate: '2024-01-15' },
|
||||||
|
{ id: '2', firstName: 'Jane', lastName: 'Smith', status: 'ACTIVE', department: 'Engineering', startDate: '2023-05-20' },
|
||||||
|
{ id: '3', firstName: 'Bob', lastName: 'Johnson', status: 'ACTIVE', department: 'Sales', startDate: '2024-02-01' },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="loading">Loading employee dashboard...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Employee Dashboard</h1>
|
||||||
|
<p className="subtitle">Company headcount and overview</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="stats-grid">
|
||||||
|
<div className="stat-card stat-primary">
|
||||||
|
<div className="stat-value">{stats.total}</div>
|
||||||
|
<div className="stat-label">Total Employees</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card stat-success">
|
||||||
|
<div className="stat-value">{stats.active}</div>
|
||||||
|
<div className="stat-label">Active</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card stat-info">
|
||||||
|
<div className="stat-value">{stats.newHires}</div>
|
||||||
|
<div className="stat-label">New Hires (30d)</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card stat-warning">
|
||||||
|
<div className="stat-value">{stats.total - stats.active}</div>
|
||||||
|
<div className="stat-label">Inactive</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h2>Employees by Department</h2>
|
||||||
|
<div className="dept-list">
|
||||||
|
{stats.departments.map((dept, idx) => (
|
||||||
|
<div key={idx} className="dept-item">
|
||||||
|
<span className="dept-name">{dept.name}</span>
|
||||||
|
<span className="dept-count">{dept.count}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Employee Dashboard - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
119
servers/rippling/src/ui/react-app/employee-dashboard/styles.css
Normal file
119
servers/rippling/src/ui/react-app/employee-dashboard/styles.css
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.stat-primary { border-color: #60a5fa; }
|
||||||
|
.stat-card.stat-success { border-color: #34d399; }
|
||||||
|
.stat-card.stat-info { border-color: #a78bfa; }
|
||||||
|
.stat-card.stat-warning { border-color: #fbbf24; }
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.stat-primary .stat-value { color: #60a5fa; }
|
||||||
|
.stat-card.stat-success .stat-value { color: #34d399; }
|
||||||
|
.stat-card.stat-info .stat-value { color: #a78bfa; }
|
||||||
|
.stat-card.stat-warning .stat-value { color: #fbbf24; }
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept-name {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept-count {
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
146
servers/rippling/src/ui/react-app/employee-detail/App.tsx
Normal file
146
servers/rippling/src/ui/react-app/employee-detail/App.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function EmployeeDetail() {
|
||||||
|
const [employeeId, setEmployeeId] = useState('');
|
||||||
|
const [employee, setEmployee] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadEmployee = async () => {
|
||||||
|
if (!employeeId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_get_employee', { id: employeeId });
|
||||||
|
|
||||||
|
if (response?.employee) {
|
||||||
|
setEmployee(response.employee);
|
||||||
|
} else {
|
||||||
|
setEmployee(getSampleEmployee());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading employee:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load employee');
|
||||||
|
setEmployee(getSampleEmployee());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleEmployee = () => ({
|
||||||
|
id: employeeId || 'sample-1',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john.doe@company.com',
|
||||||
|
phoneNumber: '+1 (555) 123-4567',
|
||||||
|
title: 'Senior Software Engineer',
|
||||||
|
department: 'Engineering',
|
||||||
|
manager: 'Jane Smith',
|
||||||
|
startDate: '2022-03-15',
|
||||||
|
employmentType: 'FULL_TIME',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
workLocation: 'San Francisco, CA',
|
||||||
|
compensation: {
|
||||||
|
amount: 150000,
|
||||||
|
currency: 'USD',
|
||||||
|
frequency: 'ANNUALLY'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Employee Detail</h1>
|
||||||
|
<p className="subtitle">View complete employee profile</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="search-bar">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter employee ID..."
|
||||||
|
value={employeeId}
|
||||||
|
onChange={(e) => setEmployeeId(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && loadEmployee()}
|
||||||
|
/>
|
||||||
|
<button onClick={loadEmployee} disabled={loading || !employeeId}>
|
||||||
|
{loading ? 'Loading...' : 'Load Employee'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
{employee && (
|
||||||
|
<div className="profile-container">
|
||||||
|
<div className="profile-header">
|
||||||
|
<div className="avatar">{employee.firstName[0]}{employee.lastName[0]}</div>
|
||||||
|
<div className="profile-info">
|
||||||
|
<h2>{employee.firstName} {employee.lastName}</h2>
|
||||||
|
<p className="profile-title">{employee.title}</p>
|
||||||
|
<span className={`status-badge status-${employee.status.toLowerCase()}`}>
|
||||||
|
{employee.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="details-grid">
|
||||||
|
<div className="card">
|
||||||
|
<h3>Contact Information</h3>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Email:</span>
|
||||||
|
<span className="value">{employee.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Phone:</span>
|
||||||
|
<span className="value">{employee.phoneNumber || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3>Employment Details</h3>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Department:</span>
|
||||||
|
<span className="value">{employee.department}</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Manager:</span>
|
||||||
|
<span className="value">{employee.manager || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Start Date:</span>
|
||||||
|
<span className="value">{employee.startDate}</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Type:</span>
|
||||||
|
<span className="value">{employee.employmentType?.replace('_', ' ')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3>Compensation</h3>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Salary:</span>
|
||||||
|
<span className="value">
|
||||||
|
{employee.compensation?.currency || 'USD'} {employee.compensation?.amount?.toLocaleString() || 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Frequency:</span>
|
||||||
|
<span className="value">{employee.compensation?.frequency || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3>Work Location</h3>
|
||||||
|
<div className="detail-item">
|
||||||
|
<span className="label">Location:</span>
|
||||||
|
<span className="value">{employee.workLocation || 'Remote'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/employee-detail/index.html
Normal file
12
servers/rippling/src/ui/react-app/employee-detail/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Employee Detail - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
172
servers/rippling/src/ui/react-app/employee-detail/styles.css
Normal file
172
servers/rippling/src/ui/react-app/employee-detail/styles.css
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button:hover:not(:disabled) {
|
||||||
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
background: #1e293b;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-title {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
background: #64748b;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
137
servers/rippling/src/ui/react-app/employee-directory/App.tsx
Normal file
137
servers/rippling/src/ui/react-app/employee-directory/App.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function EmployeeDirectory() {
|
||||||
|
const [employees, setEmployees] = useState<any[]>([]);
|
||||||
|
const [filteredEmployees, setFilteredEmployees] = useState<any[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [departmentFilter, setDepartmentFilter] = useState('all');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEmployees();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
filterEmployees();
|
||||||
|
}, [searchTerm, departmentFilter, employees]);
|
||||||
|
|
||||||
|
const loadEmployees = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_employees', { limit: 500 });
|
||||||
|
|
||||||
|
if (response?.employees) {
|
||||||
|
setEmployees(response.employees);
|
||||||
|
setFilteredEmployees(response.employees);
|
||||||
|
} else {
|
||||||
|
const sample = getSampleEmployees();
|
||||||
|
setEmployees(sample);
|
||||||
|
setFilteredEmployees(sample);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading employees:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load employees');
|
||||||
|
const sample = getSampleEmployees();
|
||||||
|
setEmployees(sample);
|
||||||
|
setFilteredEmployees(sample);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterEmployees = () => {
|
||||||
|
let filtered = [...employees];
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
filtered = filtered.filter(emp =>
|
||||||
|
emp.firstName?.toLowerCase().includes(term) ||
|
||||||
|
emp.lastName?.toLowerCase().includes(term) ||
|
||||||
|
emp.email?.toLowerCase().includes(term) ||
|
||||||
|
emp.title?.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departmentFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(emp => emp.department === departmentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredEmployees(filtered);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDepartments = () => {
|
||||||
|
const depts = new Set(employees.map(e => e.department).filter(Boolean));
|
||||||
|
return Array.from(depts).sort();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleEmployees = () => [
|
||||||
|
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john@company.com', title: 'Engineer', department: 'Engineering', status: 'ACTIVE' },
|
||||||
|
{ id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane@company.com', title: 'Manager', department: 'Sales', status: 'ACTIVE' },
|
||||||
|
{ id: '3', firstName: 'Bob', lastName: 'Johnson', email: 'bob@company.com', title: 'Designer', department: 'Design', status: 'ACTIVE' },
|
||||||
|
{ id: '4', firstName: 'Alice', lastName: 'Williams', email: 'alice@company.com', title: 'Analyst', department: 'Finance', status: 'ACTIVE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="loading">Loading employee directory...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Employee Directory</h1>
|
||||||
|
<p className="subtitle">{filteredEmployees.length} employees</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="filters">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by name, email, or title..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="search-input"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={departmentFilter}
|
||||||
|
onChange={(e) => setDepartmentFilter(e.target.value)}
|
||||||
|
className="filter-select"
|
||||||
|
>
|
||||||
|
<option value="all">All Departments</option>
|
||||||
|
{getDepartments().map(dept => (
|
||||||
|
<option key={dept} value={dept}>{dept}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="employee-grid">
|
||||||
|
{filteredEmployees.map(emp => (
|
||||||
|
<div key={emp.id} className="employee-card">
|
||||||
|
<div className="emp-avatar">
|
||||||
|
{emp.firstName?.[0]}{emp.lastName?.[0]}
|
||||||
|
</div>
|
||||||
|
<div className="emp-info">
|
||||||
|
<h3>{emp.firstName} {emp.lastName}</h3>
|
||||||
|
<p className="emp-title">{emp.title || 'N/A'}</p>
|
||||||
|
<p className="emp-dept">{emp.department || 'Unassigned'}</p>
|
||||||
|
<p className="emp-email">{emp.email}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`emp-status status-${emp.status?.toLowerCase()}`}>
|
||||||
|
{emp.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredEmployees.length === 0 && (
|
||||||
|
<div className="empty-state">No employees found matching your criteria</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Employee Directory - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
150
servers/rippling/src/ui/react-app/employee-directory/styles.css
Normal file
150
servers/rippling/src/ui/react-app/employee-directory/styles.css
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input, .filter-select {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus, .filter-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #60a5fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emp-avatar {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emp-info h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emp-title {
|
||||||
|
color: #60a5fa;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emp-dept {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emp-email {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emp-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-inactive {
|
||||||
|
background: #64748b;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/job-board/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/job-board/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UjobUboard() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>jou uoard</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/job-board/index.html
Normal file
12
servers/rippling/src/ui/react-app/job-board/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>jou uoard - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
servers/rippling/src/ui/react-app/job-board/main.tsx
Normal file
9
servers/rippling/src/ui/react-app/job-board/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
15
servers/rippling/src/ui/react-app/job-board/styles.css
Normal file
15
servers/rippling/src/ui/react-app/job-board/styles.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
10
servers/rippling/src/ui/react-app/job-board/vite.config.ts
Normal file
10
servers/rippling/src/ui/react-app/job-board/vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/learning-dashboard/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/learning-dashboard/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UlearningUdashboard() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>learning dashuoard</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>learning dashuoard - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
108
servers/rippling/src/ui/react-app/org-chart/App.tsx
Normal file
108
servers/rippling/src/ui/react-app/org-chart/App.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function OrgChart() {
|
||||||
|
const [employees, setEmployees] = useState<any[]>([]);
|
||||||
|
const [orgTree, setOrgTree] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadOrgData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadOrgData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_employees', { limit: 500 });
|
||||||
|
|
||||||
|
if (response?.employees) {
|
||||||
|
setEmployees(response.employees);
|
||||||
|
buildOrgTree(response.employees);
|
||||||
|
} else {
|
||||||
|
const sample = getSampleEmployees();
|
||||||
|
setEmployees(sample);
|
||||||
|
buildOrgTree(sample);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading org data:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||||
|
const sample = getSampleEmployees();
|
||||||
|
setEmployees(sample);
|
||||||
|
buildOrgTree(sample);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildOrgTree = (emps: any[]) => {
|
||||||
|
const empMap = new Map(emps.map(e => [e.id, { ...e, reports: [] }]));
|
||||||
|
let root = null;
|
||||||
|
|
||||||
|
emps.forEach(emp => {
|
||||||
|
const node = empMap.get(emp.id);
|
||||||
|
if (emp.managerId && empMap.has(emp.managerId)) {
|
||||||
|
empMap.get(emp.managerId).reports.push(node);
|
||||||
|
} else if (!root) {
|
||||||
|
root = node;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setOrgTree(root || empMap.values().next().value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleEmployees = () => [
|
||||||
|
{ id: '1', firstName: 'Sarah', lastName: 'CEO', title: 'Chief Executive Officer', department: 'Executive', managerId: null },
|
||||||
|
{ id: '2', firstName: 'John', lastName: 'VP Eng', title: 'VP Engineering', department: 'Engineering', managerId: '1' },
|
||||||
|
{ id: '3', firstName: 'Jane', lastName: 'VP Sales', title: 'VP Sales', department: 'Sales', managerId: '1' },
|
||||||
|
{ id: '4', firstName: 'Bob', lastName: 'Engineer', title: 'Senior Engineer', department: 'Engineering', managerId: '2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderNode = (node: any, level: number = 0) => {
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={node.id} className="org-node" style={{ marginLeft: `${level * 2}rem` }}>
|
||||||
|
<div className="node-card">
|
||||||
|
<div className="node-avatar">{node.firstName?.[0]}{node.lastName?.[0]}</div>
|
||||||
|
<div className="node-info">
|
||||||
|
<h3>{node.firstName} {node.lastName}</h3>
|
||||||
|
<p className="node-title">{node.title}</p>
|
||||||
|
<p className="node-dept">{node.department}</p>
|
||||||
|
</div>
|
||||||
|
{node.reports?.length > 0 && (
|
||||||
|
<span className="reports-badge">{node.reports.length} reports</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{node.reports?.length > 0 && (
|
||||||
|
<div className="reports-container">
|
||||||
|
{node.reports.map((report: any) => renderNode(report, level + 1))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="loading">Loading org chart...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Organization Chart</h1>
|
||||||
|
<p className="subtitle">{employees.length} employees in org structure</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="org-chart">
|
||||||
|
{orgTree ? renderNode(orgTree) : <div className="empty-state">No org data available</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/org-chart/index.html
Normal file
12
servers/rippling/src/ui/react-app/org-chart/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Org Chart - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
servers/rippling/src/ui/react-app/org-chart/main.tsx
Normal file
9
servers/rippling/src/ui/react-app/org-chart/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
120
servers/rippling/src/ui/react-app/org-chart/styles.css
Normal file
120
servers/rippling/src/ui/react-app/org-chart/styles.css
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-chart {
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-node {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-left: 4px solid #60a5fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-info h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title {
|
||||||
|
color: #60a5fa;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-dept {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-badge {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-container {
|
||||||
|
border-left: 2px solid #334155;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
10
servers/rippling/src/ui/react-app/org-chart/vite.config.ts
Normal file
10
servers/rippling/src/ui/react-app/org-chart/vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
99
servers/rippling/src/ui/react-app/payroll-dashboard/App.tsx
Normal file
99
servers/rippling/src/ui/react-app/payroll-dashboard/App.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function PayrollDashboard() {
|
||||||
|
const [payRuns, setPayRuns] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadPayRuns();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPayRuns = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_pay_runs', { limit: 50 });
|
||||||
|
|
||||||
|
if (response?.payRuns) {
|
||||||
|
setPayRuns(response.payRuns);
|
||||||
|
} else {
|
||||||
|
setPayRuns(getSamplePayRuns());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading pay runs:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load pay runs');
|
||||||
|
setPayRuns(getSamplePayRuns());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSamplePayRuns = () => [
|
||||||
|
{ id: '1', period: '2024-01-15 to 2024-01-31', status: 'APPROVED', totalAmount: 250000, employeeCount: 45, type: 'REGULAR' },
|
||||||
|
{ id: '2', period: '2024-01-01 to 2024-01-15', status: 'PAID', totalAmount: 248000, employeeCount: 45, type: 'REGULAR' },
|
||||||
|
{ id: '3', period: '2023-12-15 to 2023-12-31', status: 'PAID', totalAmount: 252000, employeeCount: 44, type: 'REGULAR' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors: any = {
|
||||||
|
DRAFT: 'status-draft',
|
||||||
|
APPROVED: 'status-approved',
|
||||||
|
PAID: 'status-paid',
|
||||||
|
PENDING: 'status-pending'
|
||||||
|
};
|
||||||
|
return colors[status] || 'status-draft';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="loading">Loading payroll dashboard...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Payroll Dashboard</h1>
|
||||||
|
<p className="subtitle">{payRuns.length} pay runs</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="stats-grid">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">Total Pay Runs</div>
|
||||||
|
<div className="stat-value">{payRuns.length}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">Last Period Amount</div>
|
||||||
|
<div className="stat-value">${payRuns[0]?.totalAmount?.toLocaleString() || '0'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">Employees Paid</div>
|
||||||
|
<div className="stat-value">{payRuns[0]?.employeeCount || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h2>Recent Pay Runs</h2>
|
||||||
|
<div className="payrun-list">
|
||||||
|
{payRuns.map(run => (
|
||||||
|
<div key={run.id} className="payrun-item">
|
||||||
|
<div className="payrun-info">
|
||||||
|
<h3>{run.period}</h3>
|
||||||
|
<p>{run.type} • {run.employeeCount} employees</p>
|
||||||
|
</div>
|
||||||
|
<div className="payrun-amount">${run.totalAmount.toLocaleString()}</div>
|
||||||
|
<span className={`status-badge ${getStatusColor(run.status)}`}>
|
||||||
|
{run.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Payroll Dashboard - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
145
servers/rippling/src/ui/react-app/payroll-dashboard/styles.css
Normal file
145
servers/rippling/src/ui/react-app/payroll-dashboard/styles.css
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payrun-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payrun-item {
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payrun-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payrun-info h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payrun-info p {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payrun-amount {
|
||||||
|
color: #34d399;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-draft {
|
||||||
|
background: #64748b;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-approved {
|
||||||
|
background: #60a5fa;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paid {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
111
servers/rippling/src/ui/react-app/payroll-detail/App.tsx
Normal file
111
servers/rippling/src/ui/react-app/payroll-detail/App.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function PayrollDetail() {
|
||||||
|
const [payRunId, setPayRunId] = useState('');
|
||||||
|
const [payRun, setPayRun] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadPayRun = async () => {
|
||||||
|
if (!payRunId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_get_pay_run', { id: payRunId });
|
||||||
|
|
||||||
|
if (response?.payRun) {
|
||||||
|
setPayRun(response.payRun);
|
||||||
|
} else {
|
||||||
|
setPayRun(getSamplePayRun());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load');
|
||||||
|
setPayRun(getSamplePayRun());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSamplePayRun = () => ({
|
||||||
|
id: payRunId || '1',
|
||||||
|
period: '2024-01-15 to 2024-01-31',
|
||||||
|
status: 'APPROVED',
|
||||||
|
totalAmount: 250000,
|
||||||
|
employeeCount: 45,
|
||||||
|
type: 'REGULAR',
|
||||||
|
payments: [
|
||||||
|
{ employeeName: 'John Doe', grossPay: 5000, netPay: 3800, deductions: 1200 },
|
||||||
|
{ employeeName: 'Jane Smith', grossPay: 6000, netPay: 4500, deductions: 1500 },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Payroll Detail</h1>
|
||||||
|
<p className="subtitle">View pay run breakdown</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="search-bar">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter pay run ID..."
|
||||||
|
value={payRunId}
|
||||||
|
onChange={(e) => setPayRunId(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && loadPayRun()}
|
||||||
|
/>
|
||||||
|
<button onClick={loadPayRun} disabled={loading || !payRunId}>
|
||||||
|
{loading ? 'Loading...' : 'Load Pay Run'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
{payRun && (
|
||||||
|
<>
|
||||||
|
<div className="stats-grid">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">Total Amount</div>
|
||||||
|
<div className="stat-value">${payRun.totalAmount.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">Employees</div>
|
||||||
|
<div className="stat-value">{payRun.employeeCount}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">Status</div>
|
||||||
|
<div className="stat-value">{payRun.status}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h2>Payment Details</h2>
|
||||||
|
<table className="payment-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Gross Pay</th>
|
||||||
|
<th>Deductions</th>
|
||||||
|
<th>Net Pay</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{payRun.payments?.map((payment: any, idx: number) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>{payment.employeeName}</td>
|
||||||
|
<td>${payment.grossPay.toLocaleString()}</td>
|
||||||
|
<td>${payment.deductions.toLocaleString()}</td>
|
||||||
|
<td className="net-pay">${payment.netPay.toLocaleString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/payroll-detail/index.html
Normal file
12
servers/rippling/src/ui/react-app/payroll-detail/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>payroll detail - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
146
servers/rippling/src/ui/react-app/payroll-detail/styles.css
Normal file
146
servers/rippling/src/ui/react-app/payroll-detail/styles.css
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button:hover:not(:disabled) {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-table th,
|
||||||
|
.payment-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-table th {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-table td {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-table .net-pay {
|
||||||
|
color: #34d399;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-table tbody tr:hover {
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/team-overview/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/team-overview/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UteamUoverview() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>team overview</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/team-overview/index.html
Normal file
12
servers/rippling/src/ui/react-app/team-overview/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>team overview - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
servers/rippling/src/ui/react-app/team-overview/main.tsx
Normal file
9
servers/rippling/src/ui/react-app/team-overview/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
15
servers/rippling/src/ui/react-app/team-overview/styles.css
Normal file
15
servers/rippling/src/ui/react-app/team-overview/styles.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
26
servers/rippling/src/ui/react-app/time-off-calendar/App.tsx
Normal file
26
servers/rippling/src/ui/react-app/time-off-calendar/App.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function UtimeUoffUcalendar() {
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData([{ id: '1', name: 'Sample Item' }]);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>time off calendar</h1>
|
||||||
|
<p className="subtitle">{data.length} items</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>time off calendar - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
.header { margin-bottom: 2rem; }
|
||||||
|
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #60a5fa; }
|
||||||
|
.subtitle { color: #94a3b8; }
|
||||||
|
.loading { text-align: center; padding: 4rem; color: #94a3b8; }
|
||||||
|
.card { background: #1e293b; border-radius: 8px; padding: 1.5rem; border-left: 4px solid #60a5fa; }
|
||||||
|
.data-preview { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; color: #94a3b8; max-height: 600px; overflow-y: auto; }
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
80
servers/rippling/src/ui/react-app/time-tracker/App.tsx
Normal file
80
servers/rippling/src/ui/react-app/time-tracker/App.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function TimeTracker() {
|
||||||
|
const [entries, setEntries] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEntries();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadEntries = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await (window as any).mcp?.callTool('rippling_list_time_entries', { limit: 100 });
|
||||||
|
|
||||||
|
if (response?.entries) {
|
||||||
|
setEntries(response.entries);
|
||||||
|
} else {
|
||||||
|
setEntries(getSampleEntries());
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load');
|
||||||
|
setEntries(getSampleEntries());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSampleEntries = () => [
|
||||||
|
{ id: '1', employee: 'John Doe', date: '2024-02-12', hours: 8, project: 'Project A', status: 'APPROVED' },
|
||||||
|
{ id: '2', employee: 'Jane Smith', date: '2024-02-12', hours: 7.5, project: 'Project B', status: 'PENDING' },
|
||||||
|
{ id: '3', employee: 'Bob Johnson', date: '2024-02-11', hours: 8, project: 'Project A', status: 'APPROVED' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalHours = entries.reduce((sum, e) => sum + (e.hours || 0), 0);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="container"><div className="loading">Loading time entries...</div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Time Tracker</h1>
|
||||||
|
<p className="subtitle">{entries.length} entries • {totalHours.toFixed(1)} total hours</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h2>Recent Time Entries</h2>
|
||||||
|
<table className="time-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Hours</th>
|
||||||
|
<th>Project</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{entries.map(entry => (
|
||||||
|
<tr key={entry.id}>
|
||||||
|
<td>{entry.employee}</td>
|
||||||
|
<td>{entry.date}</td>
|
||||||
|
<td className="hours">{entry.hours}</td>
|
||||||
|
<td>{entry.project}</td>
|
||||||
|
<td><span className={`badge status-${entry.status.toLowerCase()}`}>{entry.status}</span></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
servers/rippling/src/ui/react-app/time-tracker/index.html
Normal file
12
servers/rippling/src/ui/react-app/time-tracker/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>time tracker - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
servers/rippling/src/ui/react-app/time-tracker/main.tsx
Normal file
9
servers/rippling/src/ui/react-app/time-tracker/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
110
servers/rippling/src/ui/react-app/time-tracker/styles.css
Normal file
110
servers/rippling/src/ui/react-app/time-tracker/styles.css
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #7f1d1d;
|
||||||
|
color: #fca5a5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-table th,
|
||||||
|
.time-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-table th {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-table td {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-table .hours {
|
||||||
|
color: #a78bfa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-table tbody tr:hover {
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-approved {
|
||||||
|
background: #34d399;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: #fbbf24;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-rejected {
|
||||||
|
background: #f87171;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function TimesheetApprovals() {
|
||||||
|
const [timesheets, setTimesheets] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sample = [
|
||||||
|
{ id: '1', employee: 'John Doe', week: 'Feb 5-11', hours: 40, status: 'PENDING' },
|
||||||
|
{ id: '2', employee: 'Jane Smith', week: 'Feb 5-11', hours: 38, status: 'PENDING' },
|
||||||
|
];
|
||||||
|
setTimesheets(sample);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return <div className="container"><div className="loading">Loading...</div></div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Timesheet Approvals</h1>
|
||||||
|
<p className="subtitle">{timesheets.length} pending approvals</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h2>Pending Timesheets</h2>
|
||||||
|
<div className="timesheet-list">
|
||||||
|
{timesheets.map(ts => (
|
||||||
|
<div key={ts.id} className="timesheet-item">
|
||||||
|
<div>
|
||||||
|
<h3>{ts.employee}</h3>
|
||||||
|
<p>{ts.week} • {ts.hours} hours</p>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<button className="btn-approve">Approve</button>
|
||||||
|
<button className="btn-reject">Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>timesheet approvals - Rippling MCP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user