feat: phase 2 — master data (products, customers, drivers) via InsForge-backed Filament pages

This commit is contained in:
sean
2026-08-25 18:33:56 +07:00
parent aad2b5e256
commit 4298544381
12 changed files with 870 additions and 38 deletions
+8 -6
View File
@@ -40,17 +40,17 @@ class InsForgeClient
return $this->request('GET', '/api/auth/profiles/current')->json();
}
public function listRecords(string $table, array $params = []): array
public function listRecords(string $table, array $params = [], bool $asUser = false): array
{
return $this->request('GET', "/api/database/records/{$table}", query: $params)->json() ?? [];
return $this->request('GET', "/api/database/records/{$table}", query: $params, asUser: $asUser)->json() ?? [];
}
public function getRecord(string $table, string $id, array $params = []): ?array
public function getRecord(string $table, string $id, array $params = [], bool $asUser = false): ?array
{
$params['id'] = 'eq.'.$id;
$params['limit'] = 1;
return $this->listRecords($table, $params)[0] ?? null;
return $this->listRecords($table, $params, $asUser)[0] ?? null;
}
public function insertRecord(string $table, array $data, bool $asUser = true): array
@@ -118,9 +118,11 @@ class InsForgeClient
$verb = strtolower($method);
$uri = $query !== [] ? $path.'?'.http_build_query($query) : $path;
$response = in_array($verb, ['get', 'head'])
? $http->{$verb}($path, $query)
: $http->{$verb}($path, $body);
? $http->{$verb}($uri)
: $http->{$verb}($uri, $body);
throw_unless($response->successful(), fn () => InsForgeException::fromResponse(
$response->status(),
@@ -0,0 +1,145 @@
<?php
namespace App\Services\InsForge;
use Illuminate\Pagination\LengthAwarePaginator;
abstract class InsForgeTableService
{
public function __construct(protected readonly InsForgeClient $client) {}
abstract protected function table(): string;
/** @return list<string> */
abstract protected function searchableColumns(): array;
abstract protected function sortableColumns(): array;
protected function defaultSortColumn(): string
{
return 'created_at';
}
protected function defaultSortDirection(): string
{
return 'desc';
}
/** @param array<string, mixed> $filters column => value (eq) */
public function paginate(
?string $search = null,
?string $sortColumn = null,
?string $sortDirection = null,
int $page = 1,
int $perPage = 25,
array $filters = [],
): LengthAwarePaginator {
$query = [
'select' => '*',
'limit' => max(1, min($perPage, 100)),
'offset' => ($page - 1) * $perPage,
'order' => sprintf(
'%s.%s.nullslast',
in_array($sortColumn, $this->sortableColumns(), true) ? $sortColumn : $this->defaultSortColumn(),
strtolower($sortDirection ?? '') === 'asc' ? 'asc' : $this->defaultSortDirection(),
),
];
foreach ($filters as $column => $value) {
if ($value !== null && $value !== '') {
$query[$column] = 'eq.'.$value;
}
}
if (filled($search) && $this->searchableColumns() !== []) {
$terms = str_replace(['(', ')', ','], ' ', $search);
$clauses = array_map(
fn (string $column): string => sprintf('%s.ilike.*%s*', $column, trim($terms)),
$this->searchableColumns(),
);
$query['or'] = '('.implode(',', $clauses).')';
}
[$client, $asUser] = $this->scoped();
$response = $client->send('GET', '/api/database/records/'.$this->table(), query: $query, asUser: $asUser, headers: ['Prefer' => 'count=exact']);
$rows = $response->json() ?? [];
$total = $this->parseTotal($response->header('Content-Range')) ?? count($rows);
return new LengthAwarePaginator(
items: $rows,
total: $total,
perPage: $perPage,
currentPage: $page,
);
}
public function find(string $id): ?array
{
[$client, $asUser] = $this->scoped();
return $client->getRecord($this->table(), $id, asUser: $asUser);
}
/** @param array<string, mixed> $filters */
public function findBy(array $filters): ?array
{
[$client, $asUser] = $this->scoped();
$params = ['limit' => 1];
foreach ($filters as $column => $value) {
$params[$column] = 'eq.'.$value;
}
return $client->listRecords($this->table(), $params, asUser: $asUser)[0] ?? null;
}
public function create(array $data): void
{
[$client, $asUser] = $this->scoped();
$client->insertRecord($this->table(), [$data], asUser: $asUser);
}
public function update(string $id, array $data): void
{
[$client, $asUser] = $this->scoped();
$client->updateRecords($this->table(), ['id' => $id], $data, asUser: $asUser);
}
public function delete(string $id): void
{
[$client, $asUser] = $this->scoped();
$client->deleteRecords($this->table(), ['id' => $id], asUser: $asUser);
}
/**
* Pakai JWT user saat ada sesi (RLS aktif); jatuh ke admin key untuk konteks tanpa sesi.
*
* @return array{0: InsForgeClient, 1: bool}
*/
private function scoped(): array
{
$token = session(InsForgeAuthService::SESSION_ACCESS_TOKEN);
return [
$token ? $this->client->withToken($token) : $this->client,
filled($token),
];
}
private function parseTotal(?string $contentRange): ?int
{
if (! filled($contentRange)) {
return null;
}
$total = explode('/', $contentRange)[1] ?? '';
return ctype_digit($total) ? (int) $total : null;
}
}