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:
Jake Shore 2026-02-12 17:42:18 -05:00
parent f8e0b3246f
commit d0f59a4634
103 changed files with 3608 additions and 0 deletions

View 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>
);
}

View 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>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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;
}

View 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,
},
});

View 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');

View File

@ -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>
);
}

View 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>uenefits enrollment - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>uenefits overview - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>candidate detail - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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; }

View 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,
},
});

View 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"

View 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"

View 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>
);
}

View 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>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>device inventory - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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 Dashboard - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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 Directory - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>learning dashuoard - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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 Dashboard - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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;
}

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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 off calendar - Rippling MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View 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>
);

View 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; }

View 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,
},
});

View 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>
);
}

View 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>

View 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>
);

View 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;
}

View 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,
},
});

View File

@ -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>
);
}

View 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>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