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
|
# 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
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Configuration
|
||||||
|
|
||||||
| Variable | Required | Description |
|
Set the following environment variables:
|
||||||
|----------|----------|-------------|
|
|
||||||
| `RIPPLING_API_KEY` | Yes | Bearer API key or OAuth access token |
|
|
||||||
|
|
||||||
## 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
|
## Usage
|
||||||
- **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
|
|
||||||
|
|
||||||
### Payroll
|
### As MCP Server
|
||||||
- **get_payroll** - Get payroll runs and compensation data
|
|
||||||
|
|
||||||
### IT
|
Add to your MCP settings configuration:
|
||||||
- **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`:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"rippling": {
|
"rippling": {
|
||||||
"command": "node",
|
"command": "node",
|
||||||
"args": ["/path/to/mcp-servers/rippling/dist/index.js"],
|
"args": ["/path/to/rippling/dist/index.js"],
|
||||||
"env": {
|
"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
|
# Run in development mode
|
||||||
Generate an API key in your Rippling admin settings for server-to-server integrations.
|
npm run dev
|
||||||
|
|
||||||
### 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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
List all employees including terminated:
|
## API Client
|
||||||
```
|
|
||||||
list_employees(include_terminated: true, limit: 100)
|
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:
|
## Type Safety
|
||||||
```
|
|
||||||
list_devices(device_type: "laptop", limit: 50)
|
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:
|
## Pagination
|
||||||
```
|
|
||||||
get_leave_requests(status: "pending")
|
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:
|
## License
|
||||||
```
|
|
||||||
get_payroll(start_date: "2024-01-01", end_date: "2024-01-31")
|
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",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"description": "Complete MCP server for Rippling HR Platform API",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
"start": "node dist/index.js",
|
"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": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"zod": "^3.22.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": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^22.10.5",
|
||||||
"tsx": "^4.7.0",
|
"@types/react": "^18.3.18",
|
||||||
"typescript": "^5.3.0"
|
"@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
|
#!/usr/bin/env node
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
import { RipplingMCPServer } from './server.js';
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import {
|
|
||||||
CallToolRequestSchema,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
// ============================================
|
const server = new RipplingMCPServer();
|
||||||
// CONFIGURATION
|
server.run().catch(console.error);
|
||||||
// ============================================
|
|
||||||
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);
|
|
||||||
|
|||||||
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": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user