Refactor error handling and enhance API interactions across Filament pages
- Introduced `ApiErrorTranslator` to normalize and translate API error messages, providing clearer feedback in French. - Updated all Filament pages (Articles, Documents, Divers, Journaux, Tiers, TablesExplorer) to utilize the new error translation mechanism, improving user experience during API interactions. - Added validation for required fields before API calls, ensuring users receive immediate feedback when mandatory inputs are missing. - Implemented tracking properties to distinguish between "never searched" and "searched without results," enhancing the user interface. - Removed the obsolete `$results` property from the Articles page and added a new `$barcode` property to align with API requirements. - Updated documentation to reflect changes in API behavior and error handling, including new metadata returned by the `art_list` endpoint. - Added new tests to verify the functionality of the barcode handling and validation logic.
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
|||||||
|
|
||||||
use App\Exceptions\LogisticsApiException;
|
use App\Exceptions\LogisticsApiException;
|
||||||
use App\Services\LogisticsService;
|
use App\Services\LogisticsService;
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
|
||||||
@@ -23,9 +24,9 @@ class Articles extends Page
|
|||||||
|
|
||||||
public string $search = '';
|
public string $search = '';
|
||||||
|
|
||||||
public string $select = 'artid,artname';
|
public string $select = 'artid,name1';
|
||||||
|
|
||||||
public int $results = 10;
|
public string $barcode = '';
|
||||||
|
|
||||||
public string $stockArticleId = '';
|
public string $stockArticleId = '';
|
||||||
|
|
||||||
@@ -37,48 +38,62 @@ class Articles extends Page
|
|||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
|
public bool $hasSearched = false;
|
||||||
|
|
||||||
|
public bool $hasCheckedStock = false;
|
||||||
|
|
||||||
public function searchArticles(): void
|
public function searchArticles(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
$this->hasSearched = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
$params = array_filter([
|
$params = array_filter([
|
||||||
'select' => $this->select,
|
'select' => $this->select,
|
||||||
'results' => $this->results,
|
|
||||||
'search' => $this->search,
|
'search' => $this->search,
|
||||||
|
'barcode' => $this->barcode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $service->artList($params);
|
$response = $service->artList($params);
|
||||||
|
|
||||||
$this->data = $response['data'] ?? [];
|
$this->data = $response['data'] ?? [];
|
||||||
$this->metadata = $response['metadata'] ?? null;
|
$this->metadata = $response['metadata'] ?? null;
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
|
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStock(): void
|
public function getStock(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->stockArticleId)) {
|
if (blank($this->stockArticleId)) {
|
||||||
|
$this->errorMessage = 'Le champ identifiant article (ARTID) est obligatoire pour verifier le stock.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasCheckedStock = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->artGetStock($this->stockArticleId);
|
$response = $service->artGetStock($this->stockArticleId);
|
||||||
|
|
||||||
$this->stockData = $response['data'] ?? [];
|
$this->stockData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->stockData = [];
|
$this->stockData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->stockData = [];
|
$this->stockData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
|||||||
|
|
||||||
use App\Exceptions\LogisticsApiException;
|
use App\Exceptions\LogisticsApiException;
|
||||||
use App\Services\LogisticsService;
|
use App\Services\LogisticsService;
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
|
||||||
@@ -46,50 +47,72 @@ class Divers extends Page
|
|||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
|
// Tracking
|
||||||
|
public bool $hasSerial = false;
|
||||||
|
|
||||||
|
public bool $hasCodes = false;
|
||||||
|
|
||||||
|
public bool $hasUpdatedStock = false;
|
||||||
|
|
||||||
public function getSerialNumber(): void
|
public function getSerialNumber(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
$this->hasSerial = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->getSerialNumber();
|
$response = $service->getSerialNumber();
|
||||||
|
|
||||||
$this->serialData = is_array($response['data'] ?? null) ? $response['data'] : ['value' => $response['data'] ?? null];
|
$this->serialData = is_array($response['data'] ?? null) ? $response['data'] : ['value' => $response['data'] ?? null];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->serialData = [];
|
$this->serialData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->serialData = [];
|
$this->serialData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchCodes(): void
|
public function searchCodes(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->code)) {
|
if (blank($this->code)) {
|
||||||
|
$this->errorMessage = 'Le champ debut de code (code) est obligatoire.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasCodes = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->codesList(['code' => $this->code]);
|
$response = $service->codesList(['code' => $this->code]);
|
||||||
|
|
||||||
$this->codesData = $response['data'] ?? [];
|
$this->codesData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->codesData = [];
|
$this->codesData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->codesData = [];
|
$this->codesData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateStock(): void
|
public function updateStock(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->stkArtId) || blank($this->stkId) || blank($this->stkQty)) {
|
if (blank($this->stkArtId) || blank($this->stkId) || blank($this->stkQty)) {
|
||||||
|
$this->errorMessage = 'Les champs ARTID, STKID et QTY sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasUpdatedStock = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
@@ -105,12 +128,12 @@ class Divers extends Page
|
|||||||
$response = $service->customGeninvUpdatestock($params);
|
$response = $service->customGeninvUpdatestock($params);
|
||||||
|
|
||||||
$this->updateStockResult = $response['data'] ?? [];
|
$this->updateStockResult = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->updateStockResult = [];
|
$this->updateStockResult = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->updateStockResult = [];
|
$this->updateStockResult = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
|||||||
|
|
||||||
use App\Exceptions\LogisticsApiException;
|
use App\Exceptions\LogisticsApiException;
|
||||||
use App\Services\LogisticsService;
|
use App\Services\LogisticsService;
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
|
||||||
@@ -117,8 +118,30 @@ class Documents extends Page
|
|||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
|
// Tracking
|
||||||
|
public bool $hasSearchedDocs = false;
|
||||||
|
|
||||||
|
public bool $hasDetail = false;
|
||||||
|
|
||||||
|
public bool $hasStatus = false;
|
||||||
|
|
||||||
|
public bool $hasPrice = false;
|
||||||
|
|
||||||
|
public bool $hasDueDate = false;
|
||||||
|
|
||||||
|
public bool $hasAttach = false;
|
||||||
|
|
||||||
|
public bool $hasPdf = false;
|
||||||
|
|
||||||
|
public bool $hasAdded = false;
|
||||||
|
|
||||||
|
public bool $hasModified = false;
|
||||||
|
|
||||||
public function searchDocuments(): void
|
public function searchDocuments(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
$this->hasSearchedDocs = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
@@ -131,64 +154,82 @@ class Documents extends Page
|
|||||||
|
|
||||||
$this->data = $response['data'] ?? [];
|
$this->data = $response['data'] ?? [];
|
||||||
$this->metadata = $response['metadata'] ?? null;
|
$this->metadata = $response['metadata'] ?? null;
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDocumentDetail(): void
|
public function getDocumentDetail(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->detailJnl) || blank($this->detailNumber)) {
|
if (blank($this->detailJnl) || blank($this->detailNumber)) {
|
||||||
|
$this->errorMessage = 'Les champs code journal (jnl) et numero de document (number) sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasDetail = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->documentDetail($this->detailJnl, $this->detailNumber);
|
$response = $service->documentDetail($this->detailJnl, $this->detailNumber);
|
||||||
|
|
||||||
$this->detailData = $response['data'] ?? [];
|
$this->detailData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->detailData = [];
|
$this->detailData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->detailData = [];
|
$this->detailData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatusList(): void
|
public function getStatusList(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->statusJnl)) {
|
if (blank($this->statusJnl)) {
|
||||||
|
$this->errorMessage = 'Le champ code journal (jnl) est obligatoire.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasStatus = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->documentGetStatusList($this->statusJnl);
|
$response = $service->documentGetStatusList($this->statusJnl);
|
||||||
|
|
||||||
$this->statusData = $response['data'] ?? [];
|
$this->statusData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->statusData = [];
|
$this->statusData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->statusData = [];
|
$this->statusData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUnitPriceAndVat(): void
|
public function getUnitPriceAndVat(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->priceArtId) || blank($this->priceQty) || blank($this->priceJnl) || blank($this->priceThirdId) || blank($this->priceDate)) {
|
if (blank($this->priceArtId) || blank($this->priceQty) || blank($this->priceJnl) || blank($this->priceThirdId) || blank($this->priceDate)) {
|
||||||
|
$this->errorMessage = 'Tous les champs sont obligatoires : ARTID, QTY (format string), JNL, THIRDID et DATE.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasPrice = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->documentGetUnitPriceAndVat([
|
$response = $service->documentGetUnitPriceAndVat([
|
||||||
@@ -200,85 +241,109 @@ class Documents extends Page
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->priceData = $response['data'] ?? [];
|
$this->priceData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->priceData = [];
|
$this->priceData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->priceData = [];
|
$this->priceData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDueDate(): void
|
public function getDueDate(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->payDelay) || blank($this->dueDateInput)) {
|
if (blank($this->payDelay) || blank($this->dueDateInput)) {
|
||||||
|
$this->errorMessage = 'Les champs delai de paiement (paydelay) et date de depart (date) sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasDueDate = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->documentGetDueDate($this->payDelay, $this->dueDateInput);
|
$response = $service->documentGetDueDate($this->payDelay, $this->dueDateInput);
|
||||||
|
|
||||||
$this->dueDateData = $response['data'] ?? [];
|
$this->dueDateData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->dueDateData = [];
|
$this->dueDateData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->dueDateData = [];
|
$this->dueDateData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttachListThumbnail(): void
|
public function getAttachListThumbnail(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->attachJnl) || blank($this->attachNumber)) {
|
if (blank($this->attachJnl) || blank($this->attachNumber)) {
|
||||||
|
$this->errorMessage = 'Les champs code journal (JNL) et numero de document (NUMBER) sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasAttach = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->documentGetAttachListThumbnail($this->attachJnl, $this->attachNumber);
|
$response = $service->documentGetAttachListThumbnail($this->attachJnl, $this->attachNumber);
|
||||||
|
|
||||||
$this->attachData = $response['data'] ?? [];
|
$this->attachData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->attachData = [];
|
$this->attachData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->attachData = [];
|
$this->attachData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPdf(): void
|
public function getPdf(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->pdfJnl) || blank($this->pdfNumber) || blank($this->pdfLayout)) {
|
if (blank($this->pdfJnl) || blank($this->pdfNumber) || blank($this->pdfLayout)) {
|
||||||
|
$this->errorMessage = 'Les champs code journal (JNL), numero de document (NUMBER) et mise en page (LAYOUT) sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasPdf = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->documentGetPdf($this->pdfJnl, $this->pdfNumber, $this->pdfLayout);
|
$response = $service->documentGetPdf($this->pdfJnl, $this->pdfNumber, $this->pdfLayout);
|
||||||
|
|
||||||
$this->pdfData = $response['data'] ?? [];
|
$this->pdfData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->pdfData = [];
|
$this->pdfData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->pdfData = [];
|
$this->pdfData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addDocument(): void
|
public function addDocument(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->addThirdId) || blank($this->addDate) || blank($this->addArtIds) || blank($this->addQty) || blank($this->addSalePrice) || blank($this->addJnl)) {
|
if (blank($this->addThirdId) || blank($this->addDate) || blank($this->addArtIds) || blank($this->addQty) || blank($this->addSalePrice) || blank($this->addJnl)) {
|
||||||
|
$this->errorMessage = 'Les champs ThirdId, Date, Artid, Qty, Saleprice et JNL sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasAdded = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
@@ -304,22 +369,28 @@ class Documents extends Page
|
|||||||
$response = $service->documentAdd($params);
|
$response = $service->documentAdd($params);
|
||||||
|
|
||||||
$this->addResult = $response['data'] ?? [];
|
$this->addResult = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->addResult = [];
|
$this->addResult = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->addResult = [];
|
$this->addResult = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function modDocument(): void
|
public function modDocument(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->modNumber) || blank($this->modJnl)) {
|
if (blank($this->modNumber) || blank($this->modJnl)) {
|
||||||
|
$this->errorMessage = 'Les champs numero de document (number) et code journal (JNL) sont obligatoires.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasModified = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
@@ -344,12 +415,12 @@ class Documents extends Page
|
|||||||
$response = $service->documentMod($params);
|
$response = $service->documentMod($params);
|
||||||
|
|
||||||
$this->modResult = $response['data'] ?? [];
|
$this->modResult = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->modResult = [];
|
$this->modResult = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->modResult = [];
|
$this->modResult = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
|||||||
|
|
||||||
use App\Exceptions\LogisticsApiException;
|
use App\Exceptions\LogisticsApiException;
|
||||||
use App\Services\LogisticsService;
|
use App\Services\LogisticsService;
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
|
||||||
@@ -33,8 +34,20 @@ class Journaux extends Page
|
|||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
|
public bool $hasSearched = false;
|
||||||
|
|
||||||
public function searchJournaux(): void
|
public function searchJournaux(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
|
if (blank($this->type)) {
|
||||||
|
$this->errorMessage = 'Le champ type de journal (TYPE) est obligatoire.';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->hasSearched = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
@@ -48,12 +61,12 @@ class Journaux extends Page
|
|||||||
|
|
||||||
$this->data = $response['data'] ?? [];
|
$this->data = $response['data'] ?? [];
|
||||||
$this->metadata = $response['metadata'] ?? null;
|
$this->metadata = $response['metadata'] ?? null;
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
|||||||
|
|
||||||
use App\Exceptions\LogisticsApiException;
|
use App\Exceptions\LogisticsApiException;
|
||||||
use App\Services\LogisticsService;
|
use App\Services\LogisticsService;
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
use Livewire\Attributes\Url;
|
use Livewire\Attributes\Url;
|
||||||
@@ -92,11 +93,11 @@ class TablesExplorer extends Page
|
|||||||
|
|
||||||
$this->tables = $response['data'] ?? [];
|
$this->tables = $response['data'] ?? [];
|
||||||
$this->tablesMetadata = $response['metadata'] ?? null;
|
$this->tablesMetadata = $response['metadata'] ?? null;
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +117,12 @@ class TablesExplorer extends Page
|
|||||||
$rawColumns = $response['data'] ?? [];
|
$rawColumns = $response['data'] ?? [];
|
||||||
$this->columns = $this->deduplicateColumns($rawColumns);
|
$this->columns = $this->deduplicateColumns($rawColumns);
|
||||||
$this->columnsMetadata = $response['metadata'] ?? null;
|
$this->columnsMetadata = $response['metadata'] ?? null;
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->columns = [];
|
$this->columns = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->columns = [];
|
$this->columns = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Pages;
|
|||||||
|
|
||||||
use App\Exceptions\LogisticsApiException;
|
use App\Exceptions\LogisticsApiException;
|
||||||
use App\Services\LogisticsService;
|
use App\Services\LogisticsService;
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
|
||||||
@@ -37,8 +38,22 @@ class Tiers extends Page
|
|||||||
|
|
||||||
public ?string $errorMessage = null;
|
public ?string $errorMessage = null;
|
||||||
|
|
||||||
|
public bool $hasSearched = false;
|
||||||
|
|
||||||
|
public bool $hasHistory = false;
|
||||||
|
|
||||||
public function searchTiers(): void
|
public function searchTiers(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
|
if (blank($this->search)) {
|
||||||
|
$this->errorMessage = 'Le champ de recherche (search) est obligatoire.';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->hasSearched = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
|
|
||||||
@@ -52,33 +67,39 @@ class Tiers extends Page
|
|||||||
|
|
||||||
$this->data = $response['data'] ?? [];
|
$this->data = $response['data'] ?? [];
|
||||||
$this->metadata = $response['metadata'] ?? null;
|
$this->metadata = $response['metadata'] ?? null;
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->data = [];
|
$this->data = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArtHistory(): void
|
public function getArtHistory(): void
|
||||||
{
|
{
|
||||||
|
$this->errorMessage = null;
|
||||||
|
|
||||||
if (blank($this->historyThirdId)) {
|
if (blank($this->historyThirdId)) {
|
||||||
|
$this->errorMessage = 'Le champ identifiant tiers (thirdid) est obligatoire.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->hasHistory = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$response = $service->thirdGetArtHistory($this->historyThirdId);
|
$response = $service->thirdGetArtHistory($this->historyThirdId);
|
||||||
|
|
||||||
$this->historyData = $response['data'] ?? [];
|
$this->historyData = $response['data'] ?? [];
|
||||||
$this->errorMessage = $response['error'] ?? null;
|
$this->errorMessage = ApiErrorTranslator::translate($response['error'] ?? null);
|
||||||
} catch (LogisticsApiException $e) {
|
} catch (LogisticsApiException $e) {
|
||||||
$this->errorMessage = $e->getMessage();
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->historyData = [];
|
$this->historyData = [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
|
$this->errorMessage = ApiErrorTranslator::translate($e->getMessage());
|
||||||
$this->historyData = [];
|
$this->historyData = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
app/Support/ApiErrorTranslator.php
Normal file
74
app/Support/ApiErrorTranslator.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
class ApiErrorTranslator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private static array $explanations = [
|
||||||
|
'search terms are required' => 'Le champ de recherche est obligatoire.',
|
||||||
|
'invalid api key' => 'La cle API configuree est invalide ou absente.',
|
||||||
|
'api key is required' => 'La cle API est requise dans les en-tetes de la requete.',
|
||||||
|
'not found' => "L'element demande n'a pas ete trouve dans la base de donnees.",
|
||||||
|
'layout not found' => 'La valeur du parametre LAYOUT est inconnue ou invalide.',
|
||||||
|
'unknown stkid' => "L'identifiant de stock (STKID) fourni est inconnu.",
|
||||||
|
'invalid date' => 'Le format de date fourni est invalide.',
|
||||||
|
'thirdid is required' => "L'identifiant du tiers est obligatoire.",
|
||||||
|
'jnl is required' => 'Le code journal est obligatoire.',
|
||||||
|
'number is required' => 'Le numero de document est obligatoire.',
|
||||||
|
'artid is required' => "L'identifiant de l'article est obligatoire.",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise et traduit un message d'erreur API.
|
||||||
|
*
|
||||||
|
* Le champ `error` de l'API peut etre null, une chaine ou un tableau.
|
||||||
|
*/
|
||||||
|
public static function translate(mixed $error): ?string
|
||||||
|
{
|
||||||
|
if ($error === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = self::normalize($error);
|
||||||
|
|
||||||
|
if ($message === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$explanation = self::findExplanation($message);
|
||||||
|
|
||||||
|
if ($explanation !== null) {
|
||||||
|
return "{$message}\n\nExplication : {$explanation}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise une valeur error (string ou array) en chaine lisible.
|
||||||
|
*/
|
||||||
|
public static function normalize(mixed $error): string
|
||||||
|
{
|
||||||
|
if (is_array($error)) {
|
||||||
|
return implode(' ', array_map('strval', $error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function findExplanation(string $message): ?string
|
||||||
|
{
|
||||||
|
$lower = mb_strtolower($message);
|
||||||
|
|
||||||
|
foreach (self::$explanations as $pattern => $explanation) {
|
||||||
|
if (str_contains($lower, $pattern)) {
|
||||||
|
return $explanation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Documentation API Logistics (Flex/ESI Gescom)
|
# Documentation API Logistics (Flex/ESI Gescom)
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ Content-Type: application/json
|
|||||||
{
|
{
|
||||||
"search": "chaise",
|
"search": "chaise",
|
||||||
"results": 10,
|
"results": 10,
|
||||||
"select": "artid,artname"
|
"select": "artid,name1"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -185,6 +185,11 @@ Le service `LogisticsService` de l'application gère automatiquement :
|
|||||||
- **Retry automatique** : en cas d'échec de connexion (`ConnectionException`), le service retente automatiquement selon la configuration (`retry.times` et `retry.sleep_ms`).
|
- **Retry automatique** : en cas d'échec de connexion (`ConnectionException`), le service retente automatiquement selon la configuration (`retry.times` et `retry.sleep_ms`).
|
||||||
- **Logging** : chaque requête (réussie ou échouée) est enregistrée dans la table `api_request_logs` avec l'endpoint, les paramètres, le code de statut et la réponse.
|
- **Logging** : chaque requête (réussie ou échouée) est enregistrée dans la table `api_request_logs` avec l'endpoint, les paramètres, le code de statut et la réponse.
|
||||||
- **Exceptions** : en cas d'erreur, une `LogisticsApiException` est levée avec un message explicite en français.
|
- **Exceptions** : en cas d'erreur, une `LogisticsApiException` est levée avec un message explicite en français.
|
||||||
|
- **Traduction des erreurs** : la classe `App\Support\ApiErrorTranslator` normalise le champ `error` de la réponse API (qui peut être `null`, une chaîne ou un tableau de chaînes) et ajoute une explication en français aux messages d'erreur connus (ex : "Search terms are required" est enrichi de "Le champ de recherche est obligatoire.").
|
||||||
|
|
||||||
|
### Validation des champs
|
||||||
|
|
||||||
|
Avant chaque appel API, les pages Filament vérifient que les champs obligatoires sont remplis. Si un champ requis est vide, un message d'erreur en français est affiché et l'appel API n'est pas effectué. Les règles de validation sont basées sur la documentation fournisseur (`documentation/WEB-A-1 (3).md`). Cette validation côté client évite des appels API inutiles et fournit un retour immédiat à l'utilisateur.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -213,15 +218,15 @@ Tous les endpoints retournent un objet JSON avec la même structure :
|
|||||||
| `metadata` | `object` | Informations sur la requête. Contient toujours `rowcount` et `issuccess`. Peut contenir d'autres clés selon l'endpoint. |
|
| `metadata` | `object` | Informations sur la requête. Contient toujours `rowcount` et `issuccess`. Peut contenir d'autres clés selon l'endpoint. |
|
||||||
| `metadata.rowcount` | `int` | Nombre d'éléments retournés dans `data`. |
|
| `metadata.rowcount` | `int` | Nombre d'éléments retournés dans `data`. |
|
||||||
| `metadata.issuccess` | `bool` | `true` si la requête a réussi, `false` sinon. |
|
| `metadata.issuccess` | `bool` | `true` si la requête a réussi, `false` sinon. |
|
||||||
| `error` | `string` ou `null` | Message d'erreur en cas d'échec. `null` si la requête a réussi. |
|
| `error` | `string`, `array` ou `null` | Message d'erreur en cas d'échec. Peut être une chaîne, un tableau de chaînes, ou `null` si la requête a réussi. L'application normalise ce champ via `ApiErrorTranslator`. |
|
||||||
|
|
||||||
### Exemple de réponse réussie
|
### Exemple de réponse réussie
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{ "artid": "ART001", "artname": "Chaise bureau" },
|
{ "artid": "ART001", "name1": "Chaise bureau" },
|
||||||
{ "artid": "ART002", "artname": "Chaise visiteur" }
|
{ "artid": "ART002", "name1": "Chaise visiteur" }
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"rowcount": 2,
|
"rowcount": 2,
|
||||||
@@ -398,7 +403,7 @@ Les articles sont les références produits du catalogue. Ils sont stockés dans
|
|||||||
|
|
||||||
#### `art_list` -- Recherche d'articles
|
#### `art_list` -- Recherche d'articles
|
||||||
|
|
||||||
Retourne une liste d'articles correspondant aux critères de recherche. Permet de filtrer par texte libre ou par code-barres, et de sélectionner les colonnes à retourner.
|
Retourne une liste d'articles correspondant aux critères de recherche. L'API retourne un maximum fixe de **5 résultats** par appel, cette limite n'est pas configurable.
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -409,21 +414,58 @@ Retourne une liste d'articles correspondant aux critères de recherche. Permet d
|
|||||||
|
|
||||||
| Paramètre | Type | Obligatoire | Description |
|
| Paramètre | Type | Obligatoire | Description |
|
||||||
|-----------|------|:-----------:|-------------|
|
|-----------|------|:-----------:|-------------|
|
||||||
| `search` | `string` | Non | Filtre de recherche textuel. Recherche dans les champs principaux de l'article (identifiant, nom, description). Non requis si `barcode` est fourni. |
|
| `search` | `string` | Oui | Filtre de recherche textuel. Recherche dans les colonnes `artid` et `name1` de la table `art`. **Ce paramètre est obligatoire** : un appel sans `search` retourne une erreur "Search terms are required", même si `barcode` est fourni. |
|
||||||
| `select` | `string` | Non | Liste des colonnes à retourner, séparées par des virgules. Les noms de colonnes disponibles sont ceux de la table `art` (obtenables via `column_list/art`). Si omis, un jeu de colonnes par défaut est retourné. |
|
| `select` | `string` | Non | Liste des colonnes à retourner, séparées par des virgules. Les noms de colonnes disponibles sont ceux de la table `art` (obtenables via `column_list/art`). Si omis, un jeu de colonnes par défaut est retourné. |
|
||||||
| `results` | `int` | Non | Nombre maximum de résultats à retourner. Par défaut, l'API retourne un nombre réduit de résultats (environ 5 à 10). |
|
| `results` | `int` | Non | **Sans effet.** Ce paramètre est accepté par l'API mais n'influence pas le nombre de résultats retournés. L'API retourne toujours un maximum de 5 résultats, quelle que soit la valeur de `results` (testé avec 1, 3, 5, 10, 20, en int et en string). |
|
||||||
| `barcode` | `string` | Non | Code-barres de l'article à rechercher. Si fourni, le paramètre `search` n'est pas requis. |
|
| `barcode` | `string` | Non | **Sans effet observable.** Ce paramètre est accepté par l'API mais ne modifie pas les résultats. Les données retournées sont strictement identiques avec ou sans `barcode`. Le paramètre `search` reste obligatoire même si `barcode` est fourni. |
|
||||||
|
|
||||||
|
**Métadonnées retournées** :
|
||||||
|
|
||||||
|
L'endpoint `art_list` retourne des métadonnées spécifiques dans la clé `metadata` :
|
||||||
|
|
||||||
|
| Clé | Type | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `rowCount` | `int` | Nombre de résultats retournés. |
|
||||||
|
| `source` | `string` | Type de base de données (ex : `DBF`). |
|
||||||
|
| `executionTimeMs` | `int` | Temps d'exécution de la requête en millisecondes. |
|
||||||
|
| `searchColumns` | `string` | Colonnes utilisées pour la recherche (ex : `artid,name1`). |
|
||||||
|
| `selectColumns` | `string` | Colonnes demandées dans le paramètre `select`. |
|
||||||
|
| `searchTerms` | `string` | Termes de recherche utilisés. |
|
||||||
|
|
||||||
**Exemple de requête** :
|
**Exemple de requête** :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"search": "chaise",
|
"search": "chaise",
|
||||||
"select": "artid,artname,saleprice1",
|
"select": "artid,name1,saleprice"
|
||||||
"results": 20
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Exemple de réponse** :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "artid": "003R92572", "name1": "Papier A4 80g", "saleprice": 12.50 },
|
||||||
|
{ "artid": "003R92573", "name1": "Papier A4 90g", "saleprice": 14.00 },
|
||||||
|
{ "artid": "003R98703", "name1": "Papier A3 80g", "saleprice": 18.75 },
|
||||||
|
{ "artid": "003R98711", "name1": "Papier A3 90g", "saleprice": 21.00 },
|
||||||
|
{ "artid": "003R98718", "name1": "Papier A4 100g", "saleprice": 16.50 }
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"rowCount": 5,
|
||||||
|
"source": "DBF",
|
||||||
|
"executionTimeMs": 720,
|
||||||
|
"searchColumns": "artid,name1",
|
||||||
|
"selectColumns": "artid,name1,saleprice",
|
||||||
|
"searchTerms": "papier"
|
||||||
|
},
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Attention** : les noms de colonnes dans `select` doivent correspondre exactement aux noms retournes par `column_list/art` (ex : `name1`, pas `artname`). L'API ignore silencieusement les noms de colonnes invalides sans emettre d'erreur, ce qui peut donner l'impression que le parametre `select` ne fonctionne pas.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### `art_getstk` -- Stock d'un article
|
#### `art_getstk` -- Stock d'un article
|
||||||
@@ -673,15 +715,14 @@ Retourne une liste de tiers correspondant au filtre de recherche. Permet de trou
|
|||||||
|-----------|------|:-----------:|-------------|
|
|-----------|------|:-----------:|-------------|
|
||||||
| `search` | `string` | Oui | Filtre de recherche. **Ce paramètre est obligatoire** : un appel sans filtre retourne une erreur. |
|
| `search` | `string` | Oui | Filtre de recherche. **Ce paramètre est obligatoire** : un appel sans filtre retourne une erreur. |
|
||||||
| `select` | `string` | Non | Colonnes à retourner, séparées par des virgules (colonnes de la table `cust`). |
|
| `select` | `string` | Non | Colonnes à retourner, séparées par des virgules (colonnes de la table `cust`). |
|
||||||
| `results` | `int` | Non | Nombre maximum de résultats. |
|
| `results` | `int` | Non | **Sans effet.** Ce paramètre est accepté mais n'influence pas le nombre de résultats. L'API retourne toujours un maximum de 10 résultats (testé avec 3, 10, 20). |
|
||||||
|
|
||||||
**Exemple de requête** :
|
**Exemple de requête** :
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"search": "Dupont",
|
"search": "Dupont",
|
||||||
"select": "custid,custname,city",
|
"select": "custid,custname,city"
|
||||||
"results": 10
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -950,19 +991,23 @@ Tiers (cust)
|
|||||||
|
|
||||||
4. **Paramètre `search` obligatoire pour `third_list`** : l'endpoint `third_list` exige le paramètre `search`. Un appel sans ce paramètre retourne une erreur.
|
4. **Paramètre `search` obligatoire pour `third_list`** : l'endpoint `third_list` exige le paramètre `search`. Un appel sans ce paramètre retourne une erreur.
|
||||||
|
|
||||||
5. **Paramètre `results`** : limite le nombre de résultats retournés. Par défaut, l'API retourne un nombre réduit de résultats (environ 5 à 10). Augmentez cette valeur si vous avez besoin de plus de résultats.
|
5. **Paramètre `results`** : ce paramètre est documenté comme limitant le nombre de résultats, mais les tests réalisés montrent qu'il **n'a aucun effet observable** sur les endpoints `art_list` (toujours 5 résultats maximum) et `third_list` (toujours 10 résultats maximum). La limite semble être fixée côté serveur et non configurable via ce paramètre.
|
||||||
|
|
||||||
6. **Paramètre `select`** : permet de choisir les colonnes à retourner. Les noms de colonnes disponibles pour chaque table s'obtiennent via `column_list/{tablename}`. Si omis, un jeu de colonnes par défaut est retourné.
|
6. **Paramètre `TYPE` obligatoire pour `jnl_list`** : l'endpoint `jnl_list` exige le paramètre `TYPE`. Un appel sans ce paramètre retourne une erreur.
|
||||||
|
|
||||||
7. **Casse des paramètres** : la casse des noms de paramètres varie selon les endpoints. Par exemple, l'identifiant du tiers peut être `thirdid`, `Thirdid` ou `THIRDID` selon l'endpoint. Respectez la casse documentée pour chaque endpoint.
|
7. **Format du champ `error`** : le champ `error` de la réponse API peut être `null`, une chaîne ou un tableau de chaînes. L'application normalise ce champ automatiquement via `ApiErrorTranslator`.
|
||||||
|
|
||||||
8. **Fichiers attachés en base64** : lors de l'ajout ou la modification de documents, les fichiers joints doivent être encodés en base64 dans le champ `FileContentBase64`. Seul le contenu du fichier est encodé, pas le nom ni la description.
|
8. **Paramètre `select`** : permet de choisir les colonnes à retourner. Les noms de colonnes disponibles pour chaque table s'obtiennent via `column_list/{tablename}`. Si omis, un jeu de colonnes par défaut est retourné. **Attention** : les noms de colonnes doivent correspondre exactement à ceux retournés par `column_list` (ex : `name1`, pas `artname`). L'API ignore silencieusement les noms de colonnes invalides sans émettre d'erreur.
|
||||||
|
|
||||||
9. **Déduplication des colonnes** : l'endpoint `column_list` retourne chaque colonne en double. L'application effectue une déduplication automatique, mais si vous interrogez l'API directement, vous devez gérer ce doublon.
|
9. **Casse des paramètres** : la casse des noms de paramètres varie selon les endpoints. Par exemple, l'identifiant du tiers peut être `thirdid`, `Thirdid` ou `THIRDID` selon l'endpoint. Respectez la casse documentée pour chaque endpoint.
|
||||||
|
|
||||||
10. **Correspondance positionnelle des tableaux** : dans `document_add` et `document_mod`, les tableaux `Artid`, `Qty`, `Saleprice`, `Discount`, `Vatid` et `Vatpc` fonctionnent par correspondance positionnelle. Assurez-vous que tous les tableaux ont la même longueur.
|
10. **Fichiers attachés en base64** : lors de l'ajout ou la modification de documents, les fichiers joints doivent être encodés en base64 dans le champ `FileContentBase64`. Seul le contenu du fichier est encodé, pas le nom ni la description.
|
||||||
|
|
||||||
11. **Timeout** : certaines requêtes peuvent être lentes selon le volume de données. Le timeout par défaut est configuré à 300 secondes. En cas de timeout, vérifiez la configuration `LOGISTICS_API_TIMEOUT`.
|
11. **Déduplication des colonnes** : l'endpoint `column_list` retourne chaque colonne en double. L'application effectue une déduplication automatique, mais si vous interrogez l'API directement, vous devez gérer ce doublon.
|
||||||
|
|
||||||
|
12. **Correspondance positionnelle des tableaux** : dans `document_add` et `document_mod`, les tableaux `Artid`, `Qty`, `Saleprice`, `Discount`, `Vatid` et `Vatpc` fonctionnent par correspondance positionnelle. Assurez-vous que tous les tableaux ont la même longueur.
|
||||||
|
|
||||||
|
13. **Timeout** : certaines requêtes peuvent être lentes selon le volume de données. Le timeout par défaut est configuré à 300 secondes. En cas de timeout, vérifiez la configuration `LOGISTICS_API_TIMEOUT`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Memory Bank
|
# Memory Bank
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-20
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Présentation
|
## Présentation
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
# Active Context
|
# Active Context
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Travail en cours
|
## Travail en cours
|
||||||
|
|
||||||
Aucun travail en cours.
|
Aucun travail en cours.
|
||||||
|
|
||||||
|
## Changements récents (2026-02-23, session investigation art_list)
|
||||||
|
|
||||||
|
- **Investigation du paramètre `results`** : tests systématiques via appels API directs. Le paramètre `results` n'a aucun effet observable sur `art_list` (toujours 5 résultats max) ni sur `third_list` (toujours 10 résultats max). Testé avec les valeurs 1, 3, 5, 10, 20, en int et en string, et avec omission. La limite est fixée côté serveur et non configurable.
|
||||||
|
- **Investigation du paramètre `barcode`** : tests systématiques via appels API directs. Le paramètre `barcode` est accepté par l'API mais n'a aucun effet observable sur les résultats de `art_list`. Les données retournées sont strictement identiques avec ou sans `barcode`. De plus, le paramètre `search` reste obligatoire même quand `barcode` est fourni (l'API retourne "Search terms are required" sinon).
|
||||||
|
- **Métadonnées `art_list`** : l'API retourne des métadonnées spécifiques : `rowCount` (camelCase), `source`, `executionTimeMs`, `searchColumns` (toujours "artid,name1"), `selectColumns`, `searchTerms`. Structure différente de celle documentée initialement.
|
||||||
|
- **Page Articles modifiée** : propriété `$results` (int) supprimée, propriété `$barcode` (string) ajoutée. Le champ "Nombre de résultats" remplacé par "Code-barres (barcode)" dans le formulaire.
|
||||||
|
- **Documentation mise à jour** : `WEB-A-1 (3).md` et `documentation_api_logistics.md` corrigés pour refléter les comportements réels de `results` et `barcode` sur `art_list` et `third_list`. Ajout des métadonnées spécifiques et d'un exemple de réponse complet.
|
||||||
|
- **2 nouveaux tests Pest** ajoutés dans `ArticlesPageTest.php` : vérification que `barcode` est envoyé quand renseigné, et omis quand vide.
|
||||||
|
- Total : 124 tests passent, 1 test pré-existant en échec (FilamentDashboardTest).
|
||||||
|
|
||||||
## Décisions récentes
|
## Décisions récentes
|
||||||
|
|
||||||
- **Toggle Lecture/Ecriture** (2026-02-21) : Toutes les pages entité (Articles, Documents, Journaux, Tiers, Divers) disposent d'un toggle en haut de page permettant de basculer entre le mode Lecture (endpoints de récupération de données) et le mode Ecriture (endpoints d'envoi de données). Les pages sans endpoint d'écriture (Articles, Journaux, Tiers) affichent un état vide en mode écriture. TablesExplorer ne reçoit pas ce toggle (page de structure).
|
- **Toggle Lecture/Ecriture** (2026-02-21) : Toutes les pages entité (Articles, Documents, Journaux, Tiers, Divers) disposent d'un toggle en haut de page permettant de basculer entre le mode Lecture (endpoints de récupération de données) et le mode Ecriture (endpoints d'envoi de données). Les pages sans endpoint d'écriture (Articles, Journaux, Tiers) affichent un état vide en mode écriture. TablesExplorer ne reçoit pas ce toggle (page de structure).
|
||||||
@@ -67,6 +77,24 @@ Aucun travail en cours.
|
|||||||
- LogisticsService créé avec 17 endpoints.
|
- LogisticsService créé avec 17 endpoints.
|
||||||
- Migration api_request_logs créée.
|
- Migration api_request_logs créée.
|
||||||
|
|
||||||
|
## Changements récents (2026-02-23, session gestion erreurs et validation)
|
||||||
|
|
||||||
|
- Création de `app/Support/ApiErrorTranslator.php` : normalisation du champ `error` API (null/string/array), mapping de patterns d'erreurs connus vers des explications en français.
|
||||||
|
- Composant `error-banner.blade.php` : supporte désormais les sauts de ligne via `nl2br(e($message))` pour afficher les explications sur plusieurs lignes.
|
||||||
|
- Composant `json-block.blade.php` : nouvelle prop `$searched` (défaut `false`). Affiche un `empty-state` "Aucune donnée n'a été trouvée" quand `$searched` est `true` et `$data` est vide.
|
||||||
|
- Badge de comptage : remplacé `$metadata['rowcount'] ?? 0` par `count($data)` dans les 4 vues (articles, documents, journaux, tiers) pour un comptage fiable.
|
||||||
|
- Toutes les 6 pages API (Articles, Documents, Divers, Journaux, Tiers, TablesExplorer) utilisent désormais `ApiErrorTranslator::translate()` pour les messages d'erreur.
|
||||||
|
- Ajout de propriétés de tracking (`$hasSearched`, `$hasCheckedStock`, etc.) sur toutes les pages pour distinguer "jamais recherché" de "recherché sans résultat".
|
||||||
|
- Ajout de validation des champs obligatoires avant chaque appel API avec messages en français (au lieu de `return` silencieux). Règles basées sur `documentation/WEB-A-1 (3).md`.
|
||||||
|
- Validation ajoutée : `TYPE` obligatoire pour `jnl_list`, `search` obligatoire pour `third_list`, `ARTID` obligatoire pour `art_getstk`, `code` obligatoire pour `codes_list`, champs obligatoires pour tous les endpoints Documents.
|
||||||
|
- 4 nouveaux fichiers de tests : `ArticlesPageTest.php`, `JournauxPageTest.php`, `TiersPageTest.php`, `ApiErrorTranslatorTest.php`.
|
||||||
|
- Tests existants mis à jour : `DiversPageTest.php`, `DocumentsPageTest.php` (ajout tests de validation, mise à jour assertions pour les messages traduits).
|
||||||
|
- Total : 122 tests passent (302 assertions), 1 test pré-existant en échec (FilamentDashboardTest).
|
||||||
|
|
||||||
|
### 2026-02-23 (session investigation art_list)
|
||||||
|
|
||||||
|
- Voir section "Changements récents" ci-dessus.
|
||||||
|
|
||||||
## Prochaines étapes
|
## Prochaines étapes
|
||||||
|
|
||||||
- Corriger le test pré-existant `FilamentDashboardTest > it displays project statistics`.
|
- Corriger le test pré-existant `FilamentDashboardTest > it displays project statistics`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Product Context
|
# Product Context
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Pourquoi ce projet existe
|
## Pourquoi ce projet existe
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ L'API Logistics (Flex/ESI Gescom) est un système de gestion commerciale accessi
|
|||||||
- **Compréhension de la structure** : La page "Tables" permet de découvrir les tables et colonnes disponibles dans l'API, avec déduplication automatique des colonnes retournées en double par l'API.
|
- **Compréhension de la structure** : La page "Tables" permet de découvrir les tables et colonnes disponibles dans l'API, avec déduplication automatique des colonnes retournées en double par l'API.
|
||||||
- **Documentation intégrée** : La page "Documentation" affiche le markdown de la documentation API avec un rendu stylisé (typographie, tableaux, blocs de code) et propose un export PDF.
|
- **Documentation intégrée** : La page "Documentation" affiche le markdown de la documentation API avec un rendu stylisé (typographie, tableaux, blocs de code) et propose un export PDF.
|
||||||
- **Traçabilité** : Chaque requête effectuée (réussie ou échouée) est enregistrée dans `api_request_logs` pour pouvoir analyser les échanges.
|
- **Traçabilité** : Chaque requête effectuée (réussie ou échouée) est enregistrée dans `api_request_logs` pour pouvoir analyser les échanges.
|
||||||
- **Résilience** : Les erreurs de connexion sont gérées avec retry automatique et messages explicites en français.
|
- **Résilience** : Les erreurs de connexion sont gérées avec retry automatique et messages explicites en français. Les erreurs API sont traduites et enrichies d'explications via `ApiErrorTranslator`.
|
||||||
|
- **Validation** : Les champs obligatoires sont validés avant chaque appel API avec des messages en français. Les pages distinguent "jamais recherché" de "recherché sans résultat" grace aux propriétés de tracking.
|
||||||
- **Cohérence visuelle** : Un système de design unifié (composants `x-logistics.*`) garantit une présentation homogène sur toutes les pages.
|
- **Cohérence visuelle** : Un système de design unifié (composants `x-logistics.*`) garantit une présentation homogène sur toutes les pages.
|
||||||
- **Couverture complète des endpoints** : Les 19 endpoints disponibles dans le service sont tous accessibles depuis l'interface, y compris les 2 non fonctionnels (avec avertissement).
|
- **Couverture complète des endpoints** : Les 19 endpoints disponibles dans le service sont tous accessibles depuis l'interface, y compris les 2 non fonctionnels (avec avertissement).
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ L'utilisateur accède au dashboard Filament sur `http://api-logistics.test/admin
|
|||||||
|
|
||||||
1. **Documentation** : Affichage stylisé de la documentation API complète (markdown converti en HTML). Actions : télécharger en PDF, ouvrir dans un nouvel onglet.
|
1. **Documentation** : Affichage stylisé de la documentation API complète (markdown converti en HTML). Actions : télécharger en PDF, ouvrir dans un nouvel onglet.
|
||||||
2. **Tables** : Barre de statistiques (endpoint, type base, nombre de tables). Liste filtrable des tables avec compteur de colonnes. Clic sur une table pour voir ses colonnes avec badges de type colorés.
|
2. **Tables** : Barre de statistiques (endpoint, type base, nombre de tables). Liste filtrable des tables avec compteur de colonnes. Clic sur une table pour voir ses colonnes avec badges de type colorés.
|
||||||
3. **Articles** : Toggle Lecture/Ecriture. En lecture : formulaire de recherche (search, select, results) + vérification du stock d'un article par son ARTID. En écriture : état vide (aucun endpoint d'écriture disponible).
|
3. **Articles** : Toggle Lecture/Ecriture. En lecture : formulaire de recherche (search, select, barcode) + vérification du stock d'un article par son ARTID. L'API retourne un maximum fixe de 5 résultats (non configurable). Le paramètre `barcode` est présent dans le formulaire mais son effet côté API n'est pas observable. En écriture : état vide (aucun endpoint d'écriture disponible).
|
||||||
4. **Documents** : Toggle Lecture/Ecriture. En lecture : 7 formulaires (document_list, document_detail, Document_GetStatusList, Document_GetUnitPriceAndVat, Document_GetDueDate, Document_GetAttachListThumbnail, Document_GetPDF). En écriture : 2 formulaires (document_add, document_mod). Le formulaire Document_GetPDF affiche un bandeau d'avertissement (endpoint non fonctionnel).
|
4. **Documents** : Toggle Lecture/Ecriture. En lecture : 7 formulaires (document_list, document_detail, Document_GetStatusList, Document_GetUnitPriceAndVat, Document_GetDueDate, Document_GetAttachListThumbnail, Document_GetPDF). En écriture : 2 formulaires (document_add, document_mod). Le formulaire Document_GetPDF affiche un bandeau d'avertissement (endpoint non fonctionnel).
|
||||||
5. **Journaux** : Toggle Lecture/Ecriture. En lecture : recherche par type de journal (TYPE). En écriture : état vide.
|
5. **Journaux** : Toggle Lecture/Ecriture. En lecture : recherche par type de journal (TYPE). En écriture : état vide.
|
||||||
6. **Tiers** : Toggle Lecture/Ecriture. En lecture : recherche de tiers (search obligatoire) + historique des articles d'un tiers. En écriture : état vide.
|
6. **Tiers** : Toggle Lecture/Ecriture. En lecture : recherche de tiers (search obligatoire) + historique des articles d'un tiers. En écriture : état vide.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Progress
|
# Progress
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Ce qui fonctionne
|
## Ce qui fonctionne
|
||||||
|
|
||||||
@@ -35,6 +35,20 @@ Dernière mise à jour : 2026-02-21
|
|||||||
- [x] `README.md` créé
|
- [x] `README.md` créé
|
||||||
- [x] Formatage Pint validé
|
- [x] Formatage Pint validé
|
||||||
- [x] CI GitHub Actions (lint + tests)
|
- [x] CI GitHub Actions (lint + tests)
|
||||||
|
- [x] `ApiErrorTranslator` créé (`app/Support/ApiErrorTranslator.php`) : normalisation et traduction des erreurs API
|
||||||
|
- [x] Composant `error-banner` amélioré (supporte `nl2br` pour les explications)
|
||||||
|
- [x] Composant `json-block` amélioré (prop `$searched` + état vide)
|
||||||
|
- [x] Badge de comptage corrigé (`count($data)` au lieu de `$metadata['rowcount']`)
|
||||||
|
- [x] Validation des champs obligatoires sur toutes les pages API (messages en français)
|
||||||
|
- [x] Propriétés de tracking (`$hasSearched`, etc.) sur toutes les pages
|
||||||
|
- [x] `ApiErrorTranslator` intégré dans toutes les pages API (Articles, Documents, Divers, Journaux, Tiers, TablesExplorer)
|
||||||
|
- [x] 4 nouveaux fichiers de tests : ArticlesPageTest, JournauxPageTest, TiersPageTest, ApiErrorTranslatorTest
|
||||||
|
- [x] Tests existants mis à jour (DiversPageTest, DocumentsPageTest) avec validation et tracking
|
||||||
|
- [x] Investigation paramètre `results` : sans effet sur `art_list` (5 max) et `third_list` (10 max), limite fixe côté serveur
|
||||||
|
- [x] Investigation paramètre `barcode` : sans effet observable sur `art_list`, `search` reste obligatoire
|
||||||
|
- [x] Page Articles : `$results` supprimé, `$barcode` ajouté, formulaire mis à jour
|
||||||
|
- [x] Documentation `art_list` et `third_list` corrigée (métadonnées réelles, paramètres inefficaces documentés)
|
||||||
|
- [x] 2 nouveaux tests barcode dans ArticlesPageTest (envoi quand renseigné, omission quand vide)
|
||||||
|
|
||||||
## Ce qui reste à faire
|
## Ce qui reste à faire
|
||||||
|
|
||||||
@@ -48,13 +62,16 @@ Dernière mise à jour : 2026-02-21
|
|||||||
- Le test `FilamentDashboardTest > it displays project statistics` échoue car le dashboard ne contient pas la section "Endpoints API" / "Tables accessibles" / "Pages Filament" / "Tests Pest". Le test a été créé avant la refonte du dashboard.
|
- Le test `FilamentDashboardTest > it displays project statistics` échoue car le dashboard ne contient pas la section "Endpoints API" / "Tables accessibles" / "Pages Filament" / "Tests Pest". Le test a été créé avant la refonte du dashboard.
|
||||||
- L'erreur `SQLSTATE[HY000] [1049] Unknown database` peut apparaître lors de `composer update` si la base n'est pas encore créée (script `boost:update`). Sans impact une fois la base créée.
|
- L'erreur `SQLSTATE[HY000] [1049] Unknown database` peut apparaître lors de `composer update` si la base n'est pas encore créée (script `boost:update`). Sans impact une fois la base créée.
|
||||||
- L'API retourne chaque colonne en double dans `column_list`. Le `TablesExplorer` déduplique côté client.
|
- L'API retourne chaque colonne en double dans `column_list`. Le `TablesExplorer` déduplique côté client.
|
||||||
|
- Le paramètre `results` n'a aucun effet sur `art_list` (toujours 5 max) ni sur `third_list` (toujours 10 max). Limite fixe côté serveur.
|
||||||
|
- Le paramètre `barcode` n'a aucun effet observable sur `art_list`. Le paramètre `search` reste obligatoire même avec `barcode`.
|
||||||
|
|
||||||
## Métriques
|
## Métriques
|
||||||
|
|
||||||
- Tests : 84 passent, 1 en échec pré-existant (205 assertions)
|
- Tests : 124 passent, 1 en échec pré-existant
|
||||||
- Pages Filament : 7 (Documentation, TablesExplorer, Articles, Documents, Journaux, Tiers, Divers)
|
- Pages Filament : 7 (Documentation, TablesExplorer, Articles, Documents, Journaux, Tiers, Divers)
|
||||||
- Composants Blade design system : 10
|
- Composants Blade design system : 10
|
||||||
- Endpoints API couverts par LogisticsService : 19
|
- Endpoints API couverts par LogisticsService : 19
|
||||||
- Endpoints accessibles depuis l'interface : 19 (dont 2 non fonctionnels)
|
- Endpoints accessibles depuis l'interface : 19 (dont 2 non fonctionnels)
|
||||||
- Migrations : 5 (users, cache, jobs, two_factor, api_request_logs)
|
- Migrations : 5 (users, cache, jobs, two_factor, api_request_logs)
|
||||||
- Règles Cursor : 4 (laravel-boost, memory-bank, design-system, update-documentation)
|
- Règles Cursor : 4 (laravel-boost, memory-bank, design-system, update-documentation)
|
||||||
|
- Classes support : 1 (ApiErrorTranslator)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Project Brief
|
# Project Brief
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
@@ -22,7 +22,9 @@ Application Laravel de test dont l'objectif est de comprendre le fonctionnement
|
|||||||
- Affichage des résultats sous forme de tableaux structurés et de blocs JSON formatés.
|
- Affichage des résultats sous forme de tableaux structurés et de blocs JSON formatés.
|
||||||
- Système de design unifié avec composants Blade réutilisables (`x-logistics.*`).
|
- Système de design unifié avec composants Blade réutilisables (`x-logistics.*`).
|
||||||
- Traçage des requêtes effectuées dans une table `api_request_logs`.
|
- Traçage des requêtes effectuées dans une table `api_request_logs`.
|
||||||
- Gestion robuste des erreurs API (timeout, retry, messages utilisateur en français).
|
- Gestion robuste des erreurs API (timeout, retry, messages utilisateur en français, traduction avec explications via `ApiErrorTranslator`).
|
||||||
|
- Validation des champs obligatoires avant chaque appel API avec messages en français.
|
||||||
|
- Distinction "jamais recherché" / "recherché sans résultat" via propriétés de tracking.
|
||||||
- Avertissements visuels pour les endpoints non fonctionnels (Document_GetPDF, custom_geninv_updatestock).
|
- Avertissements visuels pour les endpoints non fonctionnels (Document_GetPDF, custom_geninv_updatestock).
|
||||||
|
|
||||||
## Contraintes
|
## Contraintes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# System Patterns
|
# System Patterns
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Architecture applicative
|
## Architecture applicative
|
||||||
|
|
||||||
@@ -70,6 +70,23 @@ Le toggle est implémenté avec deux boutons et un `wire:click="$set('mode', 're
|
|||||||
|
|
||||||
La page Documents utilise une méthode privée `splitCsv(string $value): array` qui convertit les champs de formulaire (texte séparé par virgules) en tableaux PHP pour les paramètres d'API qui attendent des arrays (Artid, Qty, Saleprice, Discount, Vatid, Vatpc). Exemple : `"ART001,ART002"` devient `["ART001", "ART002"]`.
|
La page Documents utilise une méthode privée `splitCsv(string $value): array` qui convertit les champs de formulaire (texte séparé par virgules) en tableaux PHP pour les paramètres d'API qui attendent des arrays (Artid, Qty, Saleprice, Discount, Vatid, Vatpc). Exemple : `"ART001,ART002"` devient `["ART001", "ART002"]`.
|
||||||
|
|
||||||
|
### Traduction et normalisation des erreurs API
|
||||||
|
|
||||||
|
`App\Support\ApiErrorTranslator` centralise le traitement des erreurs retournées par l'API :
|
||||||
|
|
||||||
|
- **Normalisation** : Le champ `error` de l'API peut être `null`, une chaîne ou un tableau de chaînes. La méthode `normalize()` convertit tout en chaîne lisible.
|
||||||
|
- **Traduction** : La méthode `translate()` ajoute une explication en français aux messages d'erreur connus (ex: "Search terms are required" -> "Le champ de recherche est obligatoire.").
|
||||||
|
- **Format de sortie** : Quand une explication est trouvée, le message est formaté sur plusieurs lignes : `{message original}\n\nExplication : {explication}`.
|
||||||
|
|
||||||
|
### Validation et tracking des appels API
|
||||||
|
|
||||||
|
Chaque page Filament implémente :
|
||||||
|
|
||||||
|
- **Validation** : Les champs obligatoires sont vérifiés avant chaque appel API. Si un champ requis est vide, un message d'erreur en français est affiché via `$this->errorMessage` et la méthode retourne sans appeler l'API. Les règles de validation sont basées sur la documentation fournisseur (`documentation/WEB-A-1 (3).md`).
|
||||||
|
- **Tracking** : Des propriétés booléennes publiques (`$hasSearched`, `$hasCheckedStock`, etc.) sont mises à `true` après chaque appel API (succès ou erreur). Cela permet de distinguer "jamais recherché" de "recherché sans résultat" dans les vues Blade.
|
||||||
|
- **État vide** : Le composant `json-block` accepte une prop `$searched`. Quand `$searched` est `true` et `$data` est vide, un état vide est affiché. Pour les `data-table`, les vues vérifient `$hasSearched && count($data) === 0`.
|
||||||
|
- **Badge de comptage** : Les badges de résultats utilisent `count($data)` (comptage réel PHP) au lieu de `$metadata['rowcount']` (retourné par l'API, parfois incorrect).
|
||||||
|
|
||||||
### Endpoints non fonctionnels
|
### Endpoints non fonctionnels
|
||||||
|
|
||||||
Certains endpoints (Document_GetPDF, custom_geninv_updatestock) sont présents dans l'interface avec un bandeau d'avertissement ambre expliquant pourquoi ils ne fonctionnent pas. Les méthodes service existent dans LogisticsService pour permettre le test.
|
Certains endpoints (Document_GetPDF, custom_geninv_updatestock) sont présents dans l'interface avec un bandeau d'avertissement ambre expliquant pourquoi ils ne fonctionnent pas. Les méthodes service existent dans LogisticsService pour permettre le test.
|
||||||
@@ -146,6 +163,8 @@ app/
|
|||||||
AppServiceProvider.php # Config globale (CarbonImmutable, DB safety)
|
AppServiceProvider.php # Config globale (CarbonImmutable, DB safety)
|
||||||
Services/
|
Services/
|
||||||
LogisticsService.php # Service centralisé API Logistics (19 méthodes)
|
LogisticsService.php # Service centralisé API Logistics (19 méthodes)
|
||||||
|
Support/
|
||||||
|
ApiErrorTranslator.php # Normalisation et traduction des erreurs API
|
||||||
|
|
||||||
config/
|
config/
|
||||||
logistics.php # Configuration API Logistics (URL, clé, timeout, retry)
|
logistics.php # Configuration API Logistics (URL, clé, timeout, retry)
|
||||||
@@ -174,14 +193,19 @@ database/
|
|||||||
...create_api_request_logs_table.php
|
...create_api_request_logs_table.php
|
||||||
|
|
||||||
tests/Feature/
|
tests/Feature/
|
||||||
|
ArticlesPageTest.php # 8 tests page Articles (toggle, validation, tracking, erreurs)
|
||||||
DocumentationTest.php # 5 tests page Documentation (Livewire + PDF)
|
DocumentationTest.php # 5 tests page Documentation (Livewire + PDF)
|
||||||
DocumentsPageTest.php # 13 tests page Documents (toggle, 9 endpoints, erreurs)
|
DocumentsPageTest.php # 21 tests page Documents (toggle, 9 endpoints, validation, erreurs)
|
||||||
DiversPageTest.php # 8 tests page Divers (toggle, 3 endpoints, erreurs)
|
DiversPageTest.php # 8 tests page Divers (toggle, 3 endpoints, validation, erreurs)
|
||||||
|
JournauxPageTest.php # 6 tests page Journaux (toggle, validation, tracking, erreurs)
|
||||||
LogisticsServiceTest.php # 14 tests service API (mocks HTTP)
|
LogisticsServiceTest.php # 14 tests service API (mocks HTTP)
|
||||||
TablesExplorerTest.php # 6 tests page TablesExplorer (Livewire)
|
TablesExplorerTest.php # 6 tests page TablesExplorer (Livewire)
|
||||||
|
TiersPageTest.php # 8 tests page Tiers (toggle, validation, tracking, erreurs)
|
||||||
FilamentDashboardTest.php # Tests dashboard Filament (1 test en échec pré-existant)
|
FilamentDashboardTest.php # Tests dashboard Filament (1 test en échec pré-existant)
|
||||||
DashboardTest.php # Tests dashboard
|
DashboardTest.php # Tests dashboard
|
||||||
ExampleTest.php # Test d'exemple Laravel
|
ExampleTest.php # Test d'exemple Laravel
|
||||||
|
tests/Unit/
|
||||||
|
ApiErrorTranslatorTest.php # 9 tests normalisation et traduction des erreurs API
|
||||||
|
|
||||||
routes/
|
routes/
|
||||||
web.php # Routes web (home, dashboard, documentation PDF)
|
web.php # Routes web (home, dashboard, documentation PDF)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Tech Context
|
# Tech Context
|
||||||
|
|
||||||
Dernière mise à jour : 2026-02-21
|
Dernière mise à jour : 2026-02-23
|
||||||
|
|
||||||
## Stack technique
|
## Stack technique
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ Réponse `column_list` : chaque colonne a `name`, `dataType` (C/N/T/D/L/M), `len
|
|||||||
|----------|----------------|------|-------------|-----------------------|
|
|----------|----------------|------|-------------|-----------------------|
|
||||||
| `tables_list` | `tablesList()` | Lecture | Liste des tables | - |
|
| `tables_list` | `tablesList()` | Lecture | Liste des tables | - |
|
||||||
| `column_list/{table}` | `columnList(string)` | Lecture | Colonnes d'une table | table (URL) |
|
| `column_list/{table}` | `columnList(string)` | Lecture | Colonnes d'une table | table (URL) |
|
||||||
| `art_list` | `artList(array)` | Lecture | Liste d'articles | select, results, search, barcode |
|
| `art_list` | `artList(array)` | Lecture | Liste d'articles (max 5 résultats, limite fixe serveur) | select, search, barcode (results sans effet) |
|
||||||
| `art_getstk` | `artGetStock(string)` | Lecture | Stock d'un article | ARTID |
|
| `art_getstk` | `artGetStock(string)` | Lecture | Stock d'un article | ARTID |
|
||||||
| `jnl_list` | `jnlList(array)` | Lecture | Liste des journaux | select, results, TYPE |
|
| `jnl_list` | `jnlList(array)` | Lecture | Liste des journaux | select, results, TYPE |
|
||||||
| `document_list` | `documentList(array)` | Lecture | Liste des documents | select, thirdid |
|
| `document_list` | `documentList(array)` | Lecture | Liste des documents | select, thirdid |
|
||||||
@@ -108,7 +108,7 @@ Réponse `column_list` : chaque colonne a `name`, `dataType` (C/N/T/D/L/M), `len
|
|||||||
| `Document_GetDueDate` | `documentGetDueDate(string, string)` | Lecture | Échéance | paydelay, date |
|
| `Document_GetDueDate` | `documentGetDueDate(string, string)` | Lecture | Échéance | paydelay, date |
|
||||||
| `Document_GetAttachListThumbnail` | `documentGetAttachListThumbnail(string, string)` | Lecture | Miniatures annexes | JNL, NUMBER |
|
| `Document_GetAttachListThumbnail` | `documentGetAttachListThumbnail(string, string)` | Lecture | Miniatures annexes | JNL, NUMBER |
|
||||||
| `Document_GetPDF` | `documentGetPdf(string, string, string)` | Lecture | Génération PDF | JNL, NUMBER, LAYOUT |
|
| `Document_GetPDF` | `documentGetPdf(string, string, string)` | Lecture | Génération PDF | JNL, NUMBER, LAYOUT |
|
||||||
| `third_list` | `thirdList(array)` | Lecture | Liste des tiers | select, results, search |
|
| `third_list` | `thirdList(array)` | Lecture | Liste des tiers (max 10 résultats, limite fixe serveur) | select, search (results sans effet) |
|
||||||
| `third_GetArtHistory` | `thirdGetArtHistory(string)` | Lecture | Historique articles tiers | thirdid |
|
| `third_GetArtHistory` | `thirdGetArtHistory(string)` | Lecture | Historique articles tiers | thirdid |
|
||||||
| `getserialnumber` | `getSerialNumber()` | Lecture | Numéro de série | - |
|
| `getserialnumber` | `getSerialNumber()` | Lecture | Numéro de série | - |
|
||||||
| `codes_list` | `codesList(array)` | Lecture | Données par code | code |
|
| `codes_list` | `codesList(array)` | Lecture | Données par code | code |
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
@if ($message)
|
@if ($message)
|
||||||
<div class="rounded-lg bg-danger-50 p-4 text-sm text-danger-600 dark:bg-danger-400/10 dark:text-danger-400">
|
<div class="rounded-lg bg-danger-50 p-4 text-sm text-danger-600 dark:bg-danger-400/10 dark:text-danger-400">
|
||||||
{{ $message }}
|
{!! nl2br(e($message)) !!}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
@props(['data'])
|
@props(['data', 'searched' => false])
|
||||||
|
|
||||||
@if (! empty($data))
|
@if (! empty($data))
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<pre class="rounded-lg border border-gray-200 bg-gray-50 p-4 text-xs font-mono leading-relaxed text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300">{{ json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
<pre class="rounded-lg border border-gray-200 bg-gray-50 p-4 text-xs font-mono leading-relaxed text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300">{{ json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
@elseif ($searched)
|
||||||
|
<x-logistics.empty-state
|
||||||
|
icon="heroicon-o-magnifying-glass"
|
||||||
|
title="Aucune donnée n'a été trouvée pour votre demande."
|
||||||
|
/>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -27,15 +27,13 @@
|
|||||||
wire:model="select"
|
wire:model="select"
|
||||||
label="Colonnes (select)"
|
label="Colonnes (select)"
|
||||||
id="select"
|
id="select"
|
||||||
placeholder="artid,artname"
|
placeholder="artid,name1"
|
||||||
/>
|
/>
|
||||||
<x-logistics.form-field
|
<x-logistics.form-field
|
||||||
wire:model="results"
|
wire:model="barcode"
|
||||||
label="Nombre de résultats (results)"
|
label="Code-barres (barcode)"
|
||||||
id="results"
|
id="barcode"
|
||||||
type="number"
|
placeholder="Ex: 5411068700323"
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,17 +53,25 @@
|
|||||||
<x-logistics.card>
|
<x-logistics.card>
|
||||||
<x-logistics.section-header title="Résultats art_list">
|
<x-logistics.section-header title="Résultats art_list">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
@if ($metadata)
|
|
||||||
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
||||||
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
|
{{ count($data) }} résultat(s)
|
||||||
</span>
|
</span>
|
||||||
@endif
|
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
</x-logistics.section-header>
|
</x-logistics.section-header>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<x-logistics.data-table :data="$data" />
|
<x-logistics.data-table :data="$data" />
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
@elseif ($hasSearched && ! $errorMessage)
|
||||||
|
<x-logistics.card>
|
||||||
|
<x-logistics.section-header title="Résultats art_list" />
|
||||||
|
<div class="p-6">
|
||||||
|
<x-logistics.empty-state
|
||||||
|
icon="heroicon-o-magnifying-glass"
|
||||||
|
title="Aucune donnée n'a été trouvée pour votre demande."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</x-logistics.card>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- art_getstk --}}
|
{{-- art_getstk --}}
|
||||||
@@ -92,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getStock" class="mt-4">
|
<div wire:loading.remove wire:target="getStock" class="mt-4">
|
||||||
<x-logistics.json-block :data="$stockData" />
|
<x-logistics.json-block :data="$stockData" :searched="$hasCheckedStock" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getSerialNumber" class="mt-4">
|
<div wire:loading.remove wire:target="getSerialNumber" class="mt-4">
|
||||||
<x-logistics.json-block :data="$serialData" />
|
<x-logistics.json-block :data="$serialData" :searched="$hasSerial" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="searchCodes" class="mt-4">
|
<div wire:loading.remove wire:target="searchCodes" class="mt-4">
|
||||||
<x-logistics.json-block :data="$codesData" />
|
<x-logistics.json-block :data="$codesData" :searched="$hasCodes" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="updateStock" class="mt-4">
|
<div wire:loading.remove wire:target="updateStock" class="mt-4">
|
||||||
<x-logistics.json-block :data="$updateStockResult" />
|
<x-logistics.json-block :data="$updateStockResult" :searched="$hasUpdatedStock" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
|||||||
@@ -51,17 +51,25 @@
|
|||||||
<x-logistics.card>
|
<x-logistics.card>
|
||||||
<x-logistics.section-header title="Résultats document_list">
|
<x-logistics.section-header title="Résultats document_list">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
@if ($metadata)
|
|
||||||
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
||||||
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
|
{{ count($data) }} résultat(s)
|
||||||
</span>
|
</span>
|
||||||
@endif
|
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
</x-logistics.section-header>
|
</x-logistics.section-header>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<x-logistics.data-table :data="$data" />
|
<x-logistics.data-table :data="$data" />
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
@elseif ($hasSearchedDocs && ! $errorMessage)
|
||||||
|
<x-logistics.card>
|
||||||
|
<x-logistics.section-header title="Résultats document_list" />
|
||||||
|
<div class="p-6">
|
||||||
|
<x-logistics.empty-state
|
||||||
|
icon="heroicon-o-magnifying-glass"
|
||||||
|
title="Aucune donnée n'a été trouvée pour votre demande."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</x-logistics.card>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- document_detail --}}
|
{{-- document_detail --}}
|
||||||
@@ -94,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getDocumentDetail" class="mt-4">
|
<div wire:loading.remove wire:target="getDocumentDetail" class="mt-4">
|
||||||
<x-logistics.json-block :data="$detailData" />
|
<x-logistics.json-block :data="$detailData" :searched="$hasDetail" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -123,7 +131,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getStatusList" class="mt-4">
|
<div wire:loading.remove wire:target="getStatusList" class="mt-4">
|
||||||
<x-logistics.json-block :data="$statusData" />
|
<x-logistics.json-block :data="$statusData" :searched="$hasStatus" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -176,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getUnitPriceAndVat" class="mt-4">
|
<div wire:loading.remove wire:target="getUnitPriceAndVat" class="mt-4">
|
||||||
<x-logistics.json-block :data="$priceData" />
|
<x-logistics.json-block :data="$priceData" :searched="$hasPrice" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -211,7 +219,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getDueDate" class="mt-4">
|
<div wire:loading.remove wire:target="getDueDate" class="mt-4">
|
||||||
<x-logistics.json-block :data="$dueDateData" />
|
<x-logistics.json-block :data="$dueDateData" :searched="$hasDueDate" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -246,7 +254,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getAttachListThumbnail" class="mt-4">
|
<div wire:loading.remove wire:target="getAttachListThumbnail" class="mt-4">
|
||||||
<x-logistics.json-block :data="$attachData" />
|
<x-logistics.json-block :data="$attachData" :searched="$hasAttach" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -290,7 +298,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getPdf" class="mt-4">
|
<div wire:loading.remove wire:target="getPdf" class="mt-4">
|
||||||
<x-logistics.json-block :data="$pdfData" />
|
<x-logistics.json-block :data="$pdfData" :searched="$hasPdf" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -371,7 +379,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="addDocument" class="mt-4">
|
<div wire:loading.remove wire:target="addDocument" class="mt-4">
|
||||||
<x-logistics.json-block :data="$addResult" />
|
<x-logistics.json-block :data="$addResult" :searched="$hasAdded" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
@@ -430,7 +438,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="modDocument" class="mt-4">
|
<div wire:loading.remove wire:target="modDocument" class="mt-4">
|
||||||
<x-logistics.json-block :data="$modResult" />
|
<x-logistics.json-block :data="$modResult" :searched="$hasModified" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
<x-logistics.form-field
|
<x-logistics.form-field
|
||||||
wire:model="type"
|
wire:model="type"
|
||||||
label="Type de journal (TYPE)"
|
label="Type de journal (TYPE, obligatoire)"
|
||||||
id="type"
|
id="type"
|
||||||
placeholder="Ex: V"
|
placeholder="Ex: V"
|
||||||
/>
|
/>
|
||||||
@@ -55,17 +55,25 @@
|
|||||||
<x-logistics.card>
|
<x-logistics.card>
|
||||||
<x-logistics.section-header title="Résultats jnl_list">
|
<x-logistics.section-header title="Résultats jnl_list">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
@if ($metadata)
|
|
||||||
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
||||||
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
|
{{ count($data) }} résultat(s)
|
||||||
</span>
|
</span>
|
||||||
@endif
|
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
</x-logistics.section-header>
|
</x-logistics.section-header>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<x-logistics.data-table :data="$data" />
|
<x-logistics.data-table :data="$data" />
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
@elseif ($hasSearched && ! $errorMessage)
|
||||||
|
<x-logistics.card>
|
||||||
|
<x-logistics.section-header title="Résultats jnl_list" />
|
||||||
|
<div class="p-6">
|
||||||
|
<x-logistics.empty-state
|
||||||
|
icon="heroicon-o-magnifying-glass"
|
||||||
|
title="Aucune donnée n'a été trouvée pour votre demande."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</x-logistics.card>
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<x-logistics.card>
|
<x-logistics.card>
|
||||||
|
|||||||
@@ -55,17 +55,25 @@
|
|||||||
<x-logistics.card>
|
<x-logistics.card>
|
||||||
<x-logistics.section-header title="Résultats third_list">
|
<x-logistics.section-header title="Résultats third_list">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
@if ($metadata)
|
|
||||||
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium tabular-nums text-gray-600 dark:bg-white/10 dark:text-gray-300">
|
||||||
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
|
{{ count($data) }} résultat(s)
|
||||||
</span>
|
</span>
|
||||||
@endif
|
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
</x-logistics.section-header>
|
</x-logistics.section-header>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<x-logistics.data-table :data="$data" />
|
<x-logistics.data-table :data="$data" />
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
@elseif ($hasSearched && ! $errorMessage)
|
||||||
|
<x-logistics.card>
|
||||||
|
<x-logistics.section-header title="Résultats third_list" />
|
||||||
|
<div class="p-6">
|
||||||
|
<x-logistics.empty-state
|
||||||
|
icon="heroicon-o-magnifying-glass"
|
||||||
|
title="Aucune donnée n'a été trouvée pour votre demande."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</x-logistics.card>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- third_GetArtHistory --}}
|
{{-- third_GetArtHistory --}}
|
||||||
@@ -92,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div wire:loading.remove wire:target="getArtHistory" class="mt-4">
|
<div wire:loading.remove wire:target="getArtHistory" class="mt-4">
|
||||||
<x-logistics.json-block :data="$historyData" />
|
<x-logistics.json-block :data="$historyData" :searched="$hasHistory" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-logistics.card>
|
</x-logistics.card>
|
||||||
|
|||||||
171
tests/Feature/ArticlesPageTest.php
Normal file
171
tests/Feature/ArticlesPageTest.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Pages\Articles;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config([
|
||||||
|
'logistics.base_url' => 'http://test-server.local',
|
||||||
|
'logistics.api_key' => 'test-api-key',
|
||||||
|
'logistics.folder' => 'testfolder',
|
||||||
|
'logistics.timeout' => 30,
|
||||||
|
'logistics.connect_timeout' => 10,
|
||||||
|
'logistics.retry.times' => 1,
|
||||||
|
'logistics.retry.sleep_ms' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to read mode', function () {
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->assertSet('mode', 'read');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can switch between read and write modes', function () {
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->set('mode', 'write')
|
||||||
|
->assertSet('mode', 'write')
|
||||||
|
->set('mode', 'read')
|
||||||
|
->assertSet('mode', 'read');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('searches articles via art_list', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_list' => Http::response([
|
||||||
|
'data' => [['ARTID' => 'ART001', 'NAME1' => 'Test Article']],
|
||||||
|
'metadata' => ['rowcount' => 1, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->set('search', 'test')
|
||||||
|
->set('select', 'artid,name1')
|
||||||
|
->call('searchArticles')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', [['ARTID' => 'ART001', 'NAME1' => 'Test Article']])
|
||||||
|
->assertSet('errorMessage', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends barcode parameter when provided', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_list' => Http::response([
|
||||||
|
'data' => [['ARTID' => 'ART001']],
|
||||||
|
'metadata' => ['rowcount' => 1, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->set('search', 'test')
|
||||||
|
->set('barcode', '5411068700323')
|
||||||
|
->call('searchArticles')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', [['ARTID' => 'ART001']]);
|
||||||
|
|
||||||
|
Http::assertSent(function (\Illuminate\Http\Client\Request $request) {
|
||||||
|
$body = json_decode($request->body(), true);
|
||||||
|
|
||||||
|
return $body['barcode'] === '5411068700323'
|
||||||
|
&& $body['search'] === 'test';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not send barcode parameter when empty', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_list' => Http::response([
|
||||||
|
'data' => [],
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->set('search', 'test')
|
||||||
|
->set('barcode', '')
|
||||||
|
->call('searchArticles');
|
||||||
|
|
||||||
|
Http::assertSent(function (\Illuminate\Http\Client\Request $request) {
|
||||||
|
$body = json_decode($request->body(), true);
|
||||||
|
|
||||||
|
return ! array_key_exists('barcode', $body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets hasSearched even when no results', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_list' => Http::response([
|
||||||
|
'data' => [],
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->set('search', 'nonexistent')
|
||||||
|
->call('searchArticles')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets stock for an article', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_getstk' => Http::response([
|
||||||
|
'data' => ['stock' => 42],
|
||||||
|
'metadata' => ['rowcount' => 1, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->set('stockArticleId', 'ART001')
|
||||||
|
->call('getStock')
|
||||||
|
->assertSet('hasCheckedStock', true)
|
||||||
|
->assertSet('stockData', ['stock' => 42])
|
||||||
|
->assertSet('errorMessage', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when stockArticleId is empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->call('getStock')
|
||||||
|
->assertSet('hasCheckedStock', false)
|
||||||
|
->assertSet('errorMessage', 'Le champ identifiant article (ARTID) est obligatoire pour verifier le stock.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('translates API error with explanation', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_list' => Http::response([
|
||||||
|
'data' => null,
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => false],
|
||||||
|
'error' => ['Search terms are required. Please provide search criteria.'],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->call('searchArticles')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSee('Explication');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error message on API failure', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/art_list' => Http::response([
|
||||||
|
'data' => null,
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => false],
|
||||||
|
'error' => 'Invalid API key',
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Articles::class)
|
||||||
|
->call('searchArticles')
|
||||||
|
->assertSet('hasSearched', true);
|
||||||
|
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
});
|
||||||
@@ -43,6 +43,7 @@ it('gets the serial number', function () {
|
|||||||
|
|
||||||
Livewire::test(Divers::class)
|
Livewire::test(Divers::class)
|
||||||
->call('getSerialNumber')
|
->call('getSerialNumber')
|
||||||
|
->assertSet('hasSerial', true)
|
||||||
->assertSet('serialData', ['value' => 'SN-12345']);
|
->assertSet('serialData', ['value' => 'SN-12345']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ it('searches codes by prefix', function () {
|
|||||||
Livewire::test(Divers::class)
|
Livewire::test(Divers::class)
|
||||||
->set('code', 'PAY')
|
->set('code', 'PAY')
|
||||||
->call('searchCodes')
|
->call('searchCodes')
|
||||||
|
->assertSet('hasCodes', true)
|
||||||
->assertSet('codesData', [['code' => 'PAY01', 'vala1' => 'Comptant']]);
|
->assertSet('codesData', [['code' => 'PAY01', 'vala1' => 'Comptant']]);
|
||||||
|
|
||||||
Http::assertSent(function ($request) {
|
Http::assertSent(function ($request) {
|
||||||
@@ -66,12 +68,13 @@ it('searches codes by prefix', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call searchCodes when code is empty', function () {
|
it('shows validation error when code is empty', function () {
|
||||||
Http::fake();
|
Http::fake();
|
||||||
|
|
||||||
Livewire::test(Divers::class)
|
Livewire::test(Divers::class)
|
||||||
->call('searchCodes')
|
->call('searchCodes')
|
||||||
->assertSet('codesData', []);
|
->assertSet('hasCodes', false)
|
||||||
|
->assertSet('errorMessage', 'Le champ debut de code (code) est obligatoire.');
|
||||||
|
|
||||||
Http::assertNothingSent();
|
Http::assertNothingSent();
|
||||||
});
|
});
|
||||||
@@ -91,7 +94,9 @@ it('calls custom_geninv_updatestock endpoint', function () {
|
|||||||
->set('stkId', 'STK1')
|
->set('stkId', 'STK1')
|
||||||
->set('stkQty', '10')
|
->set('stkQty', '10')
|
||||||
->call('updateStock')
|
->call('updateStock')
|
||||||
->assertSet('errorMessage', 'Unknown STKID');
|
->assertSet('hasUpdatedStock', true);
|
||||||
|
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
|
||||||
Http::assertSent(function ($request) {
|
Http::assertSent(function ($request) {
|
||||||
$body = $request->data();
|
$body = $request->data();
|
||||||
@@ -102,13 +107,14 @@ it('calls custom_geninv_updatestock endpoint', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call updateStock when required fields are empty', function () {
|
it('shows validation error when updateStock required fields are empty', function () {
|
||||||
Http::fake();
|
Http::fake();
|
||||||
|
|
||||||
Livewire::test(Divers::class)
|
Livewire::test(Divers::class)
|
||||||
->set('mode', 'write')
|
->set('mode', 'write')
|
||||||
->call('updateStock')
|
->call('updateStock')
|
||||||
->assertSet('updateStockResult', []);
|
->assertSet('hasUpdatedStock', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs ARTID, STKID et QTY sont obligatoires.');
|
||||||
|
|
||||||
Http::assertNothingSent();
|
Http::assertNothingSent();
|
||||||
});
|
});
|
||||||
@@ -122,8 +128,9 @@ it('displays error message on API failure', function () {
|
|||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(Divers::class)
|
$component = Livewire::test(Divers::class)
|
||||||
->set('code', 'TEST')
|
->set('code', 'TEST')
|
||||||
->call('searchCodes')
|
->call('searchCodes');
|
||||||
->assertSet('errorMessage', 'Invalid API key');
|
|
||||||
|
expect($component->get('errorMessage'))->toContain('Invalid API key');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ it('searches documents via document_list', function () {
|
|||||||
->set('select', 'jnl,number')
|
->set('select', 'jnl,number')
|
||||||
->set('thirdId', 'CUST001')
|
->set('thirdId', 'CUST001')
|
||||||
->call('searchDocuments')
|
->call('searchDocuments')
|
||||||
|
->assertSet('hasSearchedDocs', true)
|
||||||
->assertSet('data', [['jnl' => 'VEN', 'number' => '1']]);
|
->assertSet('data', [['jnl' => 'VEN', 'number' => '1']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,12 +144,13 @@ it('calls Document_GetPDF endpoint', function () {
|
|||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(Documents::class)
|
$component = Livewire::test(Documents::class)
|
||||||
->set('pdfJnl', 'VEN')
|
->set('pdfJnl', 'VEN')
|
||||||
->set('pdfNumber', '2026/0001')
|
->set('pdfNumber', '2026/0001')
|
||||||
->set('pdfLayout', 'DEFAULT')
|
->set('pdfLayout', 'DEFAULT')
|
||||||
->call('getPdf')
|
->call('getPdf');
|
||||||
->assertSet('errorMessage', 'Layout not found');
|
|
||||||
|
expect($component->get('errorMessage'))->toContain('Layout not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a document via document_add', function () {
|
it('adds a document via document_add', function () {
|
||||||
@@ -209,12 +211,92 @@ it('modifies a document via document_mod', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call getStatusList when statusJnl is empty', function () {
|
it('shows validation error when statusJnl is empty', function () {
|
||||||
Http::fake();
|
Http::fake();
|
||||||
|
|
||||||
Livewire::test(Documents::class)
|
Livewire::test(Documents::class)
|
||||||
->call('getStatusList')
|
->call('getStatusList')
|
||||||
->assertSet('statusData', []);
|
->assertSet('hasStatus', false)
|
||||||
|
->assertSet('errorMessage', 'Le champ code journal (jnl) est obligatoire.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when document_detail fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->call('getDocumentDetail')
|
||||||
|
->assertSet('hasDetail', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs code journal (jnl) et numero de document (number) sont obligatoires.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when getUnitPriceAndVat fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->call('getUnitPriceAndVat')
|
||||||
|
->assertSet('hasPrice', false)
|
||||||
|
->assertSet('errorMessage', 'Tous les champs sont obligatoires : ARTID, QTY (format string), JNL, THIRDID et DATE.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when getDueDate fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->call('getDueDate')
|
||||||
|
->assertSet('hasDueDate', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs delai de paiement (paydelay) et date de depart (date) sont obligatoires.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when getAttachListThumbnail fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->call('getAttachListThumbnail')
|
||||||
|
->assertSet('hasAttach', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs code journal (JNL) et numero de document (NUMBER) sont obligatoires.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when getPdf fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->call('getPdf')
|
||||||
|
->assertSet('hasPdf', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs code journal (JNL), numero de document (NUMBER) et mise en page (LAYOUT) sont obligatoires.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when addDocument required fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->set('mode', 'write')
|
||||||
|
->call('addDocument')
|
||||||
|
->assertSet('hasAdded', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs ThirdId, Date, Artid, Qty, Saleprice et JNL sont obligatoires.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when modDocument required fields are empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Documents::class)
|
||||||
|
->set('mode', 'write')
|
||||||
|
->call('modDocument')
|
||||||
|
->assertSet('hasModified', false)
|
||||||
|
->assertSet('errorMessage', 'Les champs numero de document (number) et code journal (JNL) sont obligatoires.');
|
||||||
|
|
||||||
Http::assertNothingSent();
|
Http::assertNothingSent();
|
||||||
});
|
});
|
||||||
@@ -228,7 +310,8 @@ it('displays error message on API failure', function () {
|
|||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(Documents::class)
|
$component = Livewire::test(Documents::class)
|
||||||
->call('searchDocuments')
|
->call('searchDocuments');
|
||||||
->assertSet('errorMessage', 'Invalid API key');
|
|
||||||
|
expect($component->get('errorMessage'))->toContain('Invalid API key');
|
||||||
});
|
});
|
||||||
|
|||||||
100
tests/Feature/JournauxPageTest.php
Normal file
100
tests/Feature/JournauxPageTest.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Pages\Journaux;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config([
|
||||||
|
'logistics.base_url' => 'http://test-server.local',
|
||||||
|
'logistics.api_key' => 'test-api-key',
|
||||||
|
'logistics.folder' => 'testfolder',
|
||||||
|
'logistics.timeout' => 30,
|
||||||
|
'logistics.connect_timeout' => 10,
|
||||||
|
'logistics.retry.times' => 1,
|
||||||
|
'logistics.retry.sleep_ms' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to read mode', function () {
|
||||||
|
Livewire::test(Journaux::class)
|
||||||
|
->assertSet('mode', 'read');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can switch between read and write modes', function () {
|
||||||
|
Livewire::test(Journaux::class)
|
||||||
|
->set('mode', 'write')
|
||||||
|
->assertSet('mode', 'write')
|
||||||
|
->set('mode', 'read')
|
||||||
|
->assertSet('mode', 'read');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when TYPE is empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Journaux::class)
|
||||||
|
->call('searchJournaux')
|
||||||
|
->assertSet('hasSearched', false)
|
||||||
|
->assertSet('errorMessage', 'Le champ type de journal (TYPE) est obligatoire.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('searches journaux via jnl_list', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/jnl_list' => Http::response([
|
||||||
|
'data' => [['jnlid' => 'VEN', 'jnlname' => 'Ventes']],
|
||||||
|
'metadata' => ['rowcount' => 1, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Journaux::class)
|
||||||
|
->set('type', 'V')
|
||||||
|
->set('select', 'jnlid,jnlname')
|
||||||
|
->call('searchJournaux')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', [['jnlid' => 'VEN', 'jnlname' => 'Ventes']])
|
||||||
|
->assertSet('errorMessage', null);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), 'jnl_list')
|
||||||
|
&& $request->data()['TYPE'] === 'V';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets hasSearched even when no results', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/jnl_list' => Http::response([
|
||||||
|
'data' => [],
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Journaux::class)
|
||||||
|
->set('type', 'Z')
|
||||||
|
->call('searchJournaux')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error message on API failure', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/jnl_list' => Http::response([
|
||||||
|
'data' => null,
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => false],
|
||||||
|
'error' => 'Invalid API key',
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Journaux::class)
|
||||||
|
->set('type', 'V')
|
||||||
|
->call('searchJournaux')
|
||||||
|
->assertSet('hasSearched', true);
|
||||||
|
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
});
|
||||||
@@ -65,12 +65,12 @@ it('sends correct parameters for art_list', function () {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$service = app(LogisticsService::class);
|
$service = app(LogisticsService::class);
|
||||||
$service->artList(['select' => 'artid,artname', 'search' => 'test']);
|
$service->artList(['select' => 'artid,name1', 'search' => 'test']);
|
||||||
|
|
||||||
Http::assertSent(function ($request) {
|
Http::assertSent(function ($request) {
|
||||||
$body = $request->data();
|
$body = $request->data();
|
||||||
|
|
||||||
return $body['select'] === 'artid,artname' && $body['search'] === 'test';
|
return $body['select'] === 'artid,name1' && $body['search'] === 'test';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
128
tests/Feature/TiersPageTest.php
Normal file
128
tests/Feature/TiersPageTest.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Pages\Tiers;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config([
|
||||||
|
'logistics.base_url' => 'http://test-server.local',
|
||||||
|
'logistics.api_key' => 'test-api-key',
|
||||||
|
'logistics.folder' => 'testfolder',
|
||||||
|
'logistics.timeout' => 30,
|
||||||
|
'logistics.connect_timeout' => 10,
|
||||||
|
'logistics.retry.times' => 1,
|
||||||
|
'logistics.retry.sleep_ms' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to read mode', function () {
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->assertSet('mode', 'read');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can switch between read and write modes', function () {
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->set('mode', 'write')
|
||||||
|
->assertSet('mode', 'write')
|
||||||
|
->set('mode', 'read')
|
||||||
|
->assertSet('mode', 'read');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when search is empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->call('searchTiers')
|
||||||
|
->assertSet('hasSearched', false)
|
||||||
|
->assertSet('errorMessage', 'Le champ de recherche (search) est obligatoire.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('searches tiers via third_list', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/third_list' => Http::response([
|
||||||
|
'data' => [['custid' => 'CUST001', 'custname' => 'Client Test']],
|
||||||
|
'metadata' => ['rowcount' => 1, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->set('search', 'test')
|
||||||
|
->set('select', 'custid,custname')
|
||||||
|
->call('searchTiers')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', [['custid' => 'CUST001', 'custname' => 'Client Test']])
|
||||||
|
->assertSet('errorMessage', null);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return str_contains($request->url(), 'third_list')
|
||||||
|
&& $request->data()['search'] === 'test';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets hasSearched even when no results', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/third_list' => Http::response([
|
||||||
|
'data' => [],
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->set('search', 'nonexistent')
|
||||||
|
->call('searchTiers')
|
||||||
|
->assertSet('hasSearched', true)
|
||||||
|
->assertSet('data', []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error when historyThirdId is empty', function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->call('getArtHistory')
|
||||||
|
->assertSet('hasHistory', false)
|
||||||
|
->assertSet('errorMessage', 'Le champ identifiant tiers (thirdid) est obligatoire.');
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets art history for a third party', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/third_GetArtHistory' => Http::response([
|
||||||
|
'data' => [['artid' => 'ART001', 'qty' => 5]],
|
||||||
|
'metadata' => ['rowcount' => 1, 'issuccess' => true],
|
||||||
|
'error' => null,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->set('historyThirdId', 'CUST001')
|
||||||
|
->call('getArtHistory')
|
||||||
|
->assertSet('hasHistory', true)
|
||||||
|
->assertSet('historyData', [['artid' => 'ART001', 'qty' => 5]])
|
||||||
|
->assertSet('errorMessage', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error message on API failure', function () {
|
||||||
|
Http::fake([
|
||||||
|
'*/third_list' => Http::response([
|
||||||
|
'data' => null,
|
||||||
|
'metadata' => ['rowcount' => 0, 'issuccess' => false],
|
||||||
|
'error' => 'Invalid API key',
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(Tiers::class)
|
||||||
|
->set('search', 'test')
|
||||||
|
->call('searchTiers')
|
||||||
|
->assertSet('hasSearched', true);
|
||||||
|
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
});
|
||||||
59
tests/Unit/ApiErrorTranslatorTest.php
Normal file
59
tests/Unit/ApiErrorTranslatorTest.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\ApiErrorTranslator;
|
||||||
|
|
||||||
|
it('returns null for null error', function () {
|
||||||
|
expect(ApiErrorTranslator::translate(null))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for empty string error', function () {
|
||||||
|
expect(ApiErrorTranslator::translate(''))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes array error to string', function () {
|
||||||
|
$result = ApiErrorTranslator::translate(['Error one', 'Error two']);
|
||||||
|
|
||||||
|
expect($result)->toContain('Error one');
|
||||||
|
expect($result)->toContain('Error two');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns string error as-is when no explanation found', function () {
|
||||||
|
expect(ApiErrorTranslator::translate('Some unknown error'))->toBe('Some unknown error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds explanation for known search terms error', function () {
|
||||||
|
$result = ApiErrorTranslator::translate('Search terms are required. Please provide search criteria.');
|
||||||
|
|
||||||
|
expect($result)->toContain('Search terms are required');
|
||||||
|
expect($result)->toContain('Explication');
|
||||||
|
expect($result)->toContain('Le champ de recherche est obligatoire.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds explanation for invalid api key error', function () {
|
||||||
|
$result = ApiErrorTranslator::translate('Invalid API key provided');
|
||||||
|
|
||||||
|
expect($result)->toContain('Invalid API key');
|
||||||
|
expect($result)->toContain('Explication');
|
||||||
|
expect($result)->toContain('cle API');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds explanation for unknown stkid error', function () {
|
||||||
|
$result = ApiErrorTranslator::translate('Unknown STKID');
|
||||||
|
|
||||||
|
expect($result)->toContain('Unknown STKID');
|
||||||
|
expect($result)->toContain('Explication');
|
||||||
|
expect($result)->toContain('STKID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds explanation for array error with known pattern', function () {
|
||||||
|
$result = ApiErrorTranslator::translate(['Search terms are required. Please provide search criteria.']);
|
||||||
|
|
||||||
|
expect($result)->toContain('Explication');
|
||||||
|
expect($result)->toContain('Le champ de recherche est obligatoire.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes mixed array values', function () {
|
||||||
|
$result = ApiErrorTranslator::normalize([123, 'text', null]);
|
||||||
|
|
||||||
|
expect($result)->toBe('123 text ');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user