From d0f59a4634b8abcfc1d946b16a4485d0d2250a79 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 12 Feb 2026 17:42:18 -0500 Subject: [PATCH] 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. --- .../src/ui/react-app/app-management/App.tsx | 26 ++ .../ui/react-app/app-management/index.html | 12 + .../src/ui/react-app/app-management/main.tsx | 9 + .../ui/react-app/app-management/styles.css | 15 + .../react-app/app-management/vite.config.ts | 10 + .../src/ui/react-app/ats-pipeline/App.tsx | 79 ++++++ .../src/ui/react-app/ats-pipeline/index.html | 12 + .../src/ui/react-app/ats-pipeline/main.tsx | 9 + .../src/ui/react-app/ats-pipeline/styles.css | 89 ++++++ .../ui/react-app/ats-pipeline/vite.config.ts | 10 + .../src/ui/react-app/batch-create-apps.js | 260 ++++++++++++++++++ .../ui/react-app/benefits-enrollment/App.tsx | 26 ++ .../react-app/benefits-enrollment/index.html | 12 + .../ui/react-app/benefits-enrollment/main.tsx | 9 + .../react-app/benefits-enrollment/styles.css | 15 + .../benefits-enrollment/vite.config.ts | 10 + .../ui/react-app/benefits-overview/App.tsx | 26 ++ .../ui/react-app/benefits-overview/index.html | 12 + .../ui/react-app/benefits-overview/main.tsx | 9 + .../ui/react-app/benefits-overview/styles.css | 15 + .../benefits-overview/vite.config.ts | 10 + .../src/ui/react-app/candidate-detail/App.tsx | 26 ++ .../ui/react-app/candidate-detail/index.html | 12 + .../ui/react-app/candidate-detail/main.tsx | 9 + .../ui/react-app/candidate-detail/styles.css | 15 + .../react-app/candidate-detail/vite.config.ts | 10 + .../src/ui/react-app/course-catalog/App.tsx | 26 ++ .../ui/react-app/course-catalog/index.html | 12 + .../src/ui/react-app/course-catalog/main.tsx | 9 + .../ui/react-app/course-catalog/styles.css | 15 + .../react-app/course-catalog/vite.config.ts | 10 + .../rippling/src/ui/react-app/create-apps.sh | 41 +++ .../src/ui/react-app/create-remaining.sh | 76 +++++ .../src/ui/react-app/department-grid/App.tsx | 26 ++ .../ui/react-app/department-grid/index.html | 12 + .../src/ui/react-app/department-grid/main.tsx | 9 + .../ui/react-app/department-grid/styles.css | 15 + .../react-app/department-grid/vite.config.ts | 10 + .../src/ui/react-app/device-inventory/App.tsx | 80 ++++++ .../ui/react-app/device-inventory/index.html | 12 + .../ui/react-app/device-inventory/main.tsx | 9 + .../ui/react-app/device-inventory/styles.css | 117 ++++++++ .../react-app/device-inventory/vite.config.ts | 10 + .../ui/react-app/employee-dashboard/App.tsx | 127 +++++++++ .../react-app/employee-dashboard/index.html | 12 + .../ui/react-app/employee-dashboard/main.tsx | 9 + .../react-app/employee-dashboard/styles.css | 119 ++++++++ .../employee-dashboard/vite.config.ts | 10 + .../src/ui/react-app/employee-detail/App.tsx | 146 ++++++++++ .../ui/react-app/employee-detail/index.html | 12 + .../src/ui/react-app/employee-detail/main.tsx | 9 + .../ui/react-app/employee-detail/styles.css | 172 ++++++++++++ .../react-app/employee-detail/vite.config.ts | 10 + .../ui/react-app/employee-directory/App.tsx | 137 +++++++++ .../react-app/employee-directory/index.html | 12 + .../ui/react-app/employee-directory/main.tsx | 9 + .../react-app/employee-directory/styles.css | 150 ++++++++++ .../employee-directory/vite.config.ts | 10 + .../src/ui/react-app/job-board/App.tsx | 26 ++ .../src/ui/react-app/job-board/index.html | 12 + .../src/ui/react-app/job-board/main.tsx | 9 + .../src/ui/react-app/job-board/styles.css | 15 + .../src/ui/react-app/job-board/vite.config.ts | 10 + .../ui/react-app/learning-dashboard/App.tsx | 26 ++ .../react-app/learning-dashboard/index.html | 12 + .../ui/react-app/learning-dashboard/main.tsx | 9 + .../react-app/learning-dashboard/styles.css | 15 + .../learning-dashboard/vite.config.ts | 10 + .../src/ui/react-app/org-chart/App.tsx | 108 ++++++++ .../src/ui/react-app/org-chart/index.html | 12 + .../src/ui/react-app/org-chart/main.tsx | 9 + .../src/ui/react-app/org-chart/styles.css | 120 ++++++++ .../src/ui/react-app/org-chart/vite.config.ts | 10 + .../ui/react-app/payroll-dashboard/App.tsx | 99 +++++++ .../ui/react-app/payroll-dashboard/index.html | 12 + .../ui/react-app/payroll-dashboard/main.tsx | 9 + .../ui/react-app/payroll-dashboard/styles.css | 145 ++++++++++ .../payroll-dashboard/vite.config.ts | 10 + .../src/ui/react-app/payroll-detail/App.tsx | 111 ++++++++ .../ui/react-app/payroll-detail/index.html | 12 + .../src/ui/react-app/payroll-detail/main.tsx | 9 + .../ui/react-app/payroll-detail/styles.css | 146 ++++++++++ .../react-app/payroll-detail/vite.config.ts | 10 + .../src/ui/react-app/team-overview/App.tsx | 26 ++ .../src/ui/react-app/team-overview/index.html | 12 + .../src/ui/react-app/team-overview/main.tsx | 9 + .../src/ui/react-app/team-overview/styles.css | 15 + .../ui/react-app/team-overview/vite.config.ts | 10 + .../ui/react-app/time-off-calendar/App.tsx | 26 ++ .../ui/react-app/time-off-calendar/index.html | 12 + .../ui/react-app/time-off-calendar/main.tsx | 9 + .../ui/react-app/time-off-calendar/styles.css | 15 + .../time-off-calendar/vite.config.ts | 10 + .../src/ui/react-app/time-tracker/App.tsx | 80 ++++++ .../src/ui/react-app/time-tracker/index.html | 12 + .../src/ui/react-app/time-tracker/main.tsx | 9 + .../src/ui/react-app/time-tracker/styles.css | 110 ++++++++ .../ui/react-app/time-tracker/vite.config.ts | 10 + .../ui/react-app/timesheet-approvals/App.tsx | 44 +++ .../react-app/timesheet-approvals/index.html | 12 + .../ui/react-app/timesheet-approvals/main.tsx | 9 + .../react-app/timesheet-approvals/styles.css | 22 ++ .../timesheet-approvals/vite.config.ts | 10 + 103 files changed, 3608 insertions(+) create mode 100644 servers/rippling/src/ui/react-app/app-management/App.tsx create mode 100644 servers/rippling/src/ui/react-app/app-management/index.html create mode 100644 servers/rippling/src/ui/react-app/app-management/main.tsx create mode 100644 servers/rippling/src/ui/react-app/app-management/styles.css create mode 100644 servers/rippling/src/ui/react-app/app-management/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/ats-pipeline/App.tsx create mode 100644 servers/rippling/src/ui/react-app/ats-pipeline/index.html create mode 100644 servers/rippling/src/ui/react-app/ats-pipeline/main.tsx create mode 100644 servers/rippling/src/ui/react-app/ats-pipeline/styles.css create mode 100644 servers/rippling/src/ui/react-app/ats-pipeline/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/batch-create-apps.js create mode 100644 servers/rippling/src/ui/react-app/benefits-enrollment/App.tsx create mode 100644 servers/rippling/src/ui/react-app/benefits-enrollment/index.html create mode 100644 servers/rippling/src/ui/react-app/benefits-enrollment/main.tsx create mode 100644 servers/rippling/src/ui/react-app/benefits-enrollment/styles.css create mode 100644 servers/rippling/src/ui/react-app/benefits-enrollment/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/benefits-overview/App.tsx create mode 100644 servers/rippling/src/ui/react-app/benefits-overview/index.html create mode 100644 servers/rippling/src/ui/react-app/benefits-overview/main.tsx create mode 100644 servers/rippling/src/ui/react-app/benefits-overview/styles.css create mode 100644 servers/rippling/src/ui/react-app/benefits-overview/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/candidate-detail/App.tsx create mode 100644 servers/rippling/src/ui/react-app/candidate-detail/index.html create mode 100644 servers/rippling/src/ui/react-app/candidate-detail/main.tsx create mode 100644 servers/rippling/src/ui/react-app/candidate-detail/styles.css create mode 100644 servers/rippling/src/ui/react-app/candidate-detail/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/course-catalog/App.tsx create mode 100644 servers/rippling/src/ui/react-app/course-catalog/index.html create mode 100644 servers/rippling/src/ui/react-app/course-catalog/main.tsx create mode 100644 servers/rippling/src/ui/react-app/course-catalog/styles.css create mode 100644 servers/rippling/src/ui/react-app/course-catalog/vite.config.ts create mode 100755 servers/rippling/src/ui/react-app/create-apps.sh create mode 100755 servers/rippling/src/ui/react-app/create-remaining.sh create mode 100644 servers/rippling/src/ui/react-app/department-grid/App.tsx create mode 100644 servers/rippling/src/ui/react-app/department-grid/index.html create mode 100644 servers/rippling/src/ui/react-app/department-grid/main.tsx create mode 100644 servers/rippling/src/ui/react-app/department-grid/styles.css create mode 100644 servers/rippling/src/ui/react-app/department-grid/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/device-inventory/App.tsx create mode 100644 servers/rippling/src/ui/react-app/device-inventory/index.html create mode 100644 servers/rippling/src/ui/react-app/device-inventory/main.tsx create mode 100644 servers/rippling/src/ui/react-app/device-inventory/styles.css create mode 100644 servers/rippling/src/ui/react-app/device-inventory/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/employee-dashboard/App.tsx create mode 100644 servers/rippling/src/ui/react-app/employee-dashboard/index.html create mode 100644 servers/rippling/src/ui/react-app/employee-dashboard/main.tsx create mode 100644 servers/rippling/src/ui/react-app/employee-dashboard/styles.css create mode 100644 servers/rippling/src/ui/react-app/employee-dashboard/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/employee-detail/App.tsx create mode 100644 servers/rippling/src/ui/react-app/employee-detail/index.html create mode 100644 servers/rippling/src/ui/react-app/employee-detail/main.tsx create mode 100644 servers/rippling/src/ui/react-app/employee-detail/styles.css create mode 100644 servers/rippling/src/ui/react-app/employee-detail/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/employee-directory/App.tsx create mode 100644 servers/rippling/src/ui/react-app/employee-directory/index.html create mode 100644 servers/rippling/src/ui/react-app/employee-directory/main.tsx create mode 100644 servers/rippling/src/ui/react-app/employee-directory/styles.css create mode 100644 servers/rippling/src/ui/react-app/employee-directory/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/job-board/App.tsx create mode 100644 servers/rippling/src/ui/react-app/job-board/index.html create mode 100644 servers/rippling/src/ui/react-app/job-board/main.tsx create mode 100644 servers/rippling/src/ui/react-app/job-board/styles.css create mode 100644 servers/rippling/src/ui/react-app/job-board/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/learning-dashboard/App.tsx create mode 100644 servers/rippling/src/ui/react-app/learning-dashboard/index.html create mode 100644 servers/rippling/src/ui/react-app/learning-dashboard/main.tsx create mode 100644 servers/rippling/src/ui/react-app/learning-dashboard/styles.css create mode 100644 servers/rippling/src/ui/react-app/learning-dashboard/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/org-chart/App.tsx create mode 100644 servers/rippling/src/ui/react-app/org-chart/index.html create mode 100644 servers/rippling/src/ui/react-app/org-chart/main.tsx create mode 100644 servers/rippling/src/ui/react-app/org-chart/styles.css create mode 100644 servers/rippling/src/ui/react-app/org-chart/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/payroll-dashboard/App.tsx create mode 100644 servers/rippling/src/ui/react-app/payroll-dashboard/index.html create mode 100644 servers/rippling/src/ui/react-app/payroll-dashboard/main.tsx create mode 100644 servers/rippling/src/ui/react-app/payroll-dashboard/styles.css create mode 100644 servers/rippling/src/ui/react-app/payroll-dashboard/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/payroll-detail/App.tsx create mode 100644 servers/rippling/src/ui/react-app/payroll-detail/index.html create mode 100644 servers/rippling/src/ui/react-app/payroll-detail/main.tsx create mode 100644 servers/rippling/src/ui/react-app/payroll-detail/styles.css create mode 100644 servers/rippling/src/ui/react-app/payroll-detail/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/team-overview/App.tsx create mode 100644 servers/rippling/src/ui/react-app/team-overview/index.html create mode 100644 servers/rippling/src/ui/react-app/team-overview/main.tsx create mode 100644 servers/rippling/src/ui/react-app/team-overview/styles.css create mode 100644 servers/rippling/src/ui/react-app/team-overview/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/time-off-calendar/App.tsx create mode 100644 servers/rippling/src/ui/react-app/time-off-calendar/index.html create mode 100644 servers/rippling/src/ui/react-app/time-off-calendar/main.tsx create mode 100644 servers/rippling/src/ui/react-app/time-off-calendar/styles.css create mode 100644 servers/rippling/src/ui/react-app/time-off-calendar/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/time-tracker/App.tsx create mode 100644 servers/rippling/src/ui/react-app/time-tracker/index.html create mode 100644 servers/rippling/src/ui/react-app/time-tracker/main.tsx create mode 100644 servers/rippling/src/ui/react-app/time-tracker/styles.css create mode 100644 servers/rippling/src/ui/react-app/time-tracker/vite.config.ts create mode 100644 servers/rippling/src/ui/react-app/timesheet-approvals/App.tsx create mode 100644 servers/rippling/src/ui/react-app/timesheet-approvals/index.html create mode 100644 servers/rippling/src/ui/react-app/timesheet-approvals/main.tsx create mode 100644 servers/rippling/src/ui/react-app/timesheet-approvals/styles.css create mode 100644 servers/rippling/src/ui/react-app/timesheet-approvals/vite.config.ts diff --git a/servers/rippling/src/ui/react-app/app-management/App.tsx b/servers/rippling/src/ui/react-app/app-management/App.tsx new file mode 100644 index 0000000..044044f --- /dev/null +++ b/servers/rippling/src/ui/react-app/app-management/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UappUmanagement() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

