Suma Harvest Plugin (v1.0.0)
Integrates with the Harvest time tracking API to sync project data, display budget dashboards, and send milestone alerts when budgets hit configurable thresholds.
Plugin Structure
suma-harvest/
├── suma-harvest.php ← Bootstrap, hooks, activation
├── inc/
│ ├── API.php ← Harvest API client (JoliCode)
│ ├── Data.php ← Database query layer
│ ├── Milestones.php ← Milestone CRUD operations
│ ├── Report.php ← Milestone checking logic
│ ├── Teams.php ← Microsoft Teams webhook sender
│ ├── models/
│ │ ├── Client.php ← Harvest client model
│ │ ├── Project.php ← Project model with budget
│ │ ├── Task.php ← Task definition model
│ │ ├── Time_Entry.php ← Individual time entry
│ │ └── Milestone.php ← Alert threshold model
│ └── routes/
│ ├── default.php ← Main dashboard view
│ ├── project.php ← Project detail view
│ ├── recent_time.php ← Recent time entries (7 days)
│ ├── user_project.php ← User breakdown for project
│ └── task_project.php ← Task breakdown for project
└── views/
├── dashboard.php ← Main dashboard template
├── project.php ← Project detail template
└── components/ ← Reusable UI components
Harvest API Sync
The API class wraps the JoliCode\Harvest\Client library.
Sync Flow
The full sync process (harvest_time_sync endpoint) executes in order:
1. connect() → Authenticate with Harvest API
2. prepopulate_data() → Fetch reference data (clients, tasks, users)
3. generate_data() → Fetch projects + time entries (with rate limiting)
4. save_data() → Write all data to custom database tables
5. archive_projects() → Mark completed projects as archived
Rate Limiting
Harvest enforces a 100 requests/15 seconds rate limit. The plugin adds a 10-second sleep between paginated calls:
private function fetch_with_delay(callable $api_call): array {
$results = [];
$page = 1;
do {
$response = $api_call($page);
$results = array_merge($results, $response->getResults());
$page++;
// 10-second delay between API calls
sleep(10);
} while ($response->hasNextPage());
return $results;
}
A full sync takes 10–20 minutes depending on the number of projects and time entries.
Authentication
use JoliCode\Harvest\ClientFactory;
$client = ClientFactory::create(
get_option('harvest_account_id'),
get_option('harvest_access_token')
);
Dashboard Views
The plugin provides 5 dashboard views accessible from the WordPress admin:
Main Dashboard (default)
Displays all active projects with budget information:
| Column | Description |
|---|---|
| Client | Harvest client name |
| Project | Project name (linked to detail) |
| Budget | Total budgeted hours |
| Used | Hours logged |
| % Used | Budget utilization percentage |
| Status | Color-coded budget indicator |
Budget Color Coding
| CSS Class | Range | Color | Meaning |
|---|---|---|---|
budget-fine | 0–24% | Green | Well within budget |
budget-quarter | 25–49% | Yellow | Quarter used |
budget-halfway | 50–74% | Orange | Half used |
budget-close | 75–99% | Red | Nearly exhausted |
budget-over | 100%+ | Dark Red/Bold | Over budget |
Project Detail (project)
Drills into a single project showing:
- Task breakdown with individual budgets
- Time entries grouped by task
- Collapsible task sections
- User contribution per task
Recent Time (recent_time)
Shows the last 7 days of time entries across all projects:
- Date, user, project, task, hours, notes
- Sortable columns
- Filterable by user or project
User Project (user_project)
Breaks down a specific user's time on a project:
- Daily entries for the selected user
- Running total
- Comparison to project/task budget
Task Project (task_project)
Shows all time entries for a specific task within a project:
- All contributors listed
- Budget vs. actual comparison
- Milestone trigger proximity
Milestone System
Milestones define budget thresholds that trigger alerts when reached.
Milestone Configuration
| Field | Type | Description |
|---|---|---|
kind | string | project or task |
calculation | string | Comparison operator |
value | int | Percentage threshold |
Comparison Operators
| Operator | Meaning |
|---|---|
greater_than_or_equal_to | Budget % >= threshold |
greater_than | Budget % > threshold |
equal_to | Budget % === threshold |
Alert Logic (Report Class)
public function check_milestone(Milestone $milestone, Project $project): bool {
$percentage = ($project->time_used / $project->budget) * 100;
return $this->does_milestone_trigger($milestone->calculation, $percentage, $milestone->value);
}
private function does_milestone_trigger(string $operator, float $actual, int $threshold): bool {
return match ($operator) {
'greater_than_or_equal_to' => $actual >= $threshold,
'greater_than' => $actual > $threshold,
'equal_to' => abs($actual - $threshold) < 0.5,
default => false,
};
}
Alert Deduplication
The harvest_milestone_history table prevents sending the same alert twice:
// Check if alert already sent
$already_sent = Data::milestone_history_exists(
$milestone->id,
$project->id,
$task_id
);
if (!$already_sent) {
$this->send_alert($milestone, $project, $task_id);
Data::record_milestone_history($milestone->id, $project->id, $task_id);
}
Microsoft Teams Integration
Project-Level Alerts
Sent via email to project managers (configured in Harvest project notes or ACF).
Task-Level Alerts
Sent as Teams webhook messages (MessageCard format):
class Teams {
public function send_webhook(string $webhook_url, array $data): bool {
$card = [
'@type' => 'MessageCard',
'@context' => 'http://schema.org/extensions',
'themeColor' => '0076D7',
'summary' => $data['title'],
'sections' => [[
'activityTitle' => $data['title'],
'facts' => [
['name' => 'Project', 'value' => $data['project']],
['name' => 'Task', 'value' => $data['task']],
['name' => 'Budget Used', 'value' => $data['percentage'] . '%'],
['name' => 'Milestone', 'value' => $data['milestone']],
],
'markdown' => true,
]],
'potentialAction' => [[
'@type' => 'OpenUri',
'name' => 'View in Harvest',
'targets' => [['os' => 'default', 'uri' => $data['harvest_url']]],
]],
];
wp_remote_post($webhook_url, [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode($card),
]);
}
}
Webhook URL Storage
Each Harvest user can have a Teams webhook URL stored in the teams_webhooks table:
SELECT url FROM teams_webhooks WHERE user_id = %d
Data Layer
The Data class provides static methods for database operations:
class Data {
// Projects
public static function get_active_projects(): array;
public static function get_project(int $project_id): ?Project;
public static function get_projects_for_client(int $client_id): array;
// Time entries
public static function get_time_entries(int $project_id, ?string $since = null): array;
public static function get_recent_time(int $days = 7): array;
public static function get_user_time(int $user_id, int $project_id): array;
public static function get_task_time(int $task_assignment_id): array;
// Milestones
public static function get_milestones(): array;
public static function milestone_history_exists(int $milestone_id, int $project_id, int $task_id): bool;
public static function record_milestone_history(int $milestone_id, int $project_id, int $task_id): void;
// Sync operations
public static function save_clients(array $clients): void;
public static function save_projects(array $projects): void;
public static function save_time_entries(array $entries): void;
public static function archive_completed(): void;
}
Frontend Stack
- CSS Framework: Bootstrap 5.3.2 (loaded from CDN)
- Tables: Standard HTML tables with Bootstrap classes
- Interactivity: Collapsible task sections via Bootstrap accordion
- Print: Print-friendly stylesheet for project reports
Configuration (ACF Options)
| Field | Type | Description |
|---|---|---|
harvest_account_id | Text | Harvest account identifier |
harvest_access_token | Text | Personal access token |
harvest_projects_to_exclude | Textarea | Project IDs to hide (one per line) |
harvest_projects_to_include | Textarea | Project IDs to show (overrides exclude) |
number_of_devs | Number | Developer count for capacity calculations |
Cron Integration
| Script | Schedule | Purpose |
|---|---|---|
| Harvest time sync | Daily (2 AM) | Full data synchronization |
| Update milestones | After sync | Check project-level thresholds |
| Update milestones tasks | After sync | Check task-level thresholds |