Skip to content

Commit

Permalink
feat: add search box to organization and contacts list (#7)
Browse files Browse the repository at this point in the history
* feat: add search box to organization and contacts list
* feat: add reset icon
* fix: better null handling
* fix: static code analyze & style issues
* fix: remove unneeded vars and functions
  • Loading branch information
tbreuss authored Feb 5, 2022
1 parent 5a9a94f commit 70996df
Show file tree
Hide file tree
Showing 19 changed files with 302 additions and 129 deletions.
14 changes: 5 additions & 9 deletions backend/src/Action/Contact/ContactListAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
// Get page param
$page = $this->getPageParam($request);
// Get query params from request
$params = $request->getQueryParams();
$page = isset($params['page']) ? (int)$params['page'] : 1;
$filter = isset($params['filter']) ? (string)$params['filter'] : '';

// Create the request DTO
$paginationInput = new PaginationInput($page);
$paginationInput = new PaginationInput($page, $filter);

// Invoke the Domain with inputs and retain the response
$userListOutput = $this->organizationService->listContacts($paginationInput);
Expand Down Expand Up @@ -64,10 +66,4 @@ private function transformResult(ContactListOutput $listOutput): array
}, $listOutput->data),
];
}

private function getPageParam(ServerRequestInterface $request): int
{
$params = $request->getQueryParams();
return isset($params['page']) ? (int)$params['page'] : 1;
}
}
7 changes: 3 additions & 4 deletions backend/src/Action/ErrorAction.php
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<?php

declare(strict_types=1);
declare(strict_types = 1);

namespace App\Action;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpInternalServerErrorException;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpGoneException;
use Slim\Exception\HttpInternalServerErrorException;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpNotImplementedException;
Expand All @@ -31,8 +31,8 @@ public function __invoke(
throw new HttpUnauthorizedException($request);
case 403:
throw new HttpForbiddenException($request);
default:
case 404:
default:
throw new HttpNotFoundException($request);
case 405:
throw new HttpMethodNotAllowedException($request);
Expand All @@ -50,7 +50,6 @@ public function __invoke(
break;
case 602:
trigger_error('Fatal run-time error', E_USER_ERROR);
break;
}

return $response->withHeader('Content-Type', 'application/json');
Expand Down
14 changes: 5 additions & 9 deletions backend/src/Action/Organization/OrganizationListAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
// Get page param
$page = $this->getPageParam($request);
// Get query params from request
$params = $request->getQueryParams();
$page = isset($params['page']) ? (int)$params['page'] : 1;
$filter = isset($params['filter']) ? (string)$params['filter'] : '';

// Create the request DTO
$paginationInput = new PaginationInput($page);
$paginationInput = new PaginationInput($page, $filter);

// Invoke the Domain with inputs and retain the response
$userListOutput = $this->userService->listOrganizations($paginationInput);
Expand Down Expand Up @@ -64,10 +66,4 @@ private function transformResult(OrganizationListOutput $organizationListRespons
}, $organizationListResponse->data),
];
}

private function getPageParam(ServerRequestInterface $request): int
{
$params = $request->getQueryParams();
return isset($params['page']) ? (int)$params['page'] : 1;
}
}
3 changes: 2 additions & 1 deletion backend/src/Data/PaginationInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
final class PaginationInput
{
public function __construct(
public int $page
public int $page,
public string $filter = ''
) {
}
}
1 change: 1 addition & 0 deletions backend/src/Data/PaginationOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ private function calculate(int $page, int $itemCountPerPage, int $totalItemCount
}
$pageCount = (int)ceil($totalItemCount / $itemCountPerPage);
$currentPage = min(max($page, 1), $pageCount);

return [$pageCount, $currentPage];
}
}
8 changes: 5 additions & 3 deletions backend/src/Handler/JsonErrorRenderer.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

declare(strict_types=1);
declare(strict_types = 1);

namespace App\Handler;

Expand All @@ -11,13 +11,14 @@ class JsonErrorRenderer extends \Slim\Error\AbstractErrorRenderer
/**
* @param Throwable $exception
* @param bool $displayErrorDetails
*
* @return string
*/
public function __invoke(Throwable $exception, bool $displayErrorDetails): string
{
$error = [
'message' => $this->getErrorTitle($exception),
'description' => $this->getErrorDescription($exception)
'description' => $this->getErrorDescription($exception),
];

if ($displayErrorDetails) {
Expand All @@ -27,11 +28,12 @@ public function __invoke(Throwable $exception, bool $displayErrorDetails): strin
} while ($exception = $exception->getPrevious());
}