app management

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/app-management/index.html b/servers/rippling/src/ui/react-app/app-management/index.html new file mode 100644 index 0000000..2e95700 --- /dev/null +++ b/servers/rippling/src/ui/react-app/app-management/index.html @@ -0,0 +1,12 @@ + + + + + + app management - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/app-management/main.tsx b/servers/rippling/src/ui/react-app/app-management/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/app-management/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/app-management/styles.css b/servers/rippling/src/ui/react-app/app-management/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/app-management/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/app-management/vite.config.ts b/servers/rippling/src/ui/react-app/app-management/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/app-management/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/ats-pipeline/App.tsx b/servers/rippling/src/ui/react-app/ats-pipeline/App.tsx new file mode 100644 index 0000000..d6013ed --- /dev/null +++ b/servers/rippling/src/ui/react-app/ats-pipeline/App.tsx @@ -0,0 +1,79 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function AtsPipeline() { + const [candidates, setCandidates] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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
Loading pipeline...
; + } + + return ( +
+
+

ATS Pipeline

+

{candidates.length} active candidates

+
+ + {error &&
{error}
} + +
+ {stages.map(stage => ( +
+
+

{stage.replace('_', ' ')}

+ {getCandidatesByStage(stage).length} +
+
+ {getCandidatesByStage(stage).map(candidate => ( +
+

{candidate.name}

+

{candidate.position}

+

{candidate.email}

+
+ ))} +
+
+ ))} +
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/ats-pipeline/index.html b/servers/rippling/src/ui/react-app/ats-pipeline/index.html new file mode 100644 index 0000000..09eb768 --- /dev/null +++ b/servers/rippling/src/ui/react-app/ats-pipeline/index.html @@ -0,0 +1,12 @@ + + + + + + ats pipeline - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/ats-pipeline/main.tsx b/servers/rippling/src/ui/react-app/ats-pipeline/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/ats-pipeline/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/ats-pipeline/styles.css b/servers/rippling/src/ui/react-app/ats-pipeline/styles.css new file mode 100644 index 0000000..5fd94a3 --- /dev/null +++ b/servers/rippling/src/ui/react-app/ats-pipeline/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/ats-pipeline/vite.config.ts b/servers/rippling/src/ui/react-app/ats-pipeline/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/ats-pipeline/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/batch-create-apps.js b/servers/rippling/src/ui/react-app/batch-create-apps.js new file mode 100644 index 0000000..2c8ff52 --- /dev/null +++ b/servers/rippling/src/ui/react-app/batch-create-apps.js @@ -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'), ` + + + + + ${app.title} - Rippling MCP + + +
+ + + +`); + + // 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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
Loading ${app.title.toLowerCase()}...
+
+ ); + } + + return ( +
+
+

${app.title}

+

Manage and view ${app.title.toLowerCase()}

+
+ + {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} +`); + + // 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'); diff --git a/servers/rippling/src/ui/react-app/benefits-enrollment/App.tsx b/servers/rippling/src/ui/react-app/benefits-enrollment/App.tsx new file mode 100644 index 0000000..64b90d9 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-enrollment/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UbenefitsUenrollment() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

