Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Client/Bc4Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

use Dmstr\Bc4Client\Authentication\AuthenticationInterface;
use Dmstr\Bc4Client\Exception\RequestException;
use Dmstr\Bc4Client\Resource\CardsResource;
use Dmstr\Bc4Client\Resource\CardTablesResource;
use Dmstr\Bc4Client\Resource\ColumnsResource;
use Dmstr\Bc4Client\Resource\PeopleResource;
use Dmstr\Bc4Client\Resource\ProjectsResource;
use Dmstr\Bc4Client\Resource\TodoSetsResource;
Expand Down Expand Up @@ -38,6 +41,9 @@ class Bc4Client
private ?TodolistsResource $todolists = null;
private ?TodosResource $todos = null;
private ?PeopleResource $people = null;
private ?CardTablesResource $cardTables = null;
private ?ColumnsResource $columns = null;
private ?CardsResource $cards = null;

public function __construct(
private readonly string $accountId,
Expand Down Expand Up @@ -82,6 +88,21 @@ public function people(): PeopleResource
return $this->people ??= new PeopleResource($this);
}

public function cardTables(): CardTablesResource
{
return $this->cardTables ??= new CardTablesResource($this);
}

public function columns(): ColumnsResource
{
return $this->columns ??= new ColumnsResource($this);
}

public function cards(): CardsResource
{
return $this->cards ??= new CardsResource($this);
}

/**
* Single GET-or-write call to the BC4 API with full retry/refresh handling.
*
Expand Down
74 changes: 74 additions & 0 deletions src/Resource/CardTablesResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
// file generated with AI assistance: Claude Code - 2026-06-26

declare(strict_types=1);

namespace Dmstr\Bc4Client\Resource;

use Dmstr\Bc4Client\Exception\RequestException;

/**
* BC4 projects expose tools through the `dock` array. The "kanban_board" tool
* is the entry point to a project's card table — the card-table analogue of the
* "todoset" tool resolved by {@see TodoSetsResource}. The dock tool's `id`
* equals the card table id, so it can be loaded directly.
*/
class CardTablesResource extends AbstractResource
{
/**
* Resolve the kanban_board tool for a project (from the project's dock).
*
* @return array<string, mixed>|null
*/
public function findInProject(int|string $projectId): ?array
{
$project = $this->client->projects()->get($projectId);
if ($project === null) {
return null;
}

foreach ($project['dock'] ?? [] as $tool) {
if (($tool['name'] ?? null) === 'kanban_board' && ($tool['enabled'] ?? false)) {
return $tool;
}
}

return null;
}

/**
* Load a single card table. Its JSON embeds the `lists` array (= columns).
*
* @return array<string, mixed>|null
*/
public function get(int|string $projectId, int|string $cardTableId): ?array
{
try {
return $this->client->request(
'GET',
sprintf('/buckets/%s/card_tables/%s.json', $projectId, $cardTableId),
);
} catch (RequestException $e) {
if ($e->getStatusCode() === 404) {
return null;
}
throw $e;
}
}

/**
* Resolve the project's card table via its dock and load it. Returns null
* when the project has no enabled kanban_board tool.
*
* @return array<string, mixed>|null
*/
public function getInProject(int|string $projectId): ?array
{
$tool = $this->findInProject($projectId);
if ($tool === null || !isset($tool['id'])) {
return null;
}

return $this->get($projectId, $tool['id']);
}
}
107 changes: 107 additions & 0 deletions src/Resource/CardsResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
// file generated with AI assistance: Claude Code - 2026-06-26

declare(strict_types=1);

namespace Dmstr\Bc4Client\Resource;

use Dmstr\Bc4Client\Exception\RequestException;

/**
* Cards of a BC4 card table. Cards are the kanban-board analogue of todos:
* {@see getAllInProject()} mirrors {@see TodosResource::getAllInProject()} by
* walking every column of the project's card table and enriching each card with
* its `card_table { id, title }` and `column { id, title, type }` so consumers
* can group cards the same way they group todos by todolist.
*/
class CardsResource extends AbstractResource
{
/**
* @return array<int, array<string, mixed>>
*/
public function getInColumn(int|string $projectId, int|string $columnId): array
{
return $this->client->paginate(
sprintf('/buckets/%s/card_tables/lists/%s/cards.json', $projectId, $columnId),
);
}

/**
* Walk a project's card table (all columns) and return every card enriched
* with its card table and column. Returns an empty array when the project
* has no enabled card table.
*
* @return array<int, array<string, mixed>>
*/
public function getAllInProject(int|string $projectId): array
{
$table = $this->client->cardTables()->getInProject($projectId);
if ($table === null) {
return [];
}

$tableRef = ['id' => $table['id'] ?? null, 'title' => $table['title'] ?? null];
$cards = [];

foreach ($table['lists'] ?? [] as $column) {
if (!isset($column['id'])) {
continue;
}
$columnRef = [
'id' => $column['id'],
'title' => $column['title'] ?? null,
'type' => $column['type'] ?? null,
];
foreach ($this->getInColumn($projectId, $column['id']) as $card) {
$card['card_table'] = $tableRef;
$card['column'] = $columnRef;
$cards[] = $card;
}
}

return $cards;
}

/**
* Load a single card, enriched with its column and card table (resolved via
* the card's parent column) so single-record sync matches the scan output.
*
* @return array<string, mixed>|null
*/
public function get(int|string $projectId, int|string $cardId): ?array
{
try {
$card = $this->client->request(
'GET',
sprintf('/buckets/%s/card_tables/cards/%s.json', $projectId, $cardId),
);
} catch (RequestException $e) {
if ($e->getStatusCode() === 404) {
return null;
}
throw $e;
}

// A card's `parent` is its column; the column's `parent` is the card
// table. Resolve both so the enrichment matches getAllInProject().
$columnId = $card['parent']['id'] ?? null;
if ($columnId !== null) {
$column = $this->client->columns()->get($projectId, $columnId);
if ($column !== null) {
$card['column'] = [
'id' => $column['id'] ?? $columnId,
'title' => $column['title'] ?? null,
'type' => $column['type'] ?? null,
];
if (isset($column['parent']['id'])) {
$card['card_table'] = [
'id' => $column['parent']['id'],
'title' => $column['parent']['title'] ?? null,
];
}
}
}

return $card;
}
}
45 changes: 45 additions & 0 deletions src/Resource/ColumnsResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
// file generated with AI assistance: Claude Code - 2026-06-26

declare(strict_types=1);

namespace Dmstr\Bc4Client\Resource;

use Dmstr\Bc4Client\Exception\RequestException;

/**
* Columns (BC4 calls them "lists") of a card table. BC4 has no standalone
* "list all columns" endpoint — the columns are embedded as the `lists` array
* of the card table payload — so iteration goes through the card table. A single
* column can still be loaded directly (used to resolve a card's parent table).
*/
class ColumnsResource extends AbstractResource
{
/**
* @return array<int, array<string, mixed>>
*/
public function getInCardTable(int|string $projectId, int|string $cardTableId): array
{
$table = $this->client->cardTables()->get($projectId, $cardTableId);

return is_array($table['lists'] ?? null) ? $table['lists'] : [];
}

/**
* @return array<string, mixed>|null
*/
public function get(int|string $projectId, int|string $columnId): ?array
{
try {
return $this->client->request(
'GET',
sprintf('/buckets/%s/card_tables/columns/%s.json', $projectId, $columnId),
);
} catch (RequestException $e) {
if ($e->getStatusCode() === 404) {
return null;
}
throw $e;
}
}
}
Loading
Loading