Skip to main content

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

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:

ColumnDescription
ClientHarvest client name
ProjectProject name (linked to detail)
BudgetTotal budgeted hours
UsedHours logged
% UsedBudget utilization percentage
StatusColor-coded budget indicator

Budget Color Coding

CSS ClassRangeColorMeaning
budget-fine0–24%GreenWell within budget
budget-quarter25–49%YellowQuarter used
budget-halfway50–74%OrangeHalf used
budget-close75–99%RedNearly exhausted
budget-over100%+Dark Red/BoldOver 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

FieldTypeDescription
kindstringproject or task
calculationstringComparison operator
valueintPercentage threshold

Comparison Operators

OperatorMeaning
greater_than_or_equal_toBudget % >= threshold
greater_thanBudget % > threshold
equal_toBudget % === 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)

FieldTypeDescription
harvest_account_idTextHarvest account identifier
harvest_access_tokenTextPersonal access token
harvest_projects_to_excludeTextareaProject IDs to hide (one per line)
harvest_projects_to_includeTextareaProject IDs to show (overrides exclude)
number_of_devsNumberDeveloper count for capacity calculations

Cron Integration

ScriptSchedulePurpose
Harvest time syncDaily (2 AM)Full data synchronization
Update milestonesAfter syncCheck project-level thresholds
Update milestones tasksAfter syncCheck task-level thresholds