uenefits enrollment

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/benefits-enrollment/index.html b/servers/rippling/src/ui/react-app/benefits-enrollment/index.html new file mode 100644 index 0000000..2616a63 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-enrollment/index.html @@ -0,0 +1,12 @@ + + + + + + uenefits enrollment - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/benefits-enrollment/main.tsx b/servers/rippling/src/ui/react-app/benefits-enrollment/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-enrollment/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/benefits-enrollment/styles.css b/servers/rippling/src/ui/react-app/benefits-enrollment/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-enrollment/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/benefits-enrollment/vite.config.ts b/servers/rippling/src/ui/react-app/benefits-enrollment/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-enrollment/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/benefits-overview/App.tsx b/servers/rippling/src/ui/react-app/benefits-overview/App.tsx new file mode 100644 index 0000000..cd80a21 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-overview/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UbenefitsUoverview() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

uenefits overview

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/benefits-overview/index.html b/servers/rippling/src/ui/react-app/benefits-overview/index.html new file mode 100644 index 0000000..e0ebc86 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-overview/index.html @@ -0,0 +1,12 @@ + + + + + + uenefits overview - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/benefits-overview/main.tsx b/servers/rippling/src/ui/react-app/benefits-overview/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-overview/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/benefits-overview/styles.css b/servers/rippling/src/ui/react-app/benefits-overview/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-overview/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/benefits-overview/vite.config.ts b/servers/rippling/src/ui/react-app/benefits-overview/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/benefits-overview/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/candidate-detail/App.tsx b/servers/rippling/src/ui/react-app/candidate-detail/App.tsx new file mode 100644 index 0000000..4b3de7a --- /dev/null +++ b/servers/rippling/src/ui/react-app/candidate-detail/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UcandidateUdetail() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

candidate detail

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/candidate-detail/index.html b/servers/rippling/src/ui/react-app/candidate-detail/index.html new file mode 100644 index 0000000..75d2a3a --- /dev/null +++ b/servers/rippling/src/ui/react-app/candidate-detail/index.html @@ -0,0 +1,12 @@ + + + + + + candidate detail - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/candidate-detail/main.tsx b/servers/rippling/src/ui/react-app/candidate-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/candidate-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/candidate-detail/styles.css b/servers/rippling/src/ui/react-app/candidate-detail/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/candidate-detail/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/candidate-detail/vite.config.ts b/servers/rippling/src/ui/react-app/candidate-detail/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/candidate-detail/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/course-catalog/App.tsx b/servers/rippling/src/ui/react-app/course-catalog/App.tsx new file mode 100644 index 0000000..c7a4547 --- /dev/null +++ b/servers/rippling/src/ui/react-app/course-catalog/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UcourseUcatalog() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

course catalog

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/course-catalog/index.html b/servers/rippling/src/ui/react-app/course-catalog/index.html new file mode 100644 index 0000000..9580dc2 --- /dev/null +++ b/servers/rippling/src/ui/react-app/course-catalog/index.html @@ -0,0 +1,12 @@ + + + + + + course catalog - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/course-catalog/main.tsx b/servers/rippling/src/ui/react-app/course-catalog/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/course-catalog/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/course-catalog/styles.css b/servers/rippling/src/ui/react-app/course-catalog/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/course-catalog/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/course-catalog/vite.config.ts b/servers/rippling/src/ui/react-app/course-catalog/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/course-catalog/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/create-apps.sh b/servers/rippling/src/ui/react-app/create-apps.sh new file mode 100755 index 0000000..eed6c69 --- /dev/null +++ b/servers/rippling/src/ui/react-app/create-apps.sh @@ -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( + + + +); +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" diff --git a/servers/rippling/src/ui/react-app/create-remaining.sh b/servers/rippling/src/ui/react-app/create-remaining.sh new file mode 100755 index 0000000..d5386d0 --- /dev/null +++ b/servers/rippling/src/ui/react-app/create-remaining.sh @@ -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([]); + 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
Loading...
; + + return ( +
+
+

Timesheet Approvals

+

{timesheets.length} pending approvals

+
+
+

Pending Timesheets

+
+ {timesheets.map(ts => ( +
+
+

{ts.employee}

+

{ts.week} • {ts.hours} hours

+
+
+ + +
+
+ ))} +
+
+
+ ); +} +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" diff --git a/servers/rippling/src/ui/react-app/department-grid/App.tsx b/servers/rippling/src/ui/react-app/department-grid/App.tsx new file mode 100644 index 0000000..3d51122 --- /dev/null +++ b/servers/rippling/src/ui/react-app/department-grid/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UdepartmentUgrid() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

