rippling: Complete MCP server with 50+ tools, 16 React apps, full API client
- API Client (src/clients/rippling.ts): OAuth2/API key auth, pagination, error handling - 50+ tools across 10 categories: * employees-tools.ts: 7 tools (list, get, create, update, terminate, custom fields, org chart) * companies-tools.ts: 5 tools (company, departments, locations, teams) * payroll-tools.ts: 4 tools (pay runs, pay statements) * time-tools.ts: 11 tools (time entries, timesheets, PTO requests) * benefits-tools.ts: 4 tools (plans, enrollments) * ats-tools.ts: 6 tools (candidates, jobs, applications, pipeline) * learning-tools.ts: 4 tools (courses, assignments) * devices-tools.ts: 4 tools (devices, apps/licenses) * groups-tools.ts: 6 tools (CRUD + members) * custom-objects-tools.ts: 5 tools (CRUD + query) - 16 React UI apps: * employee-dashboard, employee-detail, employee-directory, org-chart * payroll-dashboard, payroll-detail * time-tracker, timesheet-approvals, time-off-calendar * benefits-overview, ats-pipeline, job-board * learning-dashboard, device-inventory, team-overview, department-grid - Full TypeScript types for all API entities - Comprehensive README with usage examples - Production-ready with proper error handling and pagination
This commit is contained in:
parent
91a76580eb
commit
36a4d6fb4f
@ -1,119 +1,295 @@
|
||||
# Rippling MCP Server
|
||||
|
||||
MCP server for [Rippling](https://www.rippling.com/) API integration. Access employees, departments, teams, payroll, devices, and apps for HR and IT management.
|
||||
Complete Model Context Protocol (MCP) server for the Rippling HR Platform API.
|
||||
|
||||
## Setup
|
||||
## Overview
|
||||
|
||||
This MCP server provides comprehensive integration with Rippling's HR platform, enabling AI assistants to interact with employee data, payroll, time tracking, benefits, recruiting (ATS), learning management, device inventory, and more.
|
||||
|
||||
## Features
|
||||
|
||||
### 🔧 **50+ Tools Across 10 Categories**
|
||||
|
||||
#### Employees (7 tools)
|
||||
- `rippling_list_employees` - List all employees with filters
|
||||
- `rippling_get_employee` - Get detailed employee information
|
||||
- `rippling_create_employee` - Create new employee records
|
||||
- `rippling_update_employee` - Update employee information
|
||||
- `rippling_terminate_employee` - Terminate an employee
|
||||
- `rippling_list_employee_custom_fields` - List custom fields
|
||||
- `rippling_get_org_chart` - Get organizational chart
|
||||
|
||||
#### Companies (5 tools)
|
||||
- `rippling_get_company` - Get company information
|
||||
- `rippling_list_departments` - List all departments
|
||||
- `rippling_create_department` - Create new department
|
||||
- `rippling_list_locations` - List work locations
|
||||
- `rippling_list_teams` - List all teams
|
||||
|
||||
#### Payroll (4 tools)
|
||||
- `rippling_list_pay_runs` - List payroll runs
|
||||
- `rippling_get_pay_run` - Get pay run details
|
||||
- `rippling_list_pay_statements` - List pay statements
|
||||
- `rippling_get_pay_statement` - Get detailed pay statement
|
||||
|
||||
#### Time Tracking (11 tools)
|
||||
- `rippling_list_time_entries` - List time clock entries
|
||||
- `rippling_create_time_entry` - Create time entry
|
||||
- `rippling_update_time_entry` - Update time entry
|
||||
- `rippling_delete_time_entry` - Delete time entry
|
||||
- `rippling_get_timesheet` - Get timesheet
|
||||
- `rippling_approve_timesheet` - Approve timesheet
|
||||
- `rippling_list_time_off_requests` - List PTO requests
|
||||
- `rippling_create_time_off_request` - Create PTO request
|
||||
- `rippling_approve_time_off_request` - Approve PTO
|
||||
- `rippling_deny_time_off_request` - Deny PTO
|
||||
|
||||
#### Benefits (4 tools)
|
||||
- `rippling_list_benefits_plans` - List benefits plans
|
||||
- `rippling_get_benefits_plan` - Get plan details
|
||||
- `rippling_list_benefits_enrollments` - List enrollments
|
||||
- `rippling_get_benefits_enrollment` - Get enrollment details
|
||||
|
||||
#### ATS/Recruiting (6 tools)
|
||||
- `rippling_list_candidates` - List candidates
|
||||
- `rippling_get_candidate` - Get candidate details
|
||||
- `rippling_list_jobs` - List job postings
|
||||
- `rippling_get_job` - Get job details
|
||||
- `rippling_list_applications` - List applications
|
||||
- `rippling_update_application_stage` - Move candidate through pipeline
|
||||
|
||||
#### Learning (4 tools)
|
||||
- `rippling_list_courses` - List training courses
|
||||
- `rippling_get_course` - Get course details
|
||||
- `rippling_list_course_assignments` - List assignments
|
||||
- `rippling_assign_course` - Assign course to employee
|
||||
|
||||
#### Devices (4 tools)
|
||||
- `rippling_list_devices` - List hardware devices
|
||||
- `rippling_get_device` - Get device details
|
||||
- `rippling_list_apps` - List software/app licenses
|
||||
- `rippling_get_app` - Get app license details
|
||||
|
||||
#### Groups (6 tools)
|
||||
- `rippling_list_groups` - List groups
|
||||
- `rippling_get_group` - Get group details
|
||||
- `rippling_create_group` - Create new group
|
||||
- `rippling_update_group` - Update group
|
||||
- `rippling_add_group_member` - Add member to group
|
||||
- `rippling_remove_group_member` - Remove member from group
|
||||
|
||||
#### Custom Objects (5 tools)
|
||||
- `rippling_list_custom_objects` - List custom objects
|
||||
- `rippling_get_custom_object` - Get custom object
|
||||
- `rippling_create_custom_object` - Create custom object
|
||||
- `rippling_update_custom_object` - Update custom object
|
||||
- `rippling_query_custom_objects` - Query custom objects with filters
|
||||
|
||||
### 🎨 **16 React UI Components**
|
||||
|
||||
1. **EmployeeDashboard** - Overview of employee metrics and departments
|
||||
2. **EmployeeDetail** - Detailed employee profile view
|
||||
3. **EmployeeDirectory** - Searchable employee directory with filters
|
||||
4. **OrgChart** - Visual organizational chart
|
||||
5. **PayrollDashboard** - Payroll overview and recent runs
|
||||
6. **PayrollDetail** - Detailed pay statement breakdown
|
||||
7. **TimeTracker** - Clock in/out interface and time entry viewer
|
||||
8. **TimesheetApprovals** - Approve/reject employee timesheets
|
||||
9. **TimeOffCalendar** - PTO calendar and requests
|
||||
10. **BenefitsOverview** - Benefits plans and enrollments
|
||||
11. **ATSPipeline** - Recruiting pipeline visualization
|
||||
12. **JobBoard** - Job postings board
|
||||
13. **LearningDashboard** - Training and course completion dashboard
|
||||
14. **DeviceInventory** - Hardware and device management
|
||||
15. **TeamOverview** - Team composition and management
|
||||
16. **DepartmentGrid** - Department overview and headcount
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
## Configuration
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `RIPPLING_API_KEY` | Yes | Bearer API key or OAuth access token |
|
||||
Set the following environment variables:
|
||||
|
||||
## API Endpoint
|
||||
```bash
|
||||
# Option 1: API Key authentication
|
||||
export RIPPLING_API_KEY="your_api_key_here"
|
||||
|
||||
- **Base URL:** `https://api.rippling.com/platform/api`
|
||||
# Option 2: OAuth2 Bearer token
|
||||
export RIPPLING_ACCESS_TOKEN="your_access_token_here"
|
||||
|
||||
## Tools
|
||||
# Optional: Custom API base URL
|
||||
export RIPPLING_BASE_URL="https://api.rippling.com"
|
||||
```
|
||||
|
||||
### HR / People
|
||||
- **list_employees** - List employees with pagination and terminated filter
|
||||
- **get_employee** - Get detailed employee information
|
||||
- **list_departments** - List all departments
|
||||
- **list_teams** - List all teams
|
||||
- **list_levels** - List job levels (IC1, Manager, etc.)
|
||||
- **list_work_locations** - List office locations
|
||||
- **get_leave_requests** - Get time-off/leave requests
|
||||
## Usage
|
||||
|
||||
### Payroll
|
||||
- **get_payroll** - Get payroll runs and compensation data
|
||||
### As MCP Server
|
||||
|
||||
### IT
|
||||
- **list_devices** - List managed devices (laptops, phones)
|
||||
- **list_apps** - List integrated applications
|
||||
|
||||
### Company
|
||||
- **get_company** - Get company information
|
||||
- **list_groups** - List custom groups for access control
|
||||
|
||||
## Usage with Claude Desktop
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
Add to your MCP settings configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rippling": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/mcp-servers/rippling/dist/index.js"],
|
||||
"args": ["/path/to/rippling/dist/index.js"],
|
||||
"env": {
|
||||
"RIPPLING_API_KEY": "your-api-key"
|
||||
"RIPPLING_API_KEY": "your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
### Development
|
||||
|
||||
Rippling supports two authentication methods:
|
||||
```bash
|
||||
# Build TypeScript
|
||||
npm run build
|
||||
|
||||
### Bearer API Key
|
||||
Generate an API key in your Rippling admin settings for server-to-server integrations.
|
||||
|
||||
### OAuth 2.0
|
||||
For partner integrations, use OAuth flow:
|
||||
1. Register as a Rippling partner
|
||||
2. Implement OAuth installation flow
|
||||
3. Exchange authorization code for access token
|
||||
|
||||
See [Rippling Developer Docs](https://developer.rippling.com/documentation) for details.
|
||||
|
||||
## Required Scopes
|
||||
|
||||
Depending on which tools you use, request appropriate scopes:
|
||||
- `employee:read` - List/get employees
|
||||
- `department:read` - List departments
|
||||
- `team:read` - List teams
|
||||
- `payroll:read` - Access payroll data
|
||||
- `device:read` - List devices
|
||||
- `app:read` - List apps
|
||||
- `company:read` - Company info
|
||||
- `leave:read` - Leave requests
|
||||
|
||||
## Examples
|
||||
|
||||
List active employees:
|
||||
```
|
||||
list_employees(limit: 50)
|
||||
# Run in development mode
|
||||
npm run dev
|
||||
```
|
||||
|
||||
List all employees including terminated:
|
||||
```
|
||||
list_employees(include_terminated: true, limit: 100)
|
||||
## API Client
|
||||
|
||||
The `RipplingClient` class provides:
|
||||
|
||||
- ✅ Automatic authentication (API key or OAuth2)
|
||||
- ✅ Cursor-based pagination support
|
||||
- ✅ Comprehensive error handling
|
||||
- ✅ Type-safe requests and responses
|
||||
- ✅ Automatic retry logic
|
||||
- ✅ Request/response interceptors
|
||||
|
||||
### Example Usage
|
||||
|
||||
```typescript
|
||||
import { RipplingClient } from './clients/rippling.js';
|
||||
|
||||
const client = new RipplingClient({
|
||||
apiKey: process.env.RIPPLING_API_KEY,
|
||||
});
|
||||
|
||||
// List employees with pagination
|
||||
const employees = await client.listEmployees({
|
||||
status: 'ACTIVE',
|
||||
departmentId: 'dept_123',
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
// Get all pages automatically
|
||||
const allEmployees = await client.getAllPaginated('/v1/employees');
|
||||
|
||||
// Create a new employee
|
||||
const newEmployee = await client.createEmployee({
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@company.com',
|
||||
title: 'Software Engineer',
|
||||
});
|
||||
```
|
||||
|
||||
Get employee details:
|
||||
## Architecture
|
||||
|
||||
```
|
||||
get_employee(employee_id: "emp_abc123")
|
||||
rippling/
|
||||
├── src/
|
||||
│ ├── clients/
|
||||
│ │ └── rippling.ts # API client with auth & pagination
|
||||
│ ├── tools/
|
||||
│ │ ├── employees-tools.ts # Employee management (7 tools)
|
||||
│ │ ├── companies-tools.ts # Company/dept/location (5 tools)
|
||||
│ │ ├── payroll-tools.ts # Payroll operations (4 tools)
|
||||
│ │ ├── time-tools.ts # Time tracking & PTO (11 tools)
|
||||
│ │ ├── benefits-tools.ts # Benefits management (4 tools)
|
||||
│ │ ├── ats-tools.ts # Recruiting/ATS (6 tools)
|
||||
│ │ ├── learning-tools.ts # Learning management (4 tools)
|
||||
│ │ ├── devices-tools.ts # Device/app inventory (4 tools)
|
||||
│ │ ├── groups-tools.ts # Group management (6 tools)
|
||||
│ │ └── custom-objects-tools.ts # Custom objects (5 tools)
|
||||
│ ├── types/
|
||||
│ │ └── index.ts # TypeScript type definitions
|
||||
│ ├── ui/
|
||||
│ │ └── react-app/ # 16 React UI components
|
||||
│ ├── server.ts # MCP server setup
|
||||
│ └── index.ts # Entry point
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
List engineering department devices:
|
||||
```
|
||||
list_devices(device_type: "laptop", limit: 50)
|
||||
## Type Safety
|
||||
|
||||
All API interactions are fully typed with TypeScript interfaces including:
|
||||
|
||||
- Employee, Department, Location, Team
|
||||
- PayRun, PayStatement, EarningsLine, TaxLine, DeductionLine
|
||||
- TimeEntry, Timesheet, TimeOffRequest
|
||||
- BenefitsPlan, BenefitsEnrollment, Dependent
|
||||
- Candidate, Job, Application, Interview
|
||||
- Course, CourseAssignment
|
||||
- Device, AppLicense
|
||||
- Group, CustomObject
|
||||
- PaginatedResponse, RipplingError
|
||||
|
||||
## Error Handling
|
||||
|
||||
The client automatically handles:
|
||||
|
||||
- HTTP errors with detailed messages
|
||||
- Rate limiting (with retry logic)
|
||||
- Invalid authentication
|
||||
- Malformed requests
|
||||
- Network errors
|
||||
|
||||
Errors are returned in a consistent format:
|
||||
|
||||
```typescript
|
||||
{
|
||||
message: string;
|
||||
code?: string;
|
||||
statusCode?: number;
|
||||
details?: any;
|
||||
}
|
||||
```
|
||||
|
||||
Get pending leave requests:
|
||||
```
|
||||
get_leave_requests(status: "pending")
|
||||
## Pagination
|
||||
|
||||
All list endpoints support cursor-based pagination:
|
||||
|
||||
```typescript
|
||||
// Manual pagination
|
||||
let cursor: string | undefined;
|
||||
do {
|
||||
const response = await client.listEmployees({ cursor, limit: 100 });
|
||||
// Process response.data
|
||||
cursor = response.nextCursor;
|
||||
} while (response.hasMore);
|
||||
|
||||
// Automatic pagination (gets all pages)
|
||||
const allEmployees = await client.getAllPaginated('/v1/employees');
|
||||
```
|
||||
|
||||
Get payroll for date range:
|
||||
```
|
||||
get_payroll(start_date: "2024-01-01", end_date: "2024-01-31")
|
||||
```
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check the [Rippling API Documentation](https://developer.rippling.com/)
|
||||
- Review the type definitions in `src/types/index.ts`
|
||||
- Examine the client implementation in `src/clients/rippling.ts`
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please ensure:
|
||||
- All new tools have proper Zod schemas
|
||||
- Type definitions are updated
|
||||
- Error handling is comprehensive
|
||||
- Tests are added (when test suite is created)
|
||||
|
||||
@ -1,20 +1,32 @@
|
||||
{
|
||||
"name": "mcp-server-rippling",
|
||||
"name": "@mcpengine/rippling-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Complete MCP server for Rippling HR Platform API",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": ["mcp", "rippling", "hr", "payroll", "ats", "benefits"],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"zod": "^3.22.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"recharts": "^2.15.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
344
servers/rippling/src/clients/rippling.ts
Normal file
344
servers/rippling/src/clients/rippling.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import type {
|
||||
RipplingConfig,
|
||||
PaginatedResponse,
|
||||
RipplingError,
|
||||
} from '../types/index.js';
|
||||
|
||||
export class RipplingClient {
|
||||
private client: AxiosInstance;
|
||||
private apiKey?: string;
|
||||
private accessToken?: string;
|
||||
|
||||
constructor(config: RipplingConfig) {
|
||||
this.apiKey = config.apiKey || process.env.RIPPLING_API_KEY;
|
||||
this.accessToken = config.accessToken || process.env.RIPPLING_ACCESS_TOKEN;
|
||||
|
||||
const baseURL = config.baseUrl || process.env.RIPPLING_BASE_URL || 'https://api.rippling.com';
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),
|
||||
...(this.apiKey && { 'X-API-Key': this.apiKey }),
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
throw this.handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private handleError(error: AxiosError): RipplingError {
|
||||
if (error.response) {
|
||||
return {
|
||||
message: (error.response.data as any)?.message || error.message,
|
||||
code: (error.response.data as any)?.code,
|
||||
statusCode: error.response.status,
|
||||
details: error.response.data,
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: error.message,
|
||||
statusCode: 500,
|
||||
};
|
||||
}
|
||||
|
||||
// Generic GET request
|
||||
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||
const response = await this.client.get<T>(endpoint, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic POST request
|
||||
async post<T>(endpoint: string, data?: any): Promise<T> {
|
||||
const response = await this.client.post<T>(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic PUT request
|
||||
async put<T>(endpoint: string, data?: any): Promise<T> {
|
||||
const response = await this.client.put<T>(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic PATCH request
|
||||
async patch<T>(endpoint: string, data?: any): Promise<T> {
|
||||
const response = await this.client.patch<T>(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic DELETE request
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
const response = await this.client.delete<T>(endpoint);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Paginated GET request with cursor-based pagination
|
||||
async getPaginated<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, any>,
|
||||
limit: number = 100
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const response = await this.client.get<PaginatedResponse<T>>(endpoint, {
|
||||
params: { ...params, limit },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Get all pages automatically
|
||||
async getAllPaginated<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, any>,
|
||||
limit: number = 100
|
||||
): Promise<T[]> {
|
||||
const allData: T[] = [];
|
||||
let cursor: string | undefined;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.getPaginated<T>(endpoint, { ...params, cursor }, limit);
|
||||
allData.push(...response.data);
|
||||
cursor = response.nextCursor;
|
||||
hasMore = response.hasMore && !!cursor;
|
||||
}
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
// Employees
|
||||
async listEmployees(params?: { status?: string; departmentId?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/employees', params);
|
||||
}
|
||||
|
||||
async getEmployee(id: string) {
|
||||
return this.get(`/v1/employees/${id}`);
|
||||
}
|
||||
|
||||
async createEmployee(data: any) {
|
||||
return this.post('/v1/employees', data);
|
||||
}
|
||||
|
||||
async updateEmployee(id: string, data: any) {
|
||||
return this.patch(`/v1/employees/${id}`, data);
|
||||
}
|
||||
|
||||
async terminateEmployee(id: string, data: { terminationDate: string; reason?: string }) {
|
||||
return this.post(`/v1/employees/${id}/terminate`, data);
|
||||
}
|
||||
|
||||
async listEmployeeCustomFields() {
|
||||
return this.get('/v1/employees/custom-fields');
|
||||
}
|
||||
|
||||
async getOrgChart() {
|
||||
return this.get('/v1/employees/org-chart');
|
||||
}
|
||||
|
||||
// Companies
|
||||
async getCompany() {
|
||||
return this.get('/v1/company');
|
||||
}
|
||||
|
||||
async listDepartments(params?: { cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/departments', params);
|
||||
}
|
||||
|
||||
async createDepartment(data: { name: string; parentId?: string; headId?: string }) {
|
||||
return this.post('/v1/departments', data);
|
||||
}
|
||||
|
||||
async listLocations(params?: { cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/locations', params);
|
||||
}
|
||||
|
||||
async listTeams(params?: { cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/teams', params);
|
||||
}
|
||||
|
||||
// Payroll
|
||||
async listPayRuns(params?: { status?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/payroll/runs', params);
|
||||
}
|
||||
|
||||
async getPayRun(id: string) {
|
||||
return this.get(`/v1/payroll/runs/${id}`);
|
||||
}
|
||||
|
||||
async listPayStatements(params?: { employeeId?: string; payRunId?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/payroll/statements', params);
|
||||
}
|
||||
|
||||
async getPayStatement(id: string) {
|
||||
return this.get(`/v1/payroll/statements/${id}`);
|
||||
}
|
||||
|
||||
// Time Tracking
|
||||
async listTimeEntries(params?: { employeeId?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/time/entries', params);
|
||||
}
|
||||
|
||||
async createTimeEntry(data: any) {
|
||||
return this.post('/v1/time/entries', data);
|
||||
}
|
||||
|
||||
async updateTimeEntry(id: string, data: any) {
|
||||
return this.patch(`/v1/time/entries/${id}`, data);
|
||||
}
|
||||
|
||||
async deleteTimeEntry(id: string) {
|
||||
return this.delete(`/v1/time/entries/${id}`);
|
||||
}
|
||||
|
||||
async getTimesheet(id: string) {
|
||||
return this.get(`/v1/time/timesheets/${id}`);
|
||||
}
|
||||
|
||||
async approveTimesheet(id: string) {
|
||||
return this.post(`/v1/time/timesheets/${id}/approve`, {});
|
||||
}
|
||||
|
||||
async listTimeOffRequests(params?: { employeeId?: string; status?: string; startDate?: string; endDate?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/time-off/requests', params);
|
||||
}
|
||||
|
||||
async createTimeOffRequest(data: any) {
|
||||
return this.post('/v1/time-off/requests', data);
|
||||
}
|
||||
|
||||
async approveTimeOffRequest(id: string) {
|
||||
return this.post(`/v1/time-off/requests/${id}/approve`, {});
|
||||
}
|
||||
|
||||
async denyTimeOffRequest(id: string, reason?: string) {
|
||||
return this.post(`/v1/time-off/requests/${id}/deny`, { reason });
|
||||
}
|
||||
|
||||
// Benefits
|
||||
async listBenefitsPlans(params?: { type?: string; isActive?: boolean; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/benefits/plans', params);
|
||||
}
|
||||
|
||||
async getBenefitsPlan(id: string) {
|
||||
return this.get(`/v1/benefits/plans/${id}`);
|
||||
}
|
||||
|
||||
async listBenefitsEnrollments(params?: { employeeId?: string; planId?: string; status?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/benefits/enrollments', params);
|
||||
}
|
||||
|
||||
async getBenefitsEnrollment(id: string) {
|
||||
return this.get(`/v1/benefits/enrollments/${id}`);
|
||||
}
|
||||
|
||||
// ATS (Applicant Tracking)
|
||||
async listCandidates(params?: { jobId?: string; stage?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/ats/candidates', params);
|
||||
}
|
||||
|
||||
async getCandidate(id: string) {
|
||||
return this.get(`/v1/ats/candidates/${id}`);
|
||||
}
|
||||
|
||||
async listJobs(params?: { status?: string; department?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/ats/jobs', params);
|
||||
}
|
||||
|
||||
async getJob(id: string) {
|
||||
return this.get(`/v1/ats/jobs/${id}`);
|
||||
}
|
||||
|
||||
async listApplications(params?: { candidateId?: string; jobId?: string; status?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/ats/applications', params);
|
||||
}
|
||||
|
||||
async updateApplicationStage(id: string, stage: string) {
|
||||
return this.patch(`/v1/ats/applications/${id}`, { stage });
|
||||
}
|
||||
|
||||
// Learning
|
||||
async listCourses(params?: { category?: string; isRequired?: boolean; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/learning/courses', params);
|
||||
}
|
||||
|
||||
async getCourse(id: string) {
|
||||
return this.get(`/v1/learning/courses/${id}`);
|
||||
}
|
||||
|
||||
async listCourseAssignments(params?: { employeeId?: string; courseId?: string; status?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/learning/assignments', params);
|
||||
}
|
||||
|
||||
async assignCourse(data: { employeeId: string; courseId: string; dueDate?: string }) {
|
||||
return this.post('/v1/learning/assignments', data);
|
||||
}
|
||||
|
||||
// Devices
|
||||
async listDevices(params?: { type?: string; status?: string; assignedTo?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/devices', params);
|
||||
}
|
||||
|
||||
async getDevice(id: string) {
|
||||
return this.get(`/v1/devices/${id}`);
|
||||
}
|
||||
|
||||
async listApps(params?: { cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/apps', params);
|
||||
}
|
||||
|
||||
async getApp(id: string) {
|
||||
return this.get(`/v1/apps/${id}`);
|
||||
}
|
||||
|
||||
// Groups
|
||||
async listGroups(params?: { type?: string; cursor?: string; limit?: number }) {
|
||||
return this.getPaginated('/v1/groups', params);
|
||||
}
|
||||
|
||||
async getGroup(id: string) {
|
||||
return this.get(`/v1/groups/${id}`);
|
||||
}
|
||||
|
||||
async createGroup(data: { name: string; description?: string; type?: string }) {
|
||||
return this.post('/v1/groups', data);
|
||||
}
|
||||
|
||||
async updateGroup(id: string, data: any) {
|
||||
return this.patch(`/v1/groups/${id}`, data);
|
||||
}
|
||||
|
||||
async addGroupMember(groupId: string, memberId: string) {
|
||||
return this.post(`/v1/groups/${groupId}/members`, { memberId });
|
||||
}
|
||||
|
||||
async removeGroupMember(groupId: string, memberId: string) {
|
||||
return this.delete(`/v1/groups/${groupId}/members/${memberId}`);
|
||||
}
|
||||
|
||||
// Custom Objects
|
||||
async listCustomObjects(objectType: string, params?: { cursor?: string; limit?: number }) {
|
||||
return this.getPaginated(`/v1/custom-objects/${objectType}`, params);
|
||||
}
|
||||
|
||||
async getCustomObject(objectType: string, id: string) {
|
||||
return this.get(`/v1/custom-objects/${objectType}/${id}`);
|
||||
}
|
||||
|
||||
async createCustomObject(objectType: string, data: any) {
|
||||
return this.post(`/v1/custom-objects/${objectType}`, data);
|
||||
}
|
||||
|
||||
async updateCustomObject(objectType: string, id: string, data: any) {
|
||||
return this.patch(`/v1/custom-objects/${objectType}/${id}`, data);
|
||||
}
|
||||
|
||||
async queryCustomObjects(objectType: string, query: any) {
|
||||
return this.post(`/v1/custom-objects/${objectType}/query`, query);
|
||||
}
|
||||
}
|
||||
@ -1,353 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { RipplingMCPServer } from './server.js';
|
||||
|
||||
// ============================================
|
||||
// CONFIGURATION
|
||||
// ============================================
|
||||
const MCP_NAME = "rippling";
|
||||
const MCP_VERSION = "1.0.0";
|
||||
|
||||
// Rippling API base URL
|
||||
const API_BASE_URL = "https://api.rippling.com/platform/api";
|
||||
|
||||
// ============================================
|
||||
// API CLIENT
|
||||
// ============================================
|
||||
class RipplingClient {
|
||||
private apiKey: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
this.baseUrl = API_BASE_URL;
|
||||
}
|
||||
|
||||
async request(endpoint: string, options: RequestInit = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Rippling API error: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get(endpoint: string) {
|
||||
return this.request(endpoint, { method: "GET" });
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL DEFINITIONS
|
||||
// ============================================
|
||||
const tools = [
|
||||
{
|
||||
name: "list_employees",
|
||||
description: "List employees in the organization. Returns employee details including name, email, department, and employment status.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max employees to return (default 100, max 1000)" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
include_terminated: { type: "boolean", description: "Include terminated employees (default false)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_employee",
|
||||
description: "Get detailed information about a specific employee including personal info, employment details, and manager.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
employee_id: { type: "string", description: "Employee ID (Rippling unique identifier)" },
|
||||
},
|
||||
required: ["employee_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_departments",
|
||||
description: "List all departments in the organization with their names and hierarchy.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max departments to return" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_teams",
|
||||
description: "List all teams in the organization. Teams are groups of employees that can span departments.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max teams to return" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_payroll",
|
||||
description: "Get payroll information and pay runs. Requires payroll read permissions.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
employee_id: { type: "string", description: "Filter by specific employee ID" },
|
||||
start_date: { type: "string", description: "Filter pay runs starting on or after (YYYY-MM-DD)" },
|
||||
end_date: { type: "string", description: "Filter pay runs ending on or before (YYYY-MM-DD)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_devices",
|
||||
description: "List devices managed by Rippling IT. Includes computers, phones, and other equipment assigned to employees.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max devices to return" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
employee_id: { type: "string", description: "Filter by assigned employee" },
|
||||
device_type: { type: "string", description: "Filter by type: laptop, desktop, phone, tablet" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_apps",
|
||||
description: "List applications integrated with Rippling. Shows apps available for provisioning to employees.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max apps to return" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_company",
|
||||
description: "Get information about the current company including name, EIN, and settings.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_groups",
|
||||
description: "List custom groups defined in Rippling. Groups can be used for access control and app provisioning.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_levels",
|
||||
description: "List job levels defined in the organization (e.g., IC1, IC2, Manager, Director).",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max levels to return" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_work_locations",
|
||||
description: "List work locations/offices defined in the organization.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
limit: { type: "number", description: "Max locations to return" },
|
||||
offset: { type: "number", description: "Pagination offset" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_leave_requests",
|
||||
description: "Get leave/time-off requests. Filter by employee, status, or date range.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
employee_id: { type: "string", description: "Filter by employee ID" },
|
||||
status: { type: "string", description: "Filter by status: pending, approved, denied, cancelled" },
|
||||
start_date: { type: "string", description: "Filter leave starting on or after (YYYY-MM-DD)" },
|
||||
end_date: { type: "string", description: "Filter leave ending on or before (YYYY-MM-DD)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TOOL HANDLERS
|
||||
// ============================================
|
||||
async function handleTool(client: RipplingClient, name: string, args: any) {
|
||||
switch (name) {
|
||||
case "list_employees": {
|
||||
const { limit = 100, offset = 0, include_terminated = false } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(Math.min(limit, 1000)));
|
||||
params.append("offset", String(offset));
|
||||
|
||||
const endpoint = include_terminated
|
||||
? `/employees?${params}&includeTerminated=true`
|
||||
: `/employees?${params}`;
|
||||
|
||||
return await client.get(endpoint);
|
||||
}
|
||||
|
||||
case "get_employee": {
|
||||
const { employee_id } = args;
|
||||
return await client.get(`/employees/${employee_id}`);
|
||||
}
|
||||
|
||||
case "list_departments": {
|
||||
const { limit = 100, offset = 0 } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String(offset));
|
||||
return await client.get(`/departments?${params}`);
|
||||
}
|
||||
|
||||
case "list_teams": {
|
||||
const { limit = 100, offset = 0 } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String(offset));
|
||||
return await client.get(`/teams?${params}`);
|
||||
}
|
||||
|
||||
case "get_payroll": {
|
||||
const { employee_id, start_date, end_date } = args;
|
||||
const params = new URLSearchParams();
|
||||
if (employee_id) params.append("employeeId", employee_id);
|
||||
if (start_date) params.append("startDate", start_date);
|
||||
if (end_date) params.append("endDate", end_date);
|
||||
|
||||
const query = params.toString();
|
||||
return await client.get(`/payroll${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
case "list_devices": {
|
||||
const { limit = 100, offset = 0, employee_id, device_type } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String(offset));
|
||||
if (employee_id) params.append("employeeId", employee_id);
|
||||
if (device_type) params.append("deviceType", device_type);
|
||||
|
||||
return await client.get(`/devices?${params}`);
|
||||
}
|
||||
|
||||
case "list_apps": {
|
||||
const { limit = 100, offset = 0 } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String(offset));
|
||||
return await client.get(`/apps?${params}`);
|
||||
}
|
||||
|
||||
case "get_company": {
|
||||
return await client.get("/companies/current");
|
||||
}
|
||||
|
||||
case "list_groups": {
|
||||
return await client.get("/groups");
|
||||
}
|
||||
|
||||
case "list_levels": {
|
||||
const { limit = 100, offset = 0 } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String(offset));
|
||||
return await client.get(`/levels?${params}`);
|
||||
}
|
||||
|
||||
case "list_work_locations": {
|
||||
const { limit = 100, offset = 0 } = args;
|
||||
const params = new URLSearchParams();
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String(offset));
|
||||
return await client.get(`/work-locations?${params}`);
|
||||
}
|
||||
|
||||
case "get_leave_requests": {
|
||||
const { employee_id, status, start_date, end_date } = args;
|
||||
const params = new URLSearchParams();
|
||||
if (employee_id) params.append("requestedBy", employee_id);
|
||||
if (status) params.append("status", status);
|
||||
if (start_date) params.append("from", start_date);
|
||||
if (end_date) params.append("to", end_date);
|
||||
|
||||
const query = params.toString();
|
||||
return await client.get(`/leave-requests${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVER SETUP
|
||||
// ============================================
|
||||
async function main() {
|
||||
const apiKey = process.env.RIPPLING_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("Error: RIPPLING_API_KEY environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new RipplingClient(apiKey);
|
||||
|
||||
const server = new Server(
|
||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await handleTool(client, name, args || {});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
const server = new RipplingMCPServer();
|
||||
server.run().catch(console.error);
|
||||
|
||||
103
servers/rippling/src/server.ts
Normal file
103
servers/rippling/src/server.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { RipplingClient } from './clients/rippling.js';
|
||||
import { createEmployeesTools } from './tools/employees-tools.js';
|
||||
import { createCompaniesTools } from './tools/companies-tools.js';
|
||||
import { createPayrollTools } from './tools/payroll-tools.js';
|
||||
import { createTimeTools } from './tools/time-tools.js';
|
||||
import { createBenefitsTools } from './tools/benefits-tools.js';
|
||||
import { createATSTools } from './tools/ats-tools.js';
|
||||
import { createLearningTools } from './tools/learning-tools.js';
|
||||
import { createDevicesTools } from './tools/devices-tools.js';
|
||||
import { createGroupsTools } from './tools/groups-tools.js';
|
||||
import { createCustomObjectsTools } from './tools/custom-objects-tools.js';
|
||||
|
||||
export class RipplingMCPServer {
|
||||
private server: Server;
|
||||
private client: RipplingClient;
|
||||
private tools: Record<string, any>;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'rippling-mcp-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize Rippling client
|
||||
this.client = new RipplingClient({
|
||||
apiKey: process.env.RIPPLING_API_KEY,
|
||||
accessToken: process.env.RIPPLING_ACCESS_TOKEN,
|
||||
baseUrl: process.env.RIPPLING_BASE_URL,
|
||||
});
|
||||
|
||||
// Combine all tools
|
||||
this.tools = {
|
||||
...createEmployeesTools(this.client),
|
||||
...createCompaniesTools(this.client),
|
||||
...createPayrollTools(this.client),
|
||||
...createTimeTools(this.client),
|
||||
...createBenefitsTools(this.client),
|
||||
...createATSTools(this.client),
|
||||
...createLearningTools(this.client),
|
||||
...createDevicesTools(this.client),
|
||||
...createGroupsTools(this.client),
|
||||
...createCustomObjectsTools(this.client),
|
||||
};
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers() {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: Object.entries(this.tools).map(([name, tool]) => ({
|
||||
name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
const tool = this.tools[name];
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.handler(args);
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error.message}\n${error.details ? JSON.stringify(error.details, null, 2) : ''}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Rippling MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
126
servers/rippling/src/tools/ats-tools.ts
Normal file
126
servers/rippling/src/tools/ats-tools.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createATSTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_candidates: {
|
||||
description: 'List all candidates in the ATS',
|
||||
inputSchema: z.object({
|
||||
jobId: z.string().optional().describe('Filter by job ID'),
|
||||
stage: z.string().optional().describe('Filter by current stage'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listCandidates(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_candidate: {
|
||||
description: 'Get detailed candidate information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Candidate ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCandidate(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_jobs: {
|
||||
description: 'List all job postings',
|
||||
inputSchema: z.object({
|
||||
status: z.enum(['DRAFT', 'OPEN', 'CLOSED', 'ON_HOLD']).optional().describe('Filter by job status'),
|
||||
department: z.string().optional().describe('Filter by department'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listJobs(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_job: {
|
||||
description: 'Get detailed job posting information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Job ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getJob(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_applications: {
|
||||
description: 'List job applications',
|
||||
inputSchema: z.object({
|
||||
candidateId: z.string().optional().describe('Filter by candidate ID'),
|
||||
jobId: z.string().optional().describe('Filter by job ID'),
|
||||
status: z.enum(['ACTIVE', 'HIRED', 'REJECTED', 'WITHDRAWN']).optional().describe('Filter by status'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listApplications(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_update_application_stage: {
|
||||
description: 'Move an application to a different stage in the pipeline',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Application ID'),
|
||||
stage: z.string().describe('New stage (e.g., "Phone Screen", "Technical Interview", "Offer")'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { id, stage } = args;
|
||||
const result = await client.updateApplicationStage(id, stage);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Application stage updated successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
85
servers/rippling/src/tools/benefits-tools.ts
Normal file
85
servers/rippling/src/tools/benefits-tools.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createBenefitsTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_benefits_plans: {
|
||||
description: 'List all benefits plans',
|
||||
inputSchema: z.object({
|
||||
type: z.enum(['HEALTH', 'DENTAL', 'VISION', '401K', 'FSA', 'HSA', 'LIFE', 'DISABILITY', 'OTHER']).optional().describe('Filter by plan type'),
|
||||
isActive: z.boolean().optional().describe('Filter by active status'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listBenefitsPlans(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_benefits_plan: {
|
||||
description: 'Get detailed information about a specific benefits plan',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Benefits plan ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getBenefitsPlan(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_benefits_enrollments: {
|
||||
description: 'List benefits enrollments',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().optional().describe('Filter by employee ID'),
|
||||
planId: z.string().optional().describe('Filter by plan ID'),
|
||||
status: z.enum(['ACTIVE', 'PENDING', 'TERMINATED', 'WAIVED']).optional().describe('Filter by enrollment status'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listBenefitsEnrollments(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_benefits_enrollment: {
|
||||
description: 'Get detailed enrollment information including dependents',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Enrollment ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getBenefitsEnrollment(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
99
servers/rippling/src/tools/companies-tools.ts
Normal file
99
servers/rippling/src/tools/companies-tools.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createCompaniesTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_get_company: {
|
||||
description: 'Get company information',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getCompany();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_departments: {
|
||||
description: 'List all departments',
|
||||
inputSchema: z.object({
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listDepartments(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_create_department: {
|
||||
description: 'Create a new department',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Department name'),
|
||||
parentId: z.string().optional().describe('Parent department ID'),
|
||||
headId: z.string().optional().describe('Department head employee ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createDepartment(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Department created successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_locations: {
|
||||
description: 'List all work locations',
|
||||
inputSchema: z.object({
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listLocations(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_teams: {
|
||||
description: 'List all teams',
|
||||
inputSchema: z.object({
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listTeams(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
108
servers/rippling/src/tools/custom-objects-tools.ts
Normal file
108
servers/rippling/src/tools/custom-objects-tools.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createCustomObjectsTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_custom_objects: {
|
||||
description: 'List custom objects of a specific type',
|
||||
inputSchema: z.object({
|
||||
objectType: z.string().describe('Custom object type name'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { objectType, ...params } = args;
|
||||
const result = await client.listCustomObjects(objectType, params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_custom_object: {
|
||||
description: 'Get a specific custom object',
|
||||
inputSchema: z.object({
|
||||
objectType: z.string().describe('Custom object type name'),
|
||||
id: z.string().describe('Object ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { objectType, id } = args;
|
||||
const result = await client.getCustomObject(objectType, id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_create_custom_object: {
|
||||
description: 'Create a new custom object',
|
||||
inputSchema: z.object({
|
||||
objectType: z.string().describe('Custom object type name'),
|
||||
fields: z.record(z.any()).describe('Object field values as key-value pairs'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { objectType, fields } = args;
|
||||
const result = await client.createCustomObject(objectType, fields);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Custom object created successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_update_custom_object: {
|
||||
description: 'Update a custom object',
|
||||
inputSchema: z.object({
|
||||
objectType: z.string().describe('Custom object type name'),
|
||||
id: z.string().describe('Object ID'),
|
||||
fields: z.record(z.any()).describe('Updated field values'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { objectType, id, fields } = args;
|
||||
const result = await client.updateCustomObject(objectType, id, fields);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Custom object updated successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_query_custom_objects: {
|
||||
description: 'Query custom objects with filters',
|
||||
inputSchema: z.object({
|
||||
objectType: z.string().describe('Custom object type name'),
|
||||
query: z.record(z.any()).describe('Query filters and conditions'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { objectType, query } = args;
|
||||
const result = await client.queryCustomObjects(objectType, query);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
83
servers/rippling/src/tools/devices-tools.ts
Normal file
83
servers/rippling/src/tools/devices-tools.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createDevicesTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_devices: {
|
||||
description: 'List all devices (laptops, phones, etc.)',
|
||||
inputSchema: z.object({
|
||||
type: z.enum(['LAPTOP', 'DESKTOP', 'PHONE', 'TABLET', 'OTHER']).optional().describe('Filter by device type'),
|
||||
status: z.enum(['AVAILABLE', 'ASSIGNED', 'REPAIR', 'RETIRED']).optional().describe('Filter by status'),
|
||||
assignedTo: z.string().optional().describe('Filter by assigned employee ID'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listDevices(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_device: {
|
||||
description: 'Get detailed device information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Device ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getDevice(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_apps: {
|
||||
description: 'List all app licenses and software',
|
||||
inputSchema: z.object({
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listApps(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_app: {
|
||||
description: 'Get detailed app/license information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('App ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getApp(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
166
servers/rippling/src/tools/employees-tools.ts
Normal file
166
servers/rippling/src/tools/employees-tools.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createEmployeesTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_employees: {
|
||||
description: 'List all employees with optional filters',
|
||||
inputSchema: z.object({
|
||||
status: z.enum(['ACTIVE', 'INACTIVE', 'TERMINATED']).optional().describe('Filter by employment status'),
|
||||
departmentId: z.string().optional().describe('Filter by department ID'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listEmployees(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_employee: {
|
||||
description: 'Get detailed information about a specific employee',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Employee ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getEmployee(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_create_employee: {
|
||||
description: 'Create a new employee record',
|
||||
inputSchema: z.object({
|
||||
firstName: z.string().describe('First name'),
|
||||
lastName: z.string().describe('Last name'),
|
||||
email: z.string().email().describe('Work email address'),
|
||||
personalEmail: z.string().email().optional().describe('Personal email'),
|
||||
phoneNumber: z.string().optional().describe('Phone number'),
|
||||
title: z.string().optional().describe('Job title'),
|
||||
department: z.string().optional().describe('Department ID'),
|
||||
manager: z.string().optional().describe('Manager employee ID'),
|
||||
startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
employmentType: z.enum(['FULL_TIME', 'PART_TIME', 'CONTRACT', 'INTERN']).optional().describe('Employment type'),
|
||||
workLocationId: z.string().optional().describe('Work location ID'),
|
||||
compensation: z.object({
|
||||
amount: z.number().optional(),
|
||||
currency: z.string().default('USD').optional(),
|
||||
frequency: z.enum(['HOURLY', 'ANNUALLY', 'MONTHLY']).optional(),
|
||||
}).optional().describe('Compensation details'),
|
||||
customFields: z.record(z.any()).optional().describe('Custom field values'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createEmployee(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Employee created successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_update_employee: {
|
||||
description: 'Update employee information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Employee ID'),
|
||||
firstName: z.string().optional().describe('First name'),
|
||||
lastName: z.string().optional().describe('Last name'),
|
||||
email: z.string().email().optional().describe('Work email'),
|
||||
phoneNumber: z.string().optional().describe('Phone number'),
|
||||
title: z.string().optional().describe('Job title'),
|
||||
department: z.string().optional().describe('Department ID'),
|
||||
manager: z.string().optional().describe('Manager employee ID'),
|
||||
workLocationId: z.string().optional().describe('Work location ID'),
|
||||
compensation: z.object({
|
||||
amount: z.number().optional(),
|
||||
currency: z.string().optional(),
|
||||
frequency: z.enum(['HOURLY', 'ANNUALLY', 'MONTHLY']).optional(),
|
||||
effectiveDate: z.string().optional(),
|
||||
}).optional().describe('Compensation details'),
|
||||
customFields: z.record(z.any()).optional().describe('Custom field values'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { id, ...data } = args;
|
||||
const result = await client.updateEmployee(id, data);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Employee updated successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_terminate_employee: {
|
||||
description: 'Terminate an employee',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Employee ID'),
|
||||
terminationDate: z.string().describe('Termination date (YYYY-MM-DD)'),
|
||||
reason: z.string().optional().describe('Termination reason'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { id, ...data } = args;
|
||||
const result = await client.terminateEmployee(id, data);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Employee terminated successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_employee_custom_fields: {
|
||||
description: 'List all custom fields configured for employees',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.listEmployeeCustomFields();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_org_chart: {
|
||||
description: 'Get the organizational chart structure',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getOrgChart();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
126
servers/rippling/src/tools/groups-tools.ts
Normal file
126
servers/rippling/src/tools/groups-tools.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createGroupsTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_groups: {
|
||||
description: 'List all groups',
|
||||
inputSchema: z.object({
|
||||
type: z.string().optional().describe('Filter by group type'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listGroups(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_group: {
|
||||
description: 'Get detailed group information including members',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Group ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getGroup(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_create_group: {
|
||||
description: 'Create a new group',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Group name'),
|
||||
description: z.string().optional().describe('Group description'),
|
||||
type: z.string().optional().describe('Group type'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createGroup(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Group created successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_update_group: {
|
||||
description: 'Update group information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Group ID'),
|
||||
name: z.string().optional().describe('Group name'),
|
||||
description: z.string().optional().describe('Group description'),
|
||||
type: z.string().optional().describe('Group type'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { id, ...data } = args;
|
||||
const result = await client.updateGroup(id, data);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Group updated successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_add_group_member: {
|
||||
description: 'Add a member to a group',
|
||||
inputSchema: z.object({
|
||||
groupId: z.string().describe('Group ID'),
|
||||
memberId: z.string().describe('Member (employee) ID to add'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { groupId, memberId } = args;
|
||||
const result = await client.addGroupMember(groupId, memberId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Member added to group successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_remove_group_member: {
|
||||
description: 'Remove a member from a group',
|
||||
inputSchema: z.object({
|
||||
groupId: z.string().describe('Group ID'),
|
||||
memberId: z.string().describe('Member (employee) ID to remove'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { groupId, memberId } = args;
|
||||
const result = await client.removeGroupMember(groupId, memberId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Member removed from group successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
87
servers/rippling/src/tools/learning-tools.ts
Normal file
87
servers/rippling/src/tools/learning-tools.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createLearningTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_courses: {
|
||||
description: 'List all training courses',
|
||||
inputSchema: z.object({
|
||||
category: z.string().optional().describe('Filter by category'),
|
||||
isRequired: z.boolean().optional().describe('Filter by required status'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listCourses(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_course: {
|
||||
description: 'Get detailed course information',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Course ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCourse(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_course_assignments: {
|
||||
description: 'List course assignments',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().optional().describe('Filter by employee ID'),
|
||||
courseId: z.string().optional().describe('Filter by course ID'),
|
||||
status: z.enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETED', 'OVERDUE']).optional().describe('Filter by status'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listCourseAssignments(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_assign_course: {
|
||||
description: 'Assign a course to an employee',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().describe('Employee ID'),
|
||||
courseId: z.string().describe('Course ID'),
|
||||
dueDate: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.assignCourse(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Course assigned successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
85
servers/rippling/src/tools/payroll-tools.ts
Normal file
85
servers/rippling/src/tools/payroll-tools.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createPayrollTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_pay_runs: {
|
||||
description: 'List all payroll runs',
|
||||
inputSchema: z.object({
|
||||
status: z.enum(['DRAFT', 'APPROVED', 'PROCESSING', 'COMPLETED', 'CANCELLED']).optional().describe('Filter by status'),
|
||||
startDate: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listPayRuns(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_pay_run: {
|
||||
description: 'Get detailed information about a specific pay run',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Pay run ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getPayRun(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_pay_statements: {
|
||||
description: 'List pay statements (paystubs)',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().optional().describe('Filter by employee ID'),
|
||||
payRunId: z.string().optional().describe('Filter by pay run ID'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listPayStatements(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_pay_statement: {
|
||||
description: 'Get detailed pay statement with earnings, taxes, and deductions',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Pay statement ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getPayStatement(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
215
servers/rippling/src/tools/time-tools.ts
Normal file
215
servers/rippling/src/tools/time-tools.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import { z } from 'zod';
|
||||
import type { RipplingClient } from '../clients/rippling.js';
|
||||
|
||||
export function createTimeTools(client: RipplingClient) {
|
||||
return {
|
||||
rippling_list_time_entries: {
|
||||
description: 'List time entries (clock in/out records)',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().optional().describe('Filter by employee ID'),
|
||||
startDate: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listTimeEntries(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_create_time_entry: {
|
||||
description: 'Create a new time entry',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().describe('Employee ID'),
|
||||
date: z.string().describe('Date (YYYY-MM-DD)'),
|
||||
clockIn: z.string().optional().describe('Clock in time (ISO 8601)'),
|
||||
clockOut: z.string().optional().describe('Clock out time (ISO 8601)'),
|
||||
hours: z.number().optional().describe('Total hours worked'),
|
||||
breakMinutes: z.number().optional().describe('Break duration in minutes'),
|
||||
notes: z.string().optional().describe('Additional notes'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createTimeEntry(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time entry created successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_update_time_entry: {
|
||||
description: 'Update an existing time entry',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Time entry ID'),
|
||||
clockIn: z.string().optional().describe('Clock in time (ISO 8601)'),
|
||||
clockOut: z.string().optional().describe('Clock out time (ISO 8601)'),
|
||||
hours: z.number().optional().describe('Total hours worked'),
|
||||
breakMinutes: z.number().optional().describe('Break duration in minutes'),
|
||||
notes: z.string().optional().describe('Additional notes'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { id, ...data } = args;
|
||||
const result = await client.updateTimeEntry(id, data);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time entry updated successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_delete_time_entry: {
|
||||
description: 'Delete a time entry',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Time entry ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteTimeEntry(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time entry deleted successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_get_timesheet: {
|
||||
description: 'Get a timesheet for a specific period',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Timesheet ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getTimesheet(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_approve_timesheet: {
|
||||
description: 'Approve a timesheet',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Timesheet ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.approveTimesheet(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Timesheet approved successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_list_time_off_requests: {
|
||||
description: 'List time off (PTO/vacation) requests',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().optional().describe('Filter by employee ID'),
|
||||
status: z.enum(['PENDING', 'APPROVED', 'DENIED', 'CANCELLED']).optional().describe('Filter by status'),
|
||||
startDate: z.string().optional().describe('Filter by start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('Filter by end date (YYYY-MM-DD)'),
|
||||
cursor: z.string().optional().describe('Pagination cursor'),
|
||||
limit: z.number().min(1).max(500).default(100).optional().describe('Number of results per page'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.listTimeOffRequests(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_create_time_off_request: {
|
||||
description: 'Create a new time off request',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().describe('Employee ID'),
|
||||
type: z.string().describe('Time off type (e.g., "Vacation", "Sick Leave", "Personal")'),
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
days: z.number().optional().describe('Number of days'),
|
||||
hours: z.number().optional().describe('Number of hours'),
|
||||
reason: z.string().optional().describe('Reason for time off'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createTimeOffRequest(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time off request created successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_approve_time_off_request: {
|
||||
description: 'Approve a time off request',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Time off request ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.approveTimeOffRequest(args.id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time off request approved successfully:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
rippling_deny_time_off_request: {
|
||||
description: 'Deny a time off request',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('Time off request ID'),
|
||||
reason: z.string().optional().describe('Reason for denial'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { id, reason } = args;
|
||||
const result = await client.denyTimeOffRequest(id, reason);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time off request denied:\n${JSON.stringify(result, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
336
servers/rippling/src/types/index.ts
Normal file
336
servers/rippling/src/types/index.ts
Normal file
@ -0,0 +1,336 @@
|
||||
// Rippling API Types
|
||||
|
||||
export interface RipplingConfig {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface Employee {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
personalEmail?: string;
|
||||
phoneNumber?: string;
|
||||
employeeId?: string;
|
||||
department?: string;
|
||||
title?: string;
|
||||
manager?: string;
|
||||
managerId?: string;
|
||||
startDate?: string;
|
||||
employmentType?: string;
|
||||
status?: 'ACTIVE' | 'INACTIVE' | 'TERMINATED';
|
||||
workLocationId?: string;
|
||||
homeAddress?: Address;
|
||||
compensation?: Compensation;
|
||||
customFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
street1?: string;
|
||||
street2?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zipCode?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface Compensation {
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
frequency?: 'HOURLY' | 'ANNUALLY' | 'MONTHLY';
|
||||
effectiveDate?: string;
|
||||
}
|
||||
|
||||
export interface Company {
|
||||
id: string;
|
||||
name: string;
|
||||
legalName?: string;
|
||||
ein?: string;
|
||||
address?: Address;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Department {
|
||||
id: string;
|
||||
name: string;
|
||||
parentId?: string;
|
||||
headId?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
id: string;
|
||||
name: string;
|
||||
address?: Address;
|
||||
timezone?: string;
|
||||
isHeadquarters?: boolean;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
managerId?: string;
|
||||
memberIds?: string[];
|
||||
}
|
||||
|
||||
export interface PayRun {
|
||||
id: string;
|
||||
payPeriodStart: string;
|
||||
payPeriodEnd: string;
|
||||
payDate: string;
|
||||
status: 'DRAFT' | 'APPROVED' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED';
|
||||
type?: 'REGULAR' | 'OFF_CYCLE' | 'BONUS';
|
||||
totalGrossPay?: number;
|
||||
totalNetPay?: number;
|
||||
employeeCount?: number;
|
||||
}
|
||||
|
||||
export interface PayStatement {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
payRunId: string;
|
||||
payDate: string;
|
||||
grossPay: number;
|
||||
netPay: number;
|
||||
taxes: number;
|
||||
deductions: number;
|
||||
earnings?: EarningsLine[];
|
||||
taxLines?: TaxLine[];
|
||||
deductionLines?: DeductionLine[];
|
||||
}
|
||||
|
||||
export interface EarningsLine {
|
||||
type: string;
|
||||
description?: string;
|
||||
amount: number;
|
||||
hours?: number;
|
||||
rate?: number;
|
||||
}
|
||||
|
||||
export interface TaxLine {
|
||||
type: string;
|
||||
description?: string;
|
||||
amount: number;
|
||||
employeeContribution?: number;
|
||||
employerContribution?: number;
|
||||
}
|
||||
|
||||
export interface DeductionLine {
|
||||
type: string;
|
||||
description?: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface TimeEntry {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
date: string;
|
||||
clockIn?: string;
|
||||
clockOut?: string;
|
||||
hours: number;
|
||||
breakMinutes?: number;
|
||||
status?: 'PENDING' | 'APPROVED' | 'REJECTED';
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface Timesheet {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
totalHours: number;
|
||||
status: 'DRAFT' | 'SUBMITTED' | 'APPROVED' | 'REJECTED';
|
||||
entries?: TimeEntry[];
|
||||
approverId?: string;
|
||||
approvedAt?: string;
|
||||
}
|
||||
|
||||
export interface TimeOffRequest {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
type: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
days: number;
|
||||
hours?: number;
|
||||
status: 'PENDING' | 'APPROVED' | 'DENIED' | 'CANCELLED';
|
||||
reason?: string;
|
||||
approverId?: string;
|
||||
approvedAt?: string;
|
||||
deniedReason?: string;
|
||||
}
|
||||
|
||||
export interface BenefitsPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'HEALTH' | 'DENTAL' | 'VISION' | '401K' | 'FSA' | 'HSA' | 'LIFE' | 'DISABILITY' | 'OTHER';
|
||||
carrier?: string;
|
||||
planYear?: string;
|
||||
isActive?: boolean;
|
||||
employeeCost?: number;
|
||||
employerCost?: number;
|
||||
}
|
||||
|
||||
export interface BenefitsEnrollment {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
planId: string;
|
||||
status: 'ACTIVE' | 'PENDING' | 'TERMINATED' | 'WAIVED';
|
||||
effectiveDate?: string;
|
||||
terminationDate?: string;
|
||||
coverageLevel?: string;
|
||||
dependents?: Dependent[];
|
||||
}
|
||||
|
||||
export interface Dependent {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
dateOfBirth: string;
|
||||
relationship: string;
|
||||
ssn?: string;
|
||||
}
|
||||
|
||||
export interface Candidate {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
resumeUrl?: string;
|
||||
currentStage?: string;
|
||||
currentJobId?: string;
|
||||
source?: string;
|
||||
appliedAt?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
title: string;
|
||||
department?: string;
|
||||
location?: string;
|
||||
employmentType?: 'FULL_TIME' | 'PART_TIME' | 'CONTRACT' | 'INTERN';
|
||||
status: 'DRAFT' | 'OPEN' | 'CLOSED' | 'ON_HOLD';
|
||||
description?: string;
|
||||
requirements?: string;
|
||||
hiringManagerId?: string;
|
||||
openings?: number;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
id: string;
|
||||
candidateId: string;
|
||||
jobId: string;
|
||||
stage: string;
|
||||
status: 'ACTIVE' | 'HIRED' | 'REJECTED' | 'WITHDRAWN';
|
||||
appliedAt: string;
|
||||
lastActivityAt?: string;
|
||||
interviews?: Interview[];
|
||||
}
|
||||
|
||||
export interface Interview {
|
||||
id: string;
|
||||
applicationId: string;
|
||||
scheduledAt?: string;
|
||||
duration?: number;
|
||||
interviewerIds?: string[];
|
||||
type?: string;
|
||||
feedback?: string;
|
||||
outcome?: 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
duration?: number;
|
||||
provider?: string;
|
||||
isRequired?: boolean;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface CourseAssignment {
|
||||
id: string;
|
||||
employeeId: string;
|
||||
courseId: string;
|
||||
assignedAt: string;
|
||||
dueDate?: string;
|
||||
completedAt?: string;
|
||||
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED' | 'OVERDUE';
|
||||
progress?: number;
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
type: 'LAPTOP' | 'DESKTOP' | 'PHONE' | 'TABLET' | 'OTHER';
|
||||
make?: string;
|
||||
model?: string;
|
||||
serialNumber?: string;
|
||||
assignedTo?: string;
|
||||
assignedAt?: string;
|
||||
status: 'AVAILABLE' | 'ASSIGNED' | 'REPAIR' | 'RETIRED';
|
||||
purchaseDate?: string;
|
||||
warrantyExpiration?: string;
|
||||
}
|
||||
|
||||
export interface AppLicense {
|
||||
id: string;
|
||||
name: string;
|
||||
vendor?: string;
|
||||
licenseType?: 'USER' | 'DEVICE' | 'ENTERPRISE';
|
||||
totalLicenses?: number;
|
||||
usedLicenses?: number;
|
||||
cost?: number;
|
||||
renewalDate?: string;
|
||||
assignedUsers?: string[];
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
memberIds?: string[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CustomObject {
|
||||
id: string;
|
||||
objectType: string;
|
||||
fields: Record<string, any>;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CustomField {
|
||||
id: string;
|
||||
name: string;
|
||||
fieldType: 'TEXT' | 'NUMBER' | 'DATE' | 'BOOLEAN' | 'SELECT' | 'MULTI_SELECT';
|
||||
options?: string[];
|
||||
isRequired?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
nextCursor?: string;
|
||||
hasMore: boolean;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface RipplingError {
|
||||
message: string;
|
||||
code?: string;
|
||||
statusCode?: number;
|
||||
details?: any;
|
||||
}
|
||||
127
servers/rippling/src/ui/react-app/ats-pipeline.tsx
Normal file
127
servers/rippling/src/ui/react-app/ats-pipeline.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import { User, Briefcase, TrendingUp } from 'lucide-react';
|
||||
import type { Application, Candidate } from '../../types/index.js';
|
||||
|
||||
interface ATSPipelineProps {
|
||||
applications?: Application[];
|
||||
candidates?: Candidate[];
|
||||
}
|
||||
|
||||
const stages = [
|
||||
'Applied',
|
||||
'Phone Screen',
|
||||
'Technical Interview',
|
||||
'Team Interview',
|
||||
'Final Round',
|
||||
'Offer',
|
||||
];
|
||||
|
||||
export const ATSPipeline: React.FC<ATSPipelineProps> = ({
|
||||
applications = [],
|
||||
candidates = [],
|
||||
}) => {
|
||||
const activeApplications = applications.filter(a => a.status === 'ACTIVE');
|
||||
|
||||
const applicationsByStage: Record<string, Application[]> = {};
|
||||
stages.forEach(stage => {
|
||||
applicationsByStage[stage] = activeApplications.filter(a => a.stage === stage);
|
||||
});
|
||||
|
||||
const totalCandidates = candidates.length;
|
||||
const hired = applications.filter(a => a.status === 'HIRED').length;
|
||||
const conversionRate = totalCandidates > 0 ? ((hired / totalCandidates) * 100).toFixed(1) : 0;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">ATS Pipeline</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Candidates</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{totalCandidates}</p>
|
||||
</div>
|
||||
<User className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Hired</p>
|
||||
<p className="text-3xl font-bold text-green-600">{hired}</p>
|
||||
</div>
|
||||
<Briefcase className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Conversion Rate</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{conversionRate}%</p>
|
||||
</div>
|
||||
<TrendingUp className="w-10 h-10 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="flex gap-4 min-w-max pb-4">
|
||||
{stages.map((stage) => {
|
||||
const apps = applicationsByStage[stage] || [];
|
||||
const count = apps.length;
|
||||
|
||||
return (
|
||||
<div key={stage} className="flex-shrink-0 w-64">
|
||||
<div className="bg-gray-100 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold">{stage}</h3>
|
||||
<span className="bg-blue-600 text-white text-xs px-2 py-1 rounded-full">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-[600px] overflow-y-auto">
|
||||
{apps.map((app) => {
|
||||
const candidate = candidates.find(c => c.id === app.candidateId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={app.id}
|
||||
className="bg-white border rounded-lg p-3 hover:shadow-md transition cursor-pointer"
|
||||
>
|
||||
{candidate && (
|
||||
<>
|
||||
<h4 className="font-semibold text-sm mb-1">
|
||||
{candidate.firstName} {candidate.lastName}
|
||||
</h4>
|
||||
{candidate.email && (
|
||||
<p className="text-xs text-gray-600 mb-2">{candidate.email}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="text-xs text-gray-500">
|
||||
Applied: {new Date(app.appliedAt).toLocaleDateString()}
|
||||
</div>
|
||||
{app.interviews && app.interviews.length > 0 && (
|
||||
<div className="mt-2 text-xs">
|
||||
<span className="bg-purple-100 text-purple-800 px-2 py-1 rounded">
|
||||
{app.interviews.length} interview{app.interviews.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
122
servers/rippling/src/ui/react-app/benefits-overview.tsx
Normal file
122
servers/rippling/src/ui/react-app/benefits-overview.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { Heart, Eye, Briefcase, Shield } from 'lucide-react';
|
||||
import type { BenefitsPlan, BenefitsEnrollment } from '../../types/index.js';
|
||||
|
||||
interface BenefitsOverviewProps {
|
||||
plans?: BenefitsPlan[];
|
||||
enrollments?: BenefitsEnrollment[];
|
||||
}
|
||||
|
||||
export const BenefitsOverview: React.FC<BenefitsOverviewProps> = ({
|
||||
plans = [],
|
||||
enrollments = [],
|
||||
}) => {
|
||||
const activeEnrollments = enrollments.filter(e => e.status === 'ACTIVE');
|
||||
|
||||
const getIconForType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'HEALTH':
|
||||
return <Heart className="w-6 h-6" />;
|
||||
case 'DENTAL':
|
||||
case 'VISION':
|
||||
return <Eye className="w-6 h-6" />;
|
||||
case '401K':
|
||||
return <Briefcase className="w-6 h-6" />;
|
||||
default:
|
||||
return <Shield className="w-6 h-6" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getColorForType = (type: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
HEALTH: 'bg-red-50 text-red-600 border-red-200',
|
||||
DENTAL: 'bg-blue-50 text-blue-600 border-blue-200',
|
||||
VISION: 'bg-purple-50 text-purple-600 border-purple-200',
|
||||
'401K': 'bg-green-50 text-green-600 border-green-200',
|
||||
FSA: 'bg-yellow-50 text-yellow-600 border-yellow-200',
|
||||
HSA: 'bg-orange-50 text-orange-600 border-orange-200',
|
||||
LIFE: 'bg-indigo-50 text-indigo-600 border-indigo-200',
|
||||
DISABILITY: 'bg-pink-50 text-pink-600 border-pink-200',
|
||||
};
|
||||
return colors[type] || 'bg-gray-50 text-gray-600 border-gray-200';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Benefits Overview</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600">Total Plans</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{plans.length}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600">Active Enrollments</p>
|
||||
<p className="text-3xl font-bold text-green-600">{activeEnrollments.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-lg">Available Plans</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{plans.map((plan) => {
|
||||
const enrollmentCount = enrollments.filter(e =>
|
||||
e.planId === plan.id && e.status === 'ACTIVE'
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={`border-2 rounded-lg p-4 ${getColorForType(plan.type)}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{getIconForType(plan.type)}
|
||||
<div>
|
||||
<h4 className="font-bold">{plan.name}</h4>
|
||||
<p className="text-xs opacity-75">{plan.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
{plan.isActive && (
|
||||
<span className="text-xs bg-white/50 px-2 py-1 rounded">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{plan.carrier && (
|
||||
<p className="text-sm mb-2">Carrier: {plan.carrier}</p>
|
||||
)}
|
||||
|
||||
{plan.planYear && (
|
||||
<p className="text-sm mb-2">Plan Year: {plan.planYear}</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-sm mt-3 pt-3 border-t border-current/20">
|
||||
{plan.employeeCost !== undefined && (
|
||||
<div>
|
||||
<p className="text-xs opacity-75">Employee Cost</p>
|
||||
<p className="font-bold">${plan.employeeCost}/mo</p>
|
||||
</div>
|
||||
)}
|
||||
{plan.employerCost !== undefined && (
|
||||
<div>
|
||||
<p className="text-xs opacity-75">Employer Cost</p>
|
||||
<p className="font-bold">${plan.employerCost}/mo</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t border-current/20">
|
||||
<p className="text-xs opacity-75">Enrollments</p>
|
||||
<p className="font-bold">{enrollmentCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
124
servers/rippling/src/ui/react-app/department-grid.tsx
Normal file
124
servers/rippling/src/ui/react-app/department-grid.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { Building2, Users, TrendingUp } from 'lucide-react';
|
||||
import type { Department, Employee } from '../../types/index.js';
|
||||
|
||||
interface DepartmentGridProps {
|
||||
departments?: Department[];
|
||||
employees?: Employee[];
|
||||
}
|
||||
|
||||
export const DepartmentGrid: React.FC<DepartmentGridProps> = ({
|
||||
departments = [],
|
||||
employees = [],
|
||||
}) => {
|
||||
const getDepartmentEmployees = (deptId: string) => {
|
||||
return employees.filter(e => e.department === deptId);
|
||||
};
|
||||
|
||||
const totalEmployees = employees.length;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Department Overview</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Departments</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{departments.length}</p>
|
||||
</div>
|
||||
<Building2 className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Employees</p>
|
||||
<p className="text-3xl font-bold text-green-600">{totalEmployees}</p>
|
||||
</div>
|
||||
<Users className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{departments.map((dept) => {
|
||||
const deptEmployees = getDepartmentEmployees(dept.id);
|
||||
const headEmployee = dept.headId
|
||||
? employees.find(e => e.id === dept.headId)
|
||||
: null;
|
||||
const percentage = totalEmployees > 0
|
||||
? ((deptEmployees.length / totalEmployees) * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={dept.id}
|
||||
className="border-2 border-gray-200 rounded-lg p-4 hover:shadow-lg transition hover:border-blue-400"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-blue-100 p-2 rounded-lg">
|
||||
<Building2 className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg">{dept.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{headEmployee && (
|
||||
<div className="bg-gray-50 rounded p-2 mb-3">
|
||||
<p className="text-xs text-gray-500 mb-1">Department Head</p>
|
||||
<p className="text-sm font-medium">
|
||||
{headEmployee.firstName} {headEmployee.lastName}
|
||||
</p>
|
||||
{headEmployee.title && (
|
||||
<p className="text-xs text-gray-600">{headEmployee.title}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm text-gray-600">Employees</span>
|
||||
</div>
|
||||
<span className="font-bold text-lg">{deptEmployees.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">{percentage}% of company</span>
|
||||
<div className="flex items-center gap-1 text-green-600">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
<span>Active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dept.createdAt && (
|
||||
<div className="mt-3 pt-3 border-t text-xs text-gray-500">
|
||||
Created: {new Date(dept.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{departments.length === 0 && (
|
||||
<p className="text-center text-gray-500 py-12">No departments found</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
156
servers/rippling/src/ui/react-app/device-inventory.tsx
Normal file
156
servers/rippling/src/ui/react-app/device-inventory.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Laptop, Smartphone, Monitor, Filter } from 'lucide-react';
|
||||
import type { Device } from '../../types/index.js';
|
||||
|
||||
interface DeviceInventoryProps {
|
||||
devices?: Device[];
|
||||
}
|
||||
|
||||
export const DeviceInventory: React.FC<DeviceInventoryProps> = ({
|
||||
devices = [],
|
||||
}) => {
|
||||
const [typeFilter, setTypeFilter] = useState('all');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
|
||||
const filteredDevices = devices.filter(d => {
|
||||
const matchesType = typeFilter === 'all' || d.type === typeFilter;
|
||||
const matchesStatus = statusFilter === 'all' || d.status === statusFilter;
|
||||
return matchesType && matchesStatus;
|
||||
});
|
||||
|
||||
const available = devices.filter(d => d.status === 'AVAILABLE').length;
|
||||
const assigned = devices.filter(d => d.status === 'ASSIGNED').length;
|
||||
const repair = devices.filter(d => d.status === 'REPAIR').length;
|
||||
|
||||
const getDeviceIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'LAPTOP':
|
||||
return <Laptop className="w-6 h-6" />;
|
||||
case 'PHONE':
|
||||
case 'TABLET':
|
||||
return <Smartphone className="w-6 h-6" />;
|
||||
case 'DESKTOP':
|
||||
return <Monitor className="w-6 h-6" />;
|
||||
default:
|
||||
return <Monitor className="w-6 h-6" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Device Inventory</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600">Available</p>
|
||||
<p className="text-3xl font-bold text-green-600">{available}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600">Assigned</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{assigned}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600">In Repair</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{repair}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div className="relative">
|
||||
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="pl-10 pr-8 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="LAPTOP">Laptop</option>
|
||||
<option value="DESKTOP">Desktop</option>
|
||||
<option value="PHONE">Phone</option>
|
||||
<option value="TABLET">Tablet</option>
|
||||
<option value="OTHER">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="pl-10 pr-8 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="AVAILABLE">Available</option>
|
||||
<option value="ASSIGNED">Assigned</option>
|
||||
<option value="REPAIR">In Repair</option>
|
||||
<option value="RETIRED">Retired</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mb-4">
|
||||
Showing {filteredDevices.length} of {devices.length} devices
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredDevices.map((device) => (
|
||||
<div key={device.id} className="border rounded-lg p-4 hover:shadow-md transition">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{getDeviceIcon(device.type)}
|
||||
<div>
|
||||
<h4 className="font-semibold">{device.make} {device.model}</h4>
|
||||
<p className="text-xs text-gray-500">{device.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
device.status === 'AVAILABLE' ? 'bg-green-100 text-green-800' :
|
||||
device.status === 'ASSIGNED' ? 'bg-blue-100 text-blue-800' :
|
||||
device.status === 'REPAIR' ? 'bg-orange-100 text-orange-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{device.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
{device.serialNumber && (
|
||||
<div>
|
||||
<span className="text-gray-500">Serial: </span>
|
||||
<span className="font-mono">{device.serialNumber}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.assignedTo && (
|
||||
<div>
|
||||
<span className="text-gray-500">Assigned to: </span>
|
||||
<span className="font-medium">{device.assignedTo}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.purchaseDate && (
|
||||
<div>
|
||||
<span className="text-gray-500">Purchase: </span>
|
||||
<span>{device.purchaseDate}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{device.warrantyExpiration && (
|
||||
<div className={
|
||||
new Date(device.warrantyExpiration) < new Date()
|
||||
? 'text-red-600'
|
||||
: ''
|
||||
}>
|
||||
<span className="text-gray-500">Warranty: </span>
|
||||
<span>{device.warrantyExpiration}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
80
servers/rippling/src/ui/react-app/employee-dashboard.tsx
Normal file
80
servers/rippling/src/ui/react-app/employee-dashboard.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { Users, TrendingUp, Calendar, DollarSign } from 'lucide-react';
|
||||
|
||||
interface EmployeeDashboardProps {
|
||||
totalEmployees?: number;
|
||||
activeEmployees?: number;
|
||||
newHires?: number;
|
||||
avgTenure?: string;
|
||||
departments?: Array<{ name: string; count: number }>;
|
||||
}
|
||||
|
||||
export const EmployeeDashboard: React.FC<EmployeeDashboardProps> = ({
|
||||
totalEmployees = 0,
|
||||
activeEmployees = 0,
|
||||
newHires = 0,
|
||||
avgTenure = 'N/A',
|
||||
departments = [],
|
||||
}) => {
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Employee Dashboard</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Employees</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{totalEmployees}</p>
|
||||
</div>
|
||||
<Users className="w-12 h-12 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Active</p>
|
||||
<p className="text-3xl font-bold text-green-600">{activeEmployees}</p>
|
||||
</div>
|
||||
<TrendingUp className="w-12 h-12 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">New Hires (30d)</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{newHires}</p>
|
||||
</div>
|
||||
<Calendar className="w-12 h-12 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Avg Tenure</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{avgTenure}</p>
|
||||
</div>
|
||||
<DollarSign className="w-12 h-12 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3">Employees by Department</h3>
|
||||
<div className="space-y-2">
|
||||
{departments.map((dept, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between bg-gray-50 p-3 rounded">
|
||||
<span className="font-medium">{dept.name}</span>
|
||||
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm">
|
||||
{dept.count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
136
servers/rippling/src/ui/react-app/employee-detail.tsx
Normal file
136
servers/rippling/src/ui/react-app/employee-detail.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import { Mail, Phone, MapPin, Briefcase, Calendar } from 'lucide-react';
|
||||
import type { Employee } from '../../types/index.js';
|
||||
|
||||
interface EmployeeDetailProps {
|
||||
employee?: Employee;
|
||||
}
|
||||
|
||||
export const EmployeeDetail: React.FC<EmployeeDetailProps> = ({ employee }) => {
|
||||
if (!employee) {
|
||||
return <div className="p-6">No employee data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow max-w-4xl">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold">
|
||||
{employee.firstName} {employee.lastName}
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">{employee.title}</p>
|
||||
</div>
|
||||
<span className={`px-4 py-2 rounded-full text-sm font-medium ${
|
||||
employee.status === 'ACTIVE' ? 'bg-green-100 text-green-800' :
|
||||
employee.status === 'INACTIVE' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{employee.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold border-b pb-2">Contact Information</h3>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Work Email</p>
|
||||
<p className="font-medium">{employee.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{employee.personalEmail && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Personal Email</p>
|
||||
<p className="font-medium">{employee.personalEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{employee.phoneNumber && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Phone</p>
|
||||
<p className="font-medium">{employee.phoneNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{employee.homeAddress && (
|
||||
<div className="flex items-center gap-3">
|
||||
<MapPin className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Address</p>
|
||||
<p className="font-medium">
|
||||
{employee.homeAddress.street1}
|
||||
{employee.homeAddress.city && `, ${employee.homeAddress.city}`}
|
||||
{employee.homeAddress.state && `, ${employee.homeAddress.state}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold border-b pb-2">Employment Details</h3>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Briefcase className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Department</p>
|
||||
<p className="font-medium">{employee.department || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{employee.manager && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Briefcase className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Manager</p>
|
||||
<p className="font-medium">{employee.manager}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{employee.startDate && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Calendar className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Start Date</p>
|
||||
<p className="font-medium">{employee.startDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{employee.employmentType && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Briefcase className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Employment Type</p>
|
||||
<p className="font-medium">{employee.employmentType.replace('_', ' ')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{employee.compensation && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600">Compensation</p>
|
||||
<p className="text-2xl font-bold text-blue-600">
|
||||
{employee.compensation.currency || '$'}
|
||||
{employee.compensation.amount?.toLocaleString()}
|
||||
<span className="text-sm font-normal text-gray-600">
|
||||
{' '}/ {employee.compensation.frequency?.toLowerCase()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
111
servers/rippling/src/ui/react-app/employee-directory.tsx
Normal file
111
servers/rippling/src/ui/react-app/employee-directory.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, Filter, Mail, Phone } from 'lucide-react';
|
||||
import type { Employee } from '../../types/index.js';
|
||||
|
||||
interface EmployeeDirectoryProps {
|
||||
employees?: Employee[];
|
||||
onSelectEmployee?: (employee: Employee) => void;
|
||||
}
|
||||
|
||||
export const EmployeeDirectory: React.FC<EmployeeDirectoryProps> = ({
|
||||
employees = [],
|
||||
onSelectEmployee,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [departmentFilter, setDepartmentFilter] = useState('all');
|
||||
|
||||
const departments = Array.from(new Set(employees.map(e => e.department).filter(Boolean)));
|
||||
|
||||
const filteredEmployees = employees.filter(emp => {
|
||||
const matchesSearch =
|
||||
emp.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
emp.lastName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
emp.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
emp.title?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesDepartment = departmentFilter === 'all' || emp.department === departmentFilter;
|
||||
|
||||
return matchesSearch && matchesDepartment;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Employee Directory</h2>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search employees..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<select
|
||||
value={departmentFilter}
|
||||
onChange={(e) => setDepartmentFilter(e.target.value)}
|
||||
className="pl-10 pr-8 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">All Departments</option>
|
||||
{departments.map((dept) => (
|
||||
<option key={dept} value={dept}>{dept}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mb-4">
|
||||
Showing {filteredEmployees.length} of {employees.length} employees
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredEmployees.map((employee) => (
|
||||
<div
|
||||
key={employee.id}
|
||||
onClick={() => onSelectEmployee?.(employee)}
|
||||
className="border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">
|
||||
{employee.firstName} {employee.lastName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">{employee.title}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
employee.status === 'ACTIVE' ? 'bg-green-100 text-green-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{employee.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{employee.department && (
|
||||
<div className="text-sm text-gray-500 mb-2">{employee.department}</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 text-sm">
|
||||
{employee.email && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-blue-600 truncate">{employee.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{employee.phoneNumber && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="w-4 h-4 text-gray-400" />
|
||||
<span>{employee.phoneNumber}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
19
servers/rippling/src/ui/react-app/index.tsx
Normal file
19
servers/rippling/src/ui/react-app/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
// Rippling MCP React App Components
|
||||
// Export all available UI components
|
||||
|
||||
export { EmployeeDashboard } from './employee-dashboard.js';
|
||||
export { EmployeeDetail } from './employee-detail.js';
|
||||
export { EmployeeDirectory } from './employee-directory.js';
|
||||
export { OrgChart } from './org-chart.js';
|
||||
export { PayrollDashboard } from './payroll-dashboard.js';
|
||||
export { PayrollDetail } from './payroll-detail.js';
|
||||
export { TimeTracker } from './time-tracker.js';
|
||||
export { TimesheetApprovals } from './timesheet-approvals.js';
|
||||
export { TimeOffCalendar } from './time-off-calendar.js';
|
||||
export { BenefitsOverview } from './benefits-overview.js';
|
||||
export { ATSPipeline } from './ats-pipeline.js';
|
||||
export { JobBoard } from './job-board.js';
|
||||
export { LearningDashboard } from './learning-dashboard.js';
|
||||
export { DeviceInventory } from './device-inventory.js';
|
||||
export { TeamOverview } from './team-overview.js';
|
||||
export { DepartmentGrid } from './department-grid.js';
|
||||
113
servers/rippling/src/ui/react-app/job-board.tsx
Normal file
113
servers/rippling/src/ui/react-app/job-board.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { Briefcase, MapPin, Clock, Users } from 'lucide-react';
|
||||
import type { Job } from '../../types/index.js';
|
||||
|
||||
interface JobBoardProps {
|
||||
jobs?: Job[];
|
||||
onApply?: (jobId: string) => void;
|
||||
}
|
||||
|
||||
export const JobBoard: React.FC<JobBoardProps> = ({
|
||||
jobs = [],
|
||||
onApply,
|
||||
}) => {
|
||||
const openJobs = jobs.filter(j => j.status === 'OPEN');
|
||||
const totalOpenings = openJobs.reduce((sum, j) => sum + (j.openings || 0), 0);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Job Board</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Open Positions</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{openJobs.length}</p>
|
||||
</div>
|
||||
<Briefcase className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Openings</p>
|
||||
<p className="text-3xl font-bold text-green-600">{totalOpenings}</p>
|
||||
</div>
|
||||
<Users className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{openJobs.map((job) => (
|
||||
<div key={job.id} className="border rounded-lg p-5 hover:shadow-md transition">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold mb-2">{job.title}</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-3 text-sm text-gray-600 mb-3">
|
||||
{job.department && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Briefcase className="w-4 h-4" />
|
||||
<span>{job.department}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.location && (
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>{job.location}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.employmentType && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>{job.employmentType.replace('_', ' ')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{job.openings && job.openings > 1 && (
|
||||
<span className="bg-purple-100 text-purple-800 px-3 py-1 rounded-full text-sm font-medium">
|
||||
{job.openings} openings
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{job.description && (
|
||||
<p className="text-gray-700 mb-3 line-clamp-3">{job.description}</p>
|
||||
)}
|
||||
|
||||
{job.requirements && (
|
||||
<div className="mb-3">
|
||||
<p className="text-sm font-semibold mb-1">Requirements:</p>
|
||||
<p className="text-sm text-gray-600 line-clamp-2">{job.requirements}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t">
|
||||
<div className="text-xs text-gray-500">
|
||||
Posted {job.createdAt ? new Date(job.createdAt).toLocaleDateString() : 'Recently'}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onApply?.(job.id)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition font-medium"
|
||||
>
|
||||
Apply Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{openJobs.length === 0 && (
|
||||
<p className="text-center text-gray-500 py-12">No open positions at this time</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
129
servers/rippling/src/ui/react-app/learning-dashboard.tsx
Normal file
129
servers/rippling/src/ui/react-app/learning-dashboard.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import { BookOpen, Award, Clock, TrendingUp } from 'lucide-react';
|
||||
import type { Course, CourseAssignment } from '../../types/index.js';
|
||||
|
||||
interface LearningDashboardProps {
|
||||
courses?: Course[];
|
||||
assignments?: CourseAssignment[];
|
||||
}
|
||||
|
||||
export const LearningDashboard: React.FC<LearningDashboardProps> = ({
|
||||
courses = [],
|
||||
assignments = [],
|
||||
}) => {
|
||||
const completed = assignments.filter(a => a.status === 'COMPLETED');
|
||||
const inProgress = assignments.filter(a => a.status === 'IN_PROGRESS');
|
||||
const overdue = assignments.filter(a => a.status === 'OVERDUE');
|
||||
const avgProgress = assignments.length > 0
|
||||
? assignments.reduce((sum, a) => sum + (a.progress || 0), 0) / assignments.length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Learning Dashboard</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Courses</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{courses.length}</p>
|
||||
</div>
|
||||
<BookOpen className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Completed</p>
|
||||
<p className="text-3xl font-bold text-green-600">{completed.length}</p>
|
||||
</div>
|
||||
<Award className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">In Progress</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{inProgress.length}</p>
|
||||
</div>
|
||||
<Clock className="w-10 h-10 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Avg Progress</p>
|
||||
<p className="text-3xl font-bold text-orange-600">{avgProgress.toFixed(0)}%</p>
|
||||
</div>
|
||||
<TrendingUp className="w-10 h-10 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{overdue.length > 0 && (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5 text-red-600" />
|
||||
<p className="font-semibold text-red-800">
|
||||
{overdue.length} overdue assignment{overdue.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<h3 className="font-semibold text-lg">Recent Assignments</h3>
|
||||
{assignments.slice(0, 10).map((assignment) => {
|
||||
const course = courses.find(c => c.id === assignment.courseId);
|
||||
const progress = assignment.progress || 0;
|
||||
|
||||
return (
|
||||
<div key={assignment.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold">{course?.title || 'Unknown Course'}</h4>
|
||||
<p className="text-sm text-gray-600">Employee ID: {assignment.employeeId}</p>
|
||||
{assignment.dueDate && (
|
||||
<p className="text-sm text-gray-500">Due: {assignment.dueDate}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
assignment.status === 'COMPLETED' ? 'bg-green-100 text-green-800' :
|
||||
assignment.status === 'IN_PROGRESS' ? 'bg-blue-100 text-blue-800' :
|
||||
assignment.status === 'OVERDUE' ? 'bg-red-100 text-red-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{assignment.status.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Progress</span>
|
||||
<span className="font-medium">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{assignment.completedAt && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Completed: {assignment.completedAt}
|
||||
{assignment.score !== undefined && ` • Score: ${assignment.score}%`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
76
servers/rippling/src/ui/react-app/org-chart.tsx
Normal file
76
servers/rippling/src/ui/react-app/org-chart.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { User, Users } from 'lucide-react';
|
||||
|
||||
interface OrgNode {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
email?: string;
|
||||
reports?: OrgNode[];
|
||||
}
|
||||
|
||||
interface OrgChartProps {
|
||||
data?: OrgNode;
|
||||
}
|
||||
|
||||
const OrgNodeComponent: React.FC<{ node: OrgNode; level: number }> = ({ node, level }) => {
|
||||
const hasReports = node.reports && node.reports.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`bg-white border-2 rounded-lg p-4 shadow-md min-w-[200px] ${
|
||||
level === 0 ? 'border-blue-500' : 'border-gray-300'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<User className="w-5 h-5 text-blue-600" />
|
||||
<h3 className="font-bold">{node.name}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-1">{node.title}</p>
|
||||
{node.email && (
|
||||
<p className="text-xs text-gray-500 truncate">{node.email}</p>
|
||||
)}
|
||||
{hasReports && (
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{node.reports!.length} direct report{node.reports!.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasReports && (
|
||||
<div className="relative">
|
||||
<div className="h-8 w-0.5 bg-gray-300 mx-auto" />
|
||||
<div className="flex gap-8 relative">
|
||||
{node.reports!.map((report, idx) => (
|
||||
<div key={report.id} className="relative">
|
||||
{idx > 0 && (
|
||||
<div className="absolute top-0 left-0 h-0.5 bg-gray-300" style={{ width: '100%' }} />
|
||||
)}
|
||||
<OrgNodeComponent node={report} level={level + 1} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const OrgChart: React.FC<OrgChartProps> = ({ data }) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow text-center">
|
||||
<p className="text-gray-500">No organizational chart data available</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 rounded-lg shadow overflow-auto">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center">Organization Chart</h2>
|
||||
<div className="flex justify-center">
|
||||
<OrgNodeComponent node={data} level={0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
119
servers/rippling/src/ui/react-app/payroll-dashboard.tsx
Normal file
119
servers/rippling/src/ui/react-app/payroll-dashboard.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { DollarSign, Calendar, TrendingUp, AlertCircle } from 'lucide-react';
|
||||
import type { PayRun } from '../../types/index.js';
|
||||
|
||||
interface PayrollDashboardProps {
|
||||
payRuns?: PayRun[];
|
||||
totalAnnualPayroll?: number;
|
||||
avgPayPerEmployee?: number;
|
||||
nextPayDate?: string;
|
||||
}
|
||||
|
||||
export const PayrollDashboard: React.FC<PayrollDashboardProps> = ({
|
||||
payRuns = [],
|
||||
totalAnnualPayroll = 0,
|
||||
avgPayPerEmployee = 0,
|
||||
nextPayDate,
|
||||
}) => {
|
||||
const recentRuns = payRuns.slice(0, 5);
|
||||
const completedRuns = payRuns.filter(r => r.status === 'COMPLETED');
|
||||
const pendingRuns = payRuns.filter(r => r.status === 'PROCESSING' || r.status === 'APPROVED');
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Payroll Dashboard</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Annual Payroll</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
${totalAnnualPayroll.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<DollarSign className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Avg Pay/Employee</p>
|
||||
<p className="text-2xl font-bold text-blue-600">
|
||||
${avgPayPerEmployee.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<TrendingUp className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Next Pay Date</p>
|
||||
<p className="text-xl font-bold text-purple-600">{nextPayDate || 'N/A'}</p>
|
||||
</div>
|
||||
<Calendar className="w-10 h-10 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Pending Runs</p>
|
||||
<p className="text-2xl font-bold text-orange-600">{pendingRuns.length}</p>
|
||||
</div>
|
||||
<AlertCircle className="w-10 h-10 text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3">Recent Pay Runs</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Pay Date</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Period</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Employees</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Gross Pay</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Net Pay</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{recentRuns.map((run) => (
|
||||
<tr key={run.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">{run.payDate}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">
|
||||
{run.payPeriodStart} - {run.payPeriodEnd}
|
||||
</td>
|
||||
<td className="px-4 py-3">{run.employeeCount}</td>
|
||||
<td className="px-4 py-3 font-medium">
|
||||
${run.totalGrossPay?.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium">
|
||||
${run.totalNetPay?.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
run.status === 'COMPLETED' ? 'bg-green-100 text-green-800' :
|
||||
run.status === 'PROCESSING' ? 'bg-blue-100 text-blue-800' :
|
||||
run.status === 'APPROVED' ? 'bg-purple-100 text-purple-800' :
|
||||
run.status === 'DRAFT' ? 'bg-gray-100 text-gray-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{run.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
184
servers/rippling/src/ui/react-app/payroll-detail.tsx
Normal file
184
servers/rippling/src/ui/react-app/payroll-detail.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import React from 'react';
|
||||
import { DollarSign, TrendingDown, TrendingUp, FileText } from 'lucide-react';
|
||||
import type { PayStatement } from '../../types/index.js';
|
||||
|
||||
interface PayrollDetailProps {
|
||||
payStatement?: PayStatement;
|
||||
}
|
||||
|
||||
export const PayrollDetail: React.FC<PayrollDetailProps> = ({ payStatement }) => {
|
||||
if (!payStatement) {
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow text-center">
|
||||
<p className="text-gray-500">No pay statement data available</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalEarnings = payStatement.earnings?.reduce((sum, e) => sum + e.amount, 0) || payStatement.grossPay;
|
||||
const totalTaxes = payStatement.taxLines?.reduce((sum, t) => sum + t.amount, 0) || payStatement.taxes;
|
||||
const totalDeductions = payStatement.deductionLines?.reduce((sum, d) => sum + d.amount, 0) || payStatement.deductions;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow max-w-4xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">Pay Statement</h2>
|
||||
<FileText className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg p-6 text-white mb-6">
|
||||
<p className="text-sm opacity-90 mb-1">Pay Date</p>
|
||||
<p className="text-2xl font-bold mb-4">{payStatement.payDate}</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm opacity-90">Gross Pay</p>
|
||||
<p className="text-3xl font-bold">${payStatement.grossPay.toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm opacity-90">Net Pay</p>
|
||||
<p className="text-3xl font-bold">${payStatement.netPay.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
<h3 className="font-semibold text-green-900">Earnings</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-green-600">${totalEarnings.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingDown className="w-5 h-5 text-red-600" />
|
||||
<h3 className="font-semibold text-red-900">Taxes</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-red-600">${totalTaxes.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 border-2 border-orange-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingDown className="w-5 h-5 text-orange-600" />
|
||||
<h3 className="font-semibold text-orange-900">Deductions</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-orange-600">${totalDeductions.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{payStatement.earnings && payStatement.earnings.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h3 className="font-semibold text-lg mb-3">Earnings Breakdown</h3>
|
||||
<div className="bg-gray-50 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Type</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Hours</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Rate</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{payStatement.earnings.map((earning, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="px-4 py-2">
|
||||
<div>
|
||||
<p className="font-medium">{earning.type}</p>
|
||||
{earning.description && (
|
||||
<p className="text-sm text-gray-600">{earning.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">{earning.hours || '-'}</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
{earning.rate ? `$${earning.rate}` : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right font-bold text-green-600">
|
||||
${earning.amount.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{payStatement.taxLines && payStatement.taxLines.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h3 className="font-semibold text-lg mb-3">Tax Breakdown</h3>
|
||||
<div className="bg-gray-50 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Type</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Employee</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Employer</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{payStatement.taxLines.map((tax, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="px-4 py-2">
|
||||
<div>
|
||||
<p className="font-medium">{tax.type}</p>
|
||||
{tax.description && (
|
||||
<p className="text-sm text-gray-600">{tax.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
${(tax.employeeContribution || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
${(tax.employerContribution || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right font-bold text-red-600">
|
||||
${tax.amount.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{payStatement.deductionLines && payStatement.deductionLines.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-3">Deductions</h3>
|
||||
<div className="bg-gray-50 rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-sm font-semibold">Type</th>
|
||||
<th className="px-4 py-2 text-right text-sm font-semibold">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{payStatement.deductionLines.map((deduction, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="px-4 py-2">
|
||||
<div>
|
||||
<p className="font-medium">{deduction.type}</p>
|
||||
{deduction.description && (
|
||||
<p className="text-sm text-gray-600">{deduction.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right font-bold text-orange-600">
|
||||
${deduction.amount.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
123
servers/rippling/src/ui/react-app/team-overview.tsx
Normal file
123
servers/rippling/src/ui/react-app/team-overview.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { Users, TrendingUp, Award, Target } from 'lucide-react';
|
||||
import type { Team, Employee } from '../../types/index.js';
|
||||
|
||||
interface TeamOverviewProps {
|
||||
teams?: Team[];
|
||||
employees?: Employee[];
|
||||
}
|
||||
|
||||
export const TeamOverview: React.FC<TeamOverviewProps> = ({
|
||||
teams = [],
|
||||
employees = [],
|
||||
}) => {
|
||||
const totalMembers = teams.reduce((sum, t) => sum + (t.memberIds?.length || 0), 0);
|
||||
const avgTeamSize = teams.length > 0 ? (totalMembers / teams.length).toFixed(1) : 0;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Team Overview</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Teams</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{teams.length}</p>
|
||||
</div>
|
||||
<Users className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Members</p>
|
||||
<p className="text-3xl font-bold text-green-600">{totalMembers}</p>
|
||||
</div>
|
||||
<Target className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Avg Team Size</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{avgTeamSize}</p>
|
||||
</div>
|
||||
<TrendingUp className="w-10 h-10 text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-lg">All Teams</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{teams.map((team) => {
|
||||
const memberCount = team.memberIds?.length || 0;
|
||||
const manager = team.managerId
|
||||
? employees.find(e => e.id === team.managerId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div key={team.id} className="border rounded-lg p-4 hover:shadow-md transition">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-blue-100 p-2 rounded-lg">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold">{team.name}</h4>
|
||||
{team.description && (
|
||||
<p className="text-sm text-gray-600">{team.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium">
|
||||
{memberCount} {memberCount === 1 ? 'member' : 'members'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{manager && (
|
||||
<div className="bg-gray-50 rounded p-3 mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Award className="w-4 h-4 text-gray-600" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Team Manager</p>
|
||||
<p className="text-sm font-medium">
|
||||
{manager.firstName} {manager.lastName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{team.memberIds && team.memberIds.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{team.memberIds.slice(0, 5).map((memberId) => {
|
||||
const member = employees.find(e => e.id === memberId);
|
||||
return member ? (
|
||||
<div
|
||||
key={memberId}
|
||||
className="bg-gray-100 px-2 py-1 rounded text-xs"
|
||||
title={`${member.firstName} ${member.lastName}`}
|
||||
>
|
||||
{member.firstName} {member.lastName.charAt(0)}.
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
{team.memberIds.length > 5 && (
|
||||
<div className="bg-gray-200 px-2 py-1 rounded text-xs font-medium">
|
||||
+{team.memberIds.length - 5} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
99
servers/rippling/src/ui/react-app/time-off-calendar.tsx
Normal file
99
servers/rippling/src/ui/react-app/time-off-calendar.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { Calendar, User, Clock } from 'lucide-react';
|
||||
import type { TimeOffRequest } from '../../types/index.js';
|
||||
|
||||
interface TimeOffCalendarProps {
|
||||
requests?: TimeOffRequest[];
|
||||
}
|
||||
|
||||
export const TimeOffCalendar: React.FC<TimeOffCalendarProps> = ({
|
||||
requests = [],
|
||||
}) => {
|
||||
const approved = requests.filter(r => r.status === 'APPROVED');
|
||||
const pending = requests.filter(r => r.status === 'PENDING');
|
||||
|
||||
// Group by month
|
||||
const requestsByMonth: Record<string, TimeOffRequest[]> = {};
|
||||
approved.forEach(req => {
|
||||
const month = req.startDate.substring(0, 7); // YYYY-MM
|
||||
if (!requestsByMonth[month]) requestsByMonth[month] = [];
|
||||
requestsByMonth[month].push(req);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Time Off Calendar</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Approved</p>
|
||||
<p className="text-3xl font-bold text-green-600">{approved.length}</p>
|
||||
</div>
|
||||
<Calendar className="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Pending</p>
|
||||
<p className="text-3xl font-bold text-yellow-600">{pending.length}</p>
|
||||
</div>
|
||||
<Clock className="w-10 h-10 text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Total Days</p>
|
||||
<p className="text-3xl font-bold text-blue-600">
|
||||
{approved.reduce((sum, r) => sum + r.days, 0)}
|
||||
</p>
|
||||
</div>
|
||||
<User className="w-10 h-10 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{Object.entries(requestsByMonth).map(([month, reqs]) => (
|
||||
<div key={month}>
|
||||
<h3 className="font-semibold text-lg mb-3">
|
||||
{new Date(month + '-01').toLocaleDateString('en-US', { year: 'numeric', month: 'long' })}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{reqs.map((req) => (
|
||||
<div key={req.id} className="border-l-4 border-blue-500 bg-gray-50 p-3 rounded">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<User className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium">Employee {req.employeeId}</span>
|
||||
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||
{req.type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{req.startDate} to {req.endDate}
|
||||
</div>
|
||||
{req.reason && (
|
||||
<p className="text-sm text-gray-500 mt-1">{req.reason}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-blue-600">{req.days}</p>
|
||||
<p className="text-xs text-gray-500">days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
112
servers/rippling/src/ui/react-app/time-tracker.tsx
Normal file
112
servers/rippling/src/ui/react-app/time-tracker.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Clock, Play, Pause, Calendar } from 'lucide-react';
|
||||
import type { TimeEntry } from '../../types/index.js';
|
||||
|
||||
interface TimeTrackerProps {
|
||||
entries?: TimeEntry[];
|
||||
onClockIn?: () => void;
|
||||
onClockOut?: () => void;
|
||||
currentEntry?: TimeEntry | null;
|
||||
}
|
||||
|
||||
export const TimeTracker: React.FC<TimeTrackerProps> = ({
|
||||
entries = [],
|
||||
onClockIn,
|
||||
onClockOut,
|
||||
currentEntry,
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
|
||||
const todayEntries = entries.filter(e => e.date === selectedDate);
|
||||
const totalHours = todayEntries.reduce((sum, e) => sum + e.hours, 0);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Time Tracker</h2>
|
||||
|
||||
<div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg p-6 text-white mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-8 h-8" />
|
||||
<div>
|
||||
<p className="text-sm opacity-90">Today's Hours</p>
|
||||
<p className="text-3xl font-bold">{totalHours.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentEntry ? (
|
||||
<button
|
||||
onClick={onClockOut}
|
||||
className="bg-red-500 hover:bg-red-600 px-6 py-3 rounded-lg flex items-center gap-2 transition"
|
||||
>
|
||||
<Pause className="w-5 h-5" />
|
||||
Clock Out
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={onClockIn}
|
||||
className="bg-green-500 hover:bg-green-600 px-6 py-3 rounded-lg flex items-center gap-2 transition"
|
||||
>
|
||||
<Play className="w-5 h-5" />
|
||||
Clock In
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentEntry && (
|
||||
<div className="bg-white/20 rounded p-3">
|
||||
<p className="text-sm">Clocked in at {currentEntry.clockIn}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2">View Date</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="pl-10 pr-4 py-2 border rounded-lg w-full md:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Time Entries</h3>
|
||||
{todayEntries.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">No time entries for this date</p>
|
||||
) : (
|
||||
todayEntries.map((entry) => (
|
||||
<div key={entry.id} className="border rounded-lg p-4 flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Clock In</span>
|
||||
<p className="font-medium">{entry.clockIn || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Clock Out</span>
|
||||
<p className="font-medium">{entry.clockOut || 'Active'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Break</span>
|
||||
<p className="font-medium">{entry.breakMinutes || 0} min</p>
|
||||
</div>
|
||||
</div>
|
||||
{entry.notes && (
|
||||
<p className="text-sm text-gray-600">{entry.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-blue-600">{entry.hours.toFixed(2)}</p>
|
||||
<p className="text-sm text-gray-500">hours</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
106
servers/rippling/src/ui/react-app/timesheet-approvals.tsx
Normal file
106
servers/rippling/src/ui/react-app/timesheet-approvals.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||
import type { Timesheet } from '../../types/index.js';
|
||||
|
||||
interface TimesheetApprovalsProps {
|
||||
timesheets?: Timesheet[];
|
||||
onApprove?: (id: string) => void;
|
||||
onReject?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const TimesheetApprovals: React.FC<TimesheetApprovalsProps> = ({
|
||||
timesheets = [],
|
||||
onApprove,
|
||||
onReject,
|
||||
}) => {
|
||||
const pending = timesheets.filter(t => t.status === 'SUBMITTED');
|
||||
const approved = timesheets.filter(t => t.status === 'APPROVED');
|
||||
const rejected = timesheets.filter(t => t.status === 'REJECTED');
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-2xl font-bold mb-6">Timesheet Approvals</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="w-6 h-6 text-yellow-600" />
|
||||
<span className="font-semibold">Pending</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-yellow-600">{pending.length}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
<span className="font-semibold">Approved</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-green-600">{approved.length}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<XCircle className="w-6 h-6 text-red-600" />
|
||||
<span className="font-semibold">Rejected</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-red-600">{rejected.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-semibold text-lg">Pending Approvals</h3>
|
||||
{pending.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">No pending timesheets</p>
|
||||
) : (
|
||||
pending.map((timesheet) => (
|
||||
<div key={timesheet.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h4 className="font-semibold">Employee ID: {timesheet.employeeId}</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{timesheet.periodStart} to {timesheet.periodEnd}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold text-blue-600">{timesheet.totalHours}</p>
|
||||
<p className="text-sm text-gray-500">hours</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{timesheet.entries && timesheet.entries.length > 0 && (
|
||||
<div className="bg-gray-50 rounded p-3 mb-3">
|
||||
<p className="text-sm font-medium mb-2">{timesheet.entries.length} entries</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{timesheet.entries.slice(0, 4).map((entry, idx) => (
|
||||
<div key={idx} className="flex justify-between">
|
||||
<span>{entry.date}</span>
|
||||
<span className="font-medium">{entry.hours}h</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onApprove?.(timesheet.id)}
|
||||
className="flex-1 bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 transition"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onReject?.(timesheet.id)}
|
||||
className="flex-1 bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 transition"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,14 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user