return (string) json_encode($error, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return (string)json_encode($error, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}

/**
* @param Throwable $exception
*
* @return array<string|int>
*/
private function formatExceptionFragment(Throwable $exception): array
Expand Down
28 changes: 6 additions & 22 deletions backend/src/Handler/ShutdownHandler.php
Original file line number Diff line number Diff line change
@@ -1,46 +1,30 @@
<?php

declare(strict_types=1);
declare(strict_types = 1);

namespace App\Handler;

use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpInternalServerErrorException;
use Slim\ResponseEmitter;
use Slim\Handlers\ErrorHandler;
use Slim\ResponseEmitter;

class ShutdownHandler
{
/**
* @var Request
*/
private $request;
private Request $request;

/**
* @var HttpErrorHandler
*/
private $errorHandler;
private ErrorHandler $errorHandler;

/**
* @var bool
*/
private $displayErrorDetails;
private bool $displayErrorDetails;

/**
* ShutdownHandler constructor.
*
* @param Request $request
* @param HttpErrorHandler $errorHandler
* @param bool $displayErrorDetails
*/
public function __construct(Request $request, ErrorHandler $errorHandler, bool $displayErrorDetails)
{
$this->request = $request;
$this->errorHandler = $errorHandler;
$this->displayErrorDetails = $displayErrorDetails;
}

public function __invoke()
public function __invoke(): void
{
$error = error_get_last();
if ($error) {
Expand Down
96 changes: 81 additions & 15 deletions backend/src/Repository/OrganizationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ public function __construct(DecoratedPdo $connection)
$this->connection = $connection;
}

public function fetchContacts(int $offset, int $limit): array
public function fetchContacts(int $offset, int $limit, string $filter = ''): array
{
$params = ['limit' => $limit, 'offset' => $offset];
$params = $this->addFilterParam($filter, $params);
$where = $this->getFilterConditionForContactsQuery($filter);

$sql = <<<SQL
SELECT
c.id,
Expand All @@ -28,30 +32,38 @@ public function fetchContacts(int $offset, int $limit): array
o.name AS company_name
FROM contacts c
INNER JOIN organizations o ON c.organization_id = o.id
WHERE 1
$where
ORDER BY c.last_name, c.first_name
LIMIT :limit
OFFSET :offset
SQL;

return $this->connection->fetchAll($sql, [
'limit' => $limit,
'offset' => $offset,
]);
return $this->connection->fetchAll($sql, $params);
}

public function countContacts(): int
public function countContacts(string $filter = ''): int
{
$params = $this->addFilterParam($filter, []);
$where = $this->getFilterConditionForContactsQuery($filter);

$sql = <<<SQL
SELECT
COUNT(*) AS count
FROM contacts
FROM contacts AS c
WHERE 1
$where
SQL;

return (int)$this->connection->fetchValue($sql);
return (int)$this->connection->fetchValue($sql, $params);
}

public function fetchOrganizations(int $offset, int $limit): array
public function fetchOrganizations(int $offset, int $limit, string $filter = ''): array
{
$params = ['limit' => $limit, 'offset' => $offset];
$params = $this->addFilterParam($filter, $params);
$where = $this->getFilterConditionForOrganizationQuery($filter);

$sql = <<<SQL
SELECT
id,
Expand All @@ -60,15 +72,14 @@ public function fetchOrganizations(int $offset, int $limit): array
phone,
email
FROM organizations
WHERE 1
$where
ORDER BY name
LIMIT :limit
OFFSET :offset
SQL;

return $this->connection->fetchAll($sql, [
'limit' => $limit,
'offset' => $offset,
]);
return $this->connection->fetchAll($sql, $params);
}

public function fetchContact(int $id): ?array
Expand Down Expand Up @@ -98,15 +109,70 @@ public function fetchContact(int $id): ?array
return is_array($one) ? $one : null;
}

public function countOrganizations(): int
public function countOrganizations(string $filter = ''): int
{
$params = $this->addFilterParam($filter, []);
$where = $this->getFilterConditionForOrganizationQuery($filter);

$sql = <<<SQL
SELECT
COUNT(*) AS count
FROM organizations
WHERE 1
$where
SQL;

return (int)$this->connection->fetchValue($sql);
return (int)$this->connection->fetchValue($sql, $params);
}

private function getFilterConditionForOrganizationQuery(string $filter): string
{
if (strlen($filter) === 0) {
return '';
}

return <<<SQL
AND (
name LIKE :filter
OR email LIKE :filter
OR phone LIKE :filter
OR address LIKE :filter
OR city LIKE :filter
OR region LIKE :filter
OR country LIKE :filter
OR postal_code LIKE :filter
)
SQL;
}

private function getFilterConditionForContactsQuery(string $filter = ''): string
{
if (strlen($filter) === 0) {
return '';
}

return <<<SQL
AND (
c.first_name LIKE :filter
OR c.last_name LIKE :filter
OR c.email LIKE :filter
OR c.phone LIKE :filter
OR c.address LIKE :filter
OR c.city LIKE :filter
OR c.region LIKE :filter
OR c.country LIKE :filter
OR c.postal_code LIKE :filter
)
SQL;
}

private function addFilterParam(string $filter, array $params = []): array
{
if (strlen($filter) > 0) {
$params['filter'] = '%' . $filter . '%';
}

return $params;
}

public function fetchOrganization(int $id): ?array
Expand Down
16 changes: 12 additions & 4 deletions backend/src/Service/OrganizationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ public function listContacts(PaginationInput $input): ContactListOutput
// see https://docs.laminas.dev/laminas-paginator/usage/
$pagination = new PaginationOutput(
$input->page,
$this->repository->countContacts()
$this->repository->countContacts($input->filter)
);

$rows = $this->repository->fetchContacts($pagination->offset(), $pagination->limit());
$rows = $this->repository->fetchContacts(
$pagination->offset(),
$pagination->limit(),
$input->filter
);

$data = array_map(function (array $row): ContactListDataOutput {
return new ContactListDataOutput(
Expand Down Expand Up @@ -72,10 +76,14 @@ public function listOrganizations(PaginationInput $input): OrganizationListOutpu
// see https://docs.laminas.dev/laminas-paginator/usage/
$pagination = new PaginationOutput(
$input->page,
$this->repository->countOrganizations()
$this->repository->countOrganizations($input->filter)
);

$rows = $this->repository->fetchOrganizations($pagination->offset(), $pagination->limit());
$rows = $this->repository->fetchOrganizations(
$pagination->offset(),
$pagination->limit(),
$input->filter
);

$data = array_map(function (array $row): OrganizationListDataOutput {
return new OrganizationListDataOutput(
Expand Down
Loading

0 comments on commit 70996df

Please sign in to comment.