department grid

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/department-grid/index.html b/servers/rippling/src/ui/react-app/department-grid/index.html new file mode 100644 index 0000000..a824e5c --- /dev/null +++ b/servers/rippling/src/ui/react-app/department-grid/index.html @@ -0,0 +1,12 @@ + + + + + + department grid - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/department-grid/main.tsx b/servers/rippling/src/ui/react-app/department-grid/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/department-grid/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/department-grid/styles.css b/servers/rippling/src/ui/react-app/department-grid/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/department-grid/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/department-grid/vite.config.ts b/servers/rippling/src/ui/react-app/department-grid/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/department-grid/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/device-inventory/App.tsx b/servers/rippling/src/ui/react-app/device-inventory/App.tsx new file mode 100644 index 0000000..cd66728 --- /dev/null +++ b/servers/rippling/src/ui/react-app/device-inventory/App.tsx @@ -0,0 +1,80 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function DeviceInventory() { + const [devices, setDevices] = useState([]); + const [filteredDevices, setFilteredDevices] = useState([]); + 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
Loading devices...
; + + return ( +
+
+

Device Inventory

+

{filteredDevices.length} devices

+
+ +
+ +
+ +
+ {filteredDevices.map(device => ( +
+
{device.type[0]}
+
+

{device.name}

+

{device.type}

+

SN: {device.serialNumber}

+

{device.assignee || 'Unassigned'}

+
+ + {device.status} + +
+ ))} +
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/device-inventory/index.html b/servers/rippling/src/ui/react-app/device-inventory/index.html new file mode 100644 index 0000000..48daf02 --- /dev/null +++ b/servers/rippling/src/ui/react-app/device-inventory/index.html @@ -0,0 +1,12 @@ + + + + + + device inventory - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/device-inventory/main.tsx b/servers/rippling/src/ui/react-app/device-inventory/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/device-inventory/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/device-inventory/styles.css b/servers/rippling/src/ui/react-app/device-inventory/styles.css new file mode 100644 index 0000000..23f529e --- /dev/null +++ b/servers/rippling/src/ui/react-app/device-inventory/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/device-inventory/vite.config.ts b/servers/rippling/src/ui/react-app/device-inventory/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/device-inventory/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/employee-dashboard/App.tsx b/servers/rippling/src/ui/react-app/employee-dashboard/App.tsx new file mode 100644 index 0000000..d995d47 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-dashboard/App.tsx @@ -0,0 +1,127 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function EmployeeDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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(); + 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 ( +
+
Loading employee dashboard...
+
+ ); + } + + return ( +
+
+

Employee Dashboard

+

Company headcount and overview

+
+ + {error &&
{error}
} + +
+
+
{stats.total}
+
Total Employees
+
+
+
{stats.active}
+
Active
+
+
+
{stats.newHires}
+
New Hires (30d)
+
+
+
{stats.total - stats.active}
+
Inactive
+
+
+ +
+

Employees by Department

+
+ {stats.departments.map((dept, idx) => ( +
+ {dept.name} + {dept.count} +
+ ))} +
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/employee-dashboard/index.html b/servers/rippling/src/ui/react-app/employee-dashboard/index.html new file mode 100644 index 0000000..7368e36 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Employee Dashboard - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/employee-dashboard/main.tsx b/servers/rippling/src/ui/react-app/employee-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/employee-dashboard/styles.css b/servers/rippling/src/ui/react-app/employee-dashboard/styles.css new file mode 100644 index 0000000..fae8b9d --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-dashboard/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/employee-dashboard/vite.config.ts b/servers/rippling/src/ui/react-app/employee-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/employee-detail/App.tsx b/servers/rippling/src/ui/react-app/employee-detail/App.tsx new file mode 100644 index 0000000..230f627 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-detail/App.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function EmployeeDetail() { + const [employeeId, setEmployeeId] = useState(''); + const [employee, setEmployee] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 ( +
+
+

Employee Detail

+

View complete employee profile

+
+ +
+ setEmployeeId(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && loadEmployee()} + /> + +
+ + {error &&
{error}
} + + {employee && ( +
+
+
{employee.firstName[0]}{employee.lastName[0]}
+
+

{employee.firstName} {employee.lastName}

+

{employee.title}

+ + {employee.status} + +
+
+ +
+
+

Contact Information

+
+ Email: + {employee.email} +
+
+ Phone: + {employee.phoneNumber || 'N/A'} +
+
+ +
+

Employment Details

+
+ Department: + {employee.department} +
+
+ Manager: + {employee.manager || 'N/A'} +
+
+ Start Date: + {employee.startDate} +
+
+ Type: + {employee.employmentType?.replace('_', ' ')} +
+
+ +
+

Compensation

+
+ Salary: + + {employee.compensation?.currency || 'USD'} {employee.compensation?.amount?.toLocaleString() || 'N/A'} + +
+
+ Frequency: + {employee.compensation?.frequency || 'N/A'} +
+
+ +
+

Work Location

+
+ Location: + {employee.workLocation || 'Remote'} +
+
+
+
+ )} +
+ ); +} diff --git a/servers/rippling/src/ui/react-app/employee-detail/index.html b/servers/rippling/src/ui/react-app/employee-detail/index.html new file mode 100644 index 0000000..f0051cc --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Employee Detail - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/employee-detail/main.tsx b/servers/rippling/src/ui/react-app/employee-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/employee-detail/styles.css b/servers/rippling/src/ui/react-app/employee-detail/styles.css new file mode 100644 index 0000000..2fe535e --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-detail/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/employee-detail/vite.config.ts b/servers/rippling/src/ui/react-app/employee-detail/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-detail/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/employee-directory/App.tsx b/servers/rippling/src/ui/react-app/employee-directory/App.tsx new file mode 100644 index 0000000..ebb3b07 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-directory/App.tsx @@ -0,0 +1,137 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function EmployeeDirectory() { + const [employees, setEmployees] = useState([]); + const [filteredEmployees, setFilteredEmployees] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [departmentFilter, setDepartmentFilter] = useState('all'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
Loading employee directory...
+
+ ); + } + + return ( +
+
+

Employee Directory

+

{filteredEmployees.length} employees

+
+ + {error &&
{error}
} + +
+ setSearchTerm(e.target.value)} + className="search-input" + /> + +
+ +
+ {filteredEmployees.map(emp => ( +
+
+ {emp.firstName?.[0]}{emp.lastName?.[0]} +
+
+

{emp.firstName} {emp.lastName}

+

{emp.title || 'N/A'}

+

{emp.department || 'Unassigned'}

+

{emp.email}

+
+ + {emp.status} + +
+ ))} +
+ + {filteredEmployees.length === 0 && ( +
No employees found matching your criteria
+ )} +
+ ); +} diff --git a/servers/rippling/src/ui/react-app/employee-directory/index.html b/servers/rippling/src/ui/react-app/employee-directory/index.html new file mode 100644 index 0000000..7c9401b --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-directory/index.html @@ -0,0 +1,12 @@ + + + + + + Employee Directory - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/employee-directory/main.tsx b/servers/rippling/src/ui/react-app/employee-directory/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-directory/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/employee-directory/styles.css b/servers/rippling/src/ui/react-app/employee-directory/styles.css new file mode 100644 index 0000000..6467689 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-directory/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/employee-directory/vite.config.ts b/servers/rippling/src/ui/react-app/employee-directory/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/employee-directory/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/job-board/App.tsx b/servers/rippling/src/ui/react-app/job-board/App.tsx new file mode 100644 index 0000000..de6a32d --- /dev/null +++ b/servers/rippling/src/ui/react-app/job-board/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UjobUboard() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

jou uoard

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/job-board/index.html b/servers/rippling/src/ui/react-app/job-board/index.html new file mode 100644 index 0000000..bd899ec --- /dev/null +++ b/servers/rippling/src/ui/react-app/job-board/index.html @@ -0,0 +1,12 @@ + + + + + + jou uoard - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/job-board/main.tsx b/servers/rippling/src/ui/react-app/job-board/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/job-board/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/job-board/styles.css b/servers/rippling/src/ui/react-app/job-board/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/job-board/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/job-board/vite.config.ts b/servers/rippling/src/ui/react-app/job-board/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/job-board/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/learning-dashboard/App.tsx b/servers/rippling/src/ui/react-app/learning-dashboard/App.tsx new file mode 100644 index 0000000..299d0ed --- /dev/null +++ b/servers/rippling/src/ui/react-app/learning-dashboard/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UlearningUdashboard() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

learning dashuoard

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/learning-dashboard/index.html b/servers/rippling/src/ui/react-app/learning-dashboard/index.html new file mode 100644 index 0000000..a6a47b0 --- /dev/null +++ b/servers/rippling/src/ui/react-app/learning-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + learning dashuoard - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/learning-dashboard/main.tsx b/servers/rippling/src/ui/react-app/learning-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/learning-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/learning-dashboard/styles.css b/servers/rippling/src/ui/react-app/learning-dashboard/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/learning-dashboard/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/learning-dashboard/vite.config.ts b/servers/rippling/src/ui/react-app/learning-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/learning-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/org-chart/App.tsx b/servers/rippling/src/ui/react-app/org-chart/App.tsx new file mode 100644 index 0000000..2cca6fe --- /dev/null +++ b/servers/rippling/src/ui/react-app/org-chart/App.tsx @@ -0,0 +1,108 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function OrgChart() { + const [employees, setEmployees] = useState([]); + const [orgTree, setOrgTree] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+
{node.firstName?.[0]}{node.lastName?.[0]}
+
+

{node.firstName} {node.lastName}

+

{node.title}

+

{node.department}

+
+ {node.reports?.length > 0 && ( + {node.reports.length} reports + )} +
+ {node.reports?.length > 0 && ( +
+ {node.reports.map((report: any) => renderNode(report, level + 1))} +
+ )} +
+ ); + }; + + if (loading) { + return ( +
+
Loading org chart...
+
+ ); + } + + return ( +
+
+

Organization Chart

+

{employees.length} employees in org structure

+
+ + {error &&
{error}
} + +
+ {orgTree ? renderNode(orgTree) :
No org data available
} +
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/org-chart/index.html b/servers/rippling/src/ui/react-app/org-chart/index.html new file mode 100644 index 0000000..a6b3720 --- /dev/null +++ b/servers/rippling/src/ui/react-app/org-chart/index.html @@ -0,0 +1,12 @@ + + + + + + Org Chart - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/org-chart/main.tsx b/servers/rippling/src/ui/react-app/org-chart/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/org-chart/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/org-chart/styles.css b/servers/rippling/src/ui/react-app/org-chart/styles.css new file mode 100644 index 0000000..5cc2bff --- /dev/null +++ b/servers/rippling/src/ui/react-app/org-chart/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/org-chart/vite.config.ts b/servers/rippling/src/ui/react-app/org-chart/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/org-chart/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/payroll-dashboard/App.tsx b/servers/rippling/src/ui/react-app/payroll-dashboard/App.tsx new file mode 100644 index 0000000..9ce9fdd --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-dashboard/App.tsx @@ -0,0 +1,99 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function PayrollDashboard() { + const [payRuns, setPayRuns] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
Loading payroll dashboard...
+
+ ); + } + + return ( +
+
+

Payroll Dashboard

+

{payRuns.length} pay runs

+
+ + {error &&
{error}
} + +
+
+
Total Pay Runs
+
{payRuns.length}
+
+
+
Last Period Amount
+
${payRuns[0]?.totalAmount?.toLocaleString() || '0'}
+
+
+
Employees Paid
+
{payRuns[0]?.employeeCount || 0}
+
+
+ +
+

Recent Pay Runs

+
+ {payRuns.map(run => ( +
+
+

{run.period}

+

{run.type} • {run.employeeCount} employees

+
+
${run.totalAmount.toLocaleString()}
+ + {run.status} + +
+ ))} +
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/payroll-dashboard/index.html b/servers/rippling/src/ui/react-app/payroll-dashboard/index.html new file mode 100644 index 0000000..641c69a --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Payroll Dashboard - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/payroll-dashboard/main.tsx b/servers/rippling/src/ui/react-app/payroll-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/payroll-dashboard/styles.css b/servers/rippling/src/ui/react-app/payroll-dashboard/styles.css new file mode 100644 index 0000000..26ce899 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-dashboard/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/payroll-dashboard/vite.config.ts b/servers/rippling/src/ui/react-app/payroll-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/payroll-detail/App.tsx b/servers/rippling/src/ui/react-app/payroll-detail/App.tsx new file mode 100644 index 0000000..764059d --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-detail/App.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import './styles.css'; + +export default function PayrollDetail() { + const [payRunId, setPayRunId] = useState(''); + const [payRun, setPayRun] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 ( +
+
+

Payroll Detail

+

View pay run breakdown

+
+ +
+ setPayRunId(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && loadPayRun()} + /> + +
+ + {error &&
{error}
} + + {payRun && ( + <> +
+
+
Total Amount
+
${payRun.totalAmount.toLocaleString()}
+
+
+
Employees
+
{payRun.employeeCount}
+
+
+
Status
+
{payRun.status}
+
+
+ +
+

Payment Details

+ + + + + + + + + + + {payRun.payments?.map((payment: any, idx: number) => ( + + + + + + + ))} + +
EmployeeGross PayDeductionsNet Pay
{payment.employeeName}${payment.grossPay.toLocaleString()}${payment.deductions.toLocaleString()}${payment.netPay.toLocaleString()}
+
+ + )} +
+ ); +} diff --git a/servers/rippling/src/ui/react-app/payroll-detail/index.html b/servers/rippling/src/ui/react-app/payroll-detail/index.html new file mode 100644 index 0000000..7ef1148 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-detail/index.html @@ -0,0 +1,12 @@ + + + + + + payroll detail - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/payroll-detail/main.tsx b/servers/rippling/src/ui/react-app/payroll-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/payroll-detail/styles.css b/servers/rippling/src/ui/react-app/payroll-detail/styles.css new file mode 100644 index 0000000..42e9d45 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-detail/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/payroll-detail/vite.config.ts b/servers/rippling/src/ui/react-app/payroll-detail/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/payroll-detail/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/team-overview/App.tsx b/servers/rippling/src/ui/react-app/team-overview/App.tsx new file mode 100644 index 0000000..3830a39 --- /dev/null +++ b/servers/rippling/src/ui/react-app/team-overview/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UteamUoverview() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

team overview

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/team-overview/index.html b/servers/rippling/src/ui/react-app/team-overview/index.html new file mode 100644 index 0000000..70d40e1 --- /dev/null +++ b/servers/rippling/src/ui/react-app/team-overview/index.html @@ -0,0 +1,12 @@ + + + + + + team overview - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/team-overview/main.tsx b/servers/rippling/src/ui/react-app/team-overview/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/team-overview/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/team-overview/styles.css b/servers/rippling/src/ui/react-app/team-overview/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/team-overview/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/team-overview/vite.config.ts b/servers/rippling/src/ui/react-app/team-overview/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/team-overview/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/time-off-calendar/App.tsx b/servers/rippling/src/ui/react-app/time-off-calendar/App.tsx new file mode 100644 index 0000000..ffe3805 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-off-calendar/App.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function UtimeUoffUcalendar() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setData([{ id: '1', name: 'Sample Item' }]); + setLoading(false); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

time off calendar

+

{data.length} items

+
+
+
{JSON.stringify(data, null, 2)}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/time-off-calendar/index.html b/servers/rippling/src/ui/react-app/time-off-calendar/index.html new file mode 100644 index 0000000..7b9b527 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-off-calendar/index.html @@ -0,0 +1,12 @@ + + + + + + time off calendar - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/time-off-calendar/main.tsx b/servers/rippling/src/ui/react-app/time-off-calendar/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-off-calendar/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/time-off-calendar/styles.css b/servers/rippling/src/ui/react-app/time-off-calendar/styles.css new file mode 100644 index 0000000..8901290 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-off-calendar/styles.css @@ -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; } diff --git a/servers/rippling/src/ui/react-app/time-off-calendar/vite.config.ts b/servers/rippling/src/ui/react-app/time-off-calendar/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-off-calendar/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/time-tracker/App.tsx b/servers/rippling/src/ui/react-app/time-tracker/App.tsx new file mode 100644 index 0000000..dd4dc2a --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-tracker/App.tsx @@ -0,0 +1,80 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function TimeTracker() { + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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
Loading time entries...
; + } + + return ( +
+
+

Time Tracker

+

{entries.length} entries • {totalHours.toFixed(1)} total hours

+
+ + {error &&
{error}
} + +
+

Recent Time Entries

+ + + + + + + + + + + + {entries.map(entry => ( + + + + + + + + ))} + +
EmployeeDateHoursProjectStatus
{entry.employee}{entry.date}{entry.hours}{entry.project}{entry.status}
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/time-tracker/index.html b/servers/rippling/src/ui/react-app/time-tracker/index.html new file mode 100644 index 0000000..3cdf43c --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-tracker/index.html @@ -0,0 +1,12 @@ + + + + + + time tracker - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/time-tracker/main.tsx b/servers/rippling/src/ui/react-app/time-tracker/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-tracker/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/time-tracker/styles.css b/servers/rippling/src/ui/react-app/time-tracker/styles.css new file mode 100644 index 0000000..6e52c76 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-tracker/styles.css @@ -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; +} diff --git a/servers/rippling/src/ui/react-app/time-tracker/vite.config.ts b/servers/rippling/src/ui/react-app/time-tracker/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/time-tracker/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/rippling/src/ui/react-app/timesheet-approvals/App.tsx b/servers/rippling/src/ui/react-app/timesheet-approvals/App.tsx new file mode 100644 index 0000000..b760115 --- /dev/null +++ b/servers/rippling/src/ui/react-app/timesheet-approvals/App.tsx @@ -0,0 +1,44 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function TimesheetApprovals() { + const [timesheets, setTimesheets] = useState([]); + 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
Loading...
; + + return ( +
+
+

Timesheet Approvals

+

{timesheets.length} pending approvals

+
+
+

Pending Timesheets

+
+ {timesheets.map(ts => ( +
+
+

{ts.employee}

+

{ts.week} • {ts.hours} hours

+
+
+ + +
+
+ ))} +
+
+
+ ); +} diff --git a/servers/rippling/src/ui/react-app/timesheet-approvals/index.html b/servers/rippling/src/ui/react-app/timesheet-approvals/index.html new file mode 100644 index 0000000..cdb27e6 --- /dev/null +++ b/servers/rippling/src/ui/react-app/timesheet-approvals/index.html @@ -0,0 +1,12 @@ + + + + + + timesheet approvals - Rippling MCP + + +
+ + + diff --git a/servers/rippling/src/ui/react-app/timesheet-approvals/main.tsx b/servers/rippling/src/ui/react-app/timesheet-approvals/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/rippling/src/ui/react-app/timesheet-approvals/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/rippling/src/ui/react-app/timesheet-approvals/styles.css b/servers/rippling/src/ui/react-app/timesheet-approvals/styles.css new file mode 100644 index 0000000..a0a2d3b --- /dev/null +++ b/servers/rippling/src/ui/react-app/timesheet-approvals/styles.css @@ -0,0 +1,22 @@ +* { 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; } diff --git a/servers/rippling/src/ui/react-app/timesheet-approvals/vite.config.ts b/servers/rippling/src/ui/react-app/timesheet-approvals/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/rippling/src/ui/react-app/timesheet-approvals/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +});