Implement toggle for read/write mode across entity pages and enhance Documents and Divers functionality

- Added a toggle for switching between read and write modes on the Articles, Documents, Journaux, Tiers, and Divers pages, allowing users to access both data retrieval and data submission endpoints.
- Updated the Documents page to cover all 9 documented endpoints, including 7 for reading and 2 for writing, with appropriate error handling.
- Created a new Divers page to handle three endpoints: getserialnumber, codes_list, and custom_geninv_updatestock (the latter being non-functional).
- Introduced new methods in LogisticsService for handling PDF generation and stock updates, with corresponding updates in the API documentation.
- Improved form field components for better visual spacing in input fields.
This commit is contained in:
2026-02-20 15:51:58 +01:00
parent 8637dcc7cb
commit 7df94b64fa
22 changed files with 1777 additions and 376 deletions

View File

@@ -19,6 +19,8 @@ class Articles extends Page
protected string $view = 'filament.pages.articles'; protected string $view = 'filament.pages.articles';
public string $mode = 'read';
public string $search = ''; public string $search = '';
public string $select = 'artid,artname'; public string $select = 'artid,artname';

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Filament\Pages;
use App\Exceptions\LogisticsApiException;
use App\Services\LogisticsService;
use Filament\Pages\Page;
use Filament\Support\Icons\Heroicon;
class Divers extends Page
{
protected static string|\BackedEnum|null $navigationIcon = Heroicon::OutlinedWrenchScrewdriver;
protected static ?string $navigationLabel = 'Divers';
protected static ?string $title = 'Divers';
protected static ?int $navigationSort = 6;
protected string $view = 'filament.pages.divers';
public string $mode = 'read';
// getserialnumber
public array $serialData = [];
// codes_list
public string $code = '';
public array $codesData = [];
// custom_geninv_updatestock
public string $stkArtId = '';
public string $stkId = '';
public string $stkQty = '';
public string $stkToCheck = '';
public string $stkToCheckDetail = '';
public string $stkMode = '';
public array $updateStockResult = [];
public ?string $errorMessage = null;
public function getSerialNumber(): void
{
try {
$service = app(LogisticsService::class);
$response = $service->getSerialNumber();
$this->serialData = is_array($response['data'] ?? null) ? $response['data'] : ['value' => $response['data'] ?? null];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->serialData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->serialData = [];
}
}
public function searchCodes(): void
{
if (blank($this->code)) {
return;
}
try {
$service = app(LogisticsService::class);
$response = $service->codesList(['code' => $this->code]);
$this->codesData = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->codesData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->codesData = [];
}
}
public function updateStock(): void
{
if (blank($this->stkArtId) || blank($this->stkId) || blank($this->stkQty)) {
return;
}
try {
$service = app(LogisticsService::class);
$params = array_filter([
'ARTID' => $this->stkArtId,
'STKID' => $this->stkId,
'QTY' => $this->stkQty,
'TOCHECK' => $this->stkToCheck,
'TOCHECKDETAIL' => $this->stkToCheckDetail,
'MODE' => $this->stkMode,
]);
$response = $service->customGeninvUpdatestock($params);
$this->updateStockResult = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->updateStockResult = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->updateStockResult = [];
}
}
}

View File

@@ -19,18 +19,100 @@ class Documents extends Page
protected string $view = 'filament.pages.documents'; protected string $view = 'filament.pages.documents';
public string $mode = 'read';
// document_list
public string $select = 'jnl,number,thirdid,date'; public string $select = 'jnl,number,thirdid,date';
public string $thirdId = ''; public string $thirdId = '';
// document_detail
public string $detailJnl = ''; public string $detailJnl = '';
public string $detailNumber = ''; public string $detailNumber = '';
// Document_GetStatusList
public string $statusJnl = '';
// Document_GetUnitPriceAndVat
public string $priceArtId = '';
public string $priceQty = '';
public string $priceJnl = '';
public string $priceThirdId = '';
public string $priceDate = '';
// Document_GetDueDate
public string $payDelay = '';
public string $dueDateInput = '';
// Document_GetAttachListThumbnail
public string $attachJnl = '';
public string $attachNumber = '';
// Document_GetPDF
public string $pdfJnl = '';
public string $pdfNumber = '';
public string $pdfLayout = '';
// document_add
public string $addThirdId = '';
public string $addDate = '';
public string $addArtIds = '';
public string $addQty = '';
public string $addSalePrice = '';
public string $addJnl = '';
public string $addDiscount = '';
public string $addVatId = '';
public string $addVatPc = '';
// document_mod
public string $modNumber = '';
public string $modJnl = '';
public string $modThirdId = '';
public string $modArtIds = '';
public string $modQty = '';
public string $modSalePrice = '';
// Data holders
public array $data = []; public array $data = [];
public array $detailData = []; public array $detailData = [];
public array $statusData = [];
public array $priceData = [];
public array $dueDateData = [];
public array $attachData = [];
public array $pdfData = [];
public array $addResult = [];
public array $modResult = [];
public ?array $metadata = null; public ?array $metadata = null;
public ?string $errorMessage = null; public ?string $errorMessage = null;
@@ -79,4 +161,204 @@ class Documents extends Page
$this->detailData = []; $this->detailData = [];
} }
} }
public function getStatusList(): void
{
if (blank($this->statusJnl)) {
return;
}
try {
$service = app(LogisticsService::class);
$response = $service->documentGetStatusList($this->statusJnl);
$this->statusData = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->statusData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->statusData = [];
}
}
public function getUnitPriceAndVat(): void
{
if (blank($this->priceArtId) || blank($this->priceQty) || blank($this->priceJnl) || blank($this->priceThirdId) || blank($this->priceDate)) {
return;
}
try {
$service = app(LogisticsService::class);
$response = $service->documentGetUnitPriceAndVat([
'ARTID' => $this->priceArtId,
'QTY' => $this->priceQty,
'JNL' => $this->priceJnl,
'THIRDID' => $this->priceThirdId,
'DATE' => $this->priceDate,
]);
$this->priceData = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->priceData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->priceData = [];
}
}
public function getDueDate(): void
{
if (blank($this->payDelay) || blank($this->dueDateInput)) {
return;
}
try {
$service = app(LogisticsService::class);
$response = $service->documentGetDueDate($this->payDelay, $this->dueDateInput);
$this->dueDateData = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->dueDateData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->dueDateData = [];
}
}
public function getAttachListThumbnail(): void
{
if (blank($this->attachJnl) || blank($this->attachNumber)) {
return;
}
try {
$service = app(LogisticsService::class);
$response = $service->documentGetAttachListThumbnail($this->attachJnl, $this->attachNumber);
$this->attachData = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->attachData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->attachData = [];
}
}
public function getPdf(): void
{
if (blank($this->pdfJnl) || blank($this->pdfNumber) || blank($this->pdfLayout)) {
return;
}
try {
$service = app(LogisticsService::class);
$response = $service->documentGetPdf($this->pdfJnl, $this->pdfNumber, $this->pdfLayout);
$this->pdfData = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->pdfData = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->pdfData = [];
}
}
public function addDocument(): void
{
if (blank($this->addThirdId) || blank($this->addDate) || blank($this->addArtIds) || blank($this->addQty) || blank($this->addSalePrice) || blank($this->addJnl)) {
return;
}
try {
$service = app(LogisticsService::class);
$params = [
'ThirdId' => $this->addThirdId,
'Date' => $this->addDate,
'Artid' => $this->splitCsv($this->addArtIds),
'Qty' => $this->splitCsv($this->addQty),
'Saleprice' => $this->splitCsv($this->addSalePrice),
'JNL' => $this->addJnl,
];
if (filled($this->addDiscount)) {
$params['Discount'] = $this->splitCsv($this->addDiscount);
}
if (filled($this->addVatId)) {
$params['Vatid'] = $this->splitCsv($this->addVatId);
}
if (filled($this->addVatPc)) {
$params['Vatpc'] = $this->splitCsv($this->addVatPc);
}
$response = $service->documentAdd($params);
$this->addResult = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->addResult = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->addResult = [];
}
}
public function modDocument(): void
{
if (blank($this->modNumber) || blank($this->modJnl)) {
return;
}
try {
$service = app(LogisticsService::class);
$params = [
'number' => $this->modNumber,
'JNL' => $this->modJnl,
];
if (filled($this->modThirdId)) {
$params['Thirdid'] = $this->modThirdId;
}
if (filled($this->modArtIds)) {
$params['Artid'] = $this->splitCsv($this->modArtIds);
}
if (filled($this->modQty)) {
$params['Qty'] = $this->splitCsv($this->modQty);
}
if (filled($this->modSalePrice)) {
$params['Saleprice'] = $this->splitCsv($this->modSalePrice);
}
$response = $service->documentMod($params);
$this->modResult = $response['data'] ?? [];
$this->errorMessage = $response['error'] ?? null;
} catch (LogisticsApiException $e) {
$this->errorMessage = $e->getMessage();
$this->modResult = [];
} catch (\Throwable $e) {
$this->errorMessage = "Erreur inattendue : {$e->getMessage()}";
$this->modResult = [];
}
}
/**
* @return array<int, string>
*/
private function splitCsv(string $value): array
{
return array_map('trim', explode(',', $value));
}
} }

View File

@@ -19,6 +19,8 @@ class Journaux extends Page
protected string $view = 'filament.pages.journaux'; protected string $view = 'filament.pages.journaux';
public string $mode = 'read';
public string $select = ''; public string $select = '';
public int $results = 10; public int $results = 10;

View File

@@ -19,6 +19,8 @@ class Tiers extends Page
protected string $view = 'filament.pages.tiers'; protected string $view = 'filament.pages.tiers';
public string $mode = 'read';
public string $select = 'custid,custname'; public string $select = 'custid,custname';
public string $search = ''; public string $search = '';

View File

@@ -153,6 +153,23 @@ class LogisticsService
return $this->post('codes_list', $params); return $this->post('codes_list', $params);
} }
public function documentGetPdf(string $jnl, string $number, string $layout): array
{
return $this->post('Document_GetPDF', [
'JNL' => $jnl,
'NUMBER' => $number,
'LAYOUT' => $layout,
]);
}
/**
* @param array{ARTID: string, STKID: string, QTY: string, TOCHECK?: string, TOCHECKDETAIL?: string, MODE?: string} $params
*/
public function customGeninvUpdatestock(array $params): array
{
return $this->post('custom_geninv_updatestock', $params);
}
/** /**
* @return array{data: mixed, metadata: array, error: mixed} * @return array{data: mixed, metadata: array, error: mixed}
* *

View File

@@ -1,6 +1,6 @@
# Documentation API Logistics (Flex/ESI Gescom) # Documentation API Logistics (Flex/ESI Gescom)
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
--- ---
@@ -871,6 +871,7 @@ Les endpoints suivants ont été identifiés mais ne fonctionnent pas dans l'ét
| | | | | |
|---|---| |---|---|
| **URL** | `POST /{dossier}/Document_GetPDF` | | **URL** | `POST /{dossier}/Document_GetPDF` |
| **Méthode service** | `LogisticsService::documentGetPdf(string $jnl, string $number, string $layout)` |
| **Statut** | Non fonctionnel | | **Statut** | Non fonctionnel |
**Paramètres connus** : **Paramètres connus** :
@@ -890,6 +891,7 @@ Les endpoints suivants ont été identifiés mais ne fonctionnent pas dans l'ét
| | | | | |
|---|---| |---|---|
| **URL** | `POST /{dossier}/custom_geninv_updatestock` | | **URL** | `POST /{dossier}/custom_geninv_updatestock` |
| **Méthode service** | `LogisticsService::customGeninvUpdatestock(array $params)` |
| **Statut** | Non fonctionnel | | **Statut** | Non fonctionnel |
**Paramètres connus** : **Paramètres connus** :

View File

@@ -1,6 +1,6 @@
# Active Context # Active Context
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
## Travail en cours ## Travail en cours
@@ -8,72 +8,68 @@ Aucun travail en cours.
## Décisions récentes ## Décisions récentes
- **Convention d'écriture avec accents** (2026-02-20) : Tous les contenus rédigés en français (documentation, memory bank, règles Cursor) doivent utiliser les accents appropriés. La documentation API et le memory bank ont été entièrement réécrits avec les accents. - **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).
- **Page Documentation ajoue** (2026-02-20) : Nouvelle page Filament `Documentation` qui affiche le fichier markdown `documentation/documentation_api_logistics.md` converti en HTML via `Str::markdown()`. Actions d'en-tête : télécharger en PDF et ouvrir dans un nouvel onglet. - **Endpoints Documents complés** (2026-02-21) : La page Documents couvre désormais les 9 endpoints documentés : 7 en lecture (document_list, document_detail, Document_GetStatusList, Document_GetUnitPriceAndVat, Document_GetDueDate, Document_GetAttachListThumbnail, Document_GetPDF) et 2 en écriture (document_add, document_mod). Les champs tableaux (Artid, Qty, Saleprice, etc.) sont saisis en texte séparé par virgules et convertis en arrays PHP côté serveur via `splitCsv()`.
- **Styles prose personnalisés** (2026-02-20) : Le thème CSS Filament (`theme.css`) a été enrichi avec des styles `.documentation-prose` dédiés au rendu du markdown en dark mode : hiérarchie de titres avec bordures, tableaux avec bordures et en-têtes stylisés, blocs de code avec fond sombre, code inline coloré, liens bleus, listes à marqueurs, séparateurs visibles. - **Page Divers créée** (2026-02-21) : Nouvelle page Filament pour les endpoints divers : getserialnumber (lecture), codes_list (lecture), custom_geninv_updatestock (écriture, non fonctionnel).
- **Plugin @tailwindcss/typography** (2026-02-20) : Ajout du plugin via `@plugin "@tailwindcss/typography"` dans le thème Filament pour activer les classes `prose`. - **Endpoints non fonctionnels dans le service** (2026-02-21) : Les méthodes `documentGetPdf()` et `customGeninvUpdatestock()` ont été ajoutées au LogisticsService pour permettre le test de ces endpoints depuis l'interface, même s'ils sont non fonctionnels. Un bandeau d'avertissement ambre est affiché dans les formulaires correspondants.
- **Export PDF de la documentation** (2026-02-20) : Route `documentation.download-pdf` dans `routes/web.php` utilisant `barryvdh/laravel-dompdf` pour générer un PDF téléchargeable de la documentation. - **Padding gauche sur les inputs** (2026-02-21) : Ajout de `pl-3` dans le composant `form-field.blade.php` pour un meilleur espacement visuel du texte dans les champs de saisie.
- **Règle update-documentation** (2026-02-20) : Nouvelle règle Cursor `.cursor/rules/update-documentation.mdc` définissant la procédure de mise à jour de la documentation quand l'utilisateur dit "update documentation". - **Convention d'écriture avec accents** (2026-02-20) : Tous les contenus rédigés en français (documentation, memory bank, règles Cursor) doivent utiliser les accents appropriés.
- **Page Documentation ajoutée** (2026-02-20) : Nouvelle page Filament `Documentation` qui affiche le fichier markdown converti en HTML via `Str::markdown()`. Actions d'en-tête : télécharger en PDF et ouvrir dans un nouvel onglet.
- **Système de design unifié** (2026-02-20) : 10 composants Blade réutilisables dans `resources/views/components/logistics/` avec convention documentée dans `.cursor/rules/design-system.mdc`.
- **Filament v5 sans authentification** : Le `AdminPanelProvider` a été configuré sans `->login()` et sans `authMiddleware` pour permettre un accès libre au dashboard. - **Filament v5 sans authentification** : Le `AdminPanelProvider` a été configuré sans `->login()` et sans `authMiddleware` pour permettre un accès libre au dashboard.
- **Pages personnalisées plutôt que Resources** : L'application interroge une API externe, il n'y a pas de modèles Eloquent à gérer en CRUD. - **Pages personnalisées plutôt que Resources** : L'application interroge une API externe, il n'y a pas de modèles Eloquent à gérer en CRUD.
- **MySQL au lieu de SQLite** : Choix de l'utilisateur pour la base de données.
- **LogisticsService** : Toutes les interactions avec l'API sont centralisées dans un seul service. - **LogisticsService** : Toutes les interactions avec l'API sont centralisées dans un seul service.
- **Heroicon enum** : Filament v5 impose l'utilisation de `BackedEnum` pour `$navigationIcon` au lieu de strings.
- **$view non-static** : Filament v5 a changé `$view` de static à instance property.
- **Timeout et retry configurables** : Les paramètres sont dans `config/logistics.php` et pilotés par `.env`.
- **LogisticsApiException** : Exception dédiée créée pour distinguer les erreurs de connexion des erreurs de requête génériques. - **LogisticsApiException** : Exception dédiée créée pour distinguer les erreurs de connexion des erreurs de requête génériques.
- **Logging des échecs** : Les requêtes échouées sont aussi enregistrées dans `api_request_logs` avec `response_status = 0`.
- **Système de design unifié** (2026-02-20) : Création de 10 composants Blade réutilisables dans `resources/views/components/logistics/` et d'une convention documentée dans `.cursor/rules/design-system.mdc`.
- **Thème Filament personnalisé** (2026-02-20) : Création de `resources/css/filament/admin/theme.css` enregistré dans `AdminPanelProvider` via `->viteTheme()`.
- **TablesExplorer amélioré** (2026-02-20) : Remplacement de `wire:then` (invalide) par une méthode `selectTable()`. Ajout de la déduplication des colonnes, d'un filtre de tables, de la propriété computed `filteredTables`, des labels de type (`getDataTypeLabel`), et du stockage des métadonnées.
- **Connectivité API résolue** (2026-02-20) : Le serveur API est accessible via `http://tse-10-test.esi.local` (réseau interne). Le timeout a été augmenté à 300s dans `.env`.
## Changements récents (2026-02-20, session documentation) ## Changements récents (2026-02-21, session endpoints manquants)
- Page Filament `Documentation` créée (`app/Filament/Pages/Documentation.php`). - 2 méthodes ajoutées à `LogisticsService` : `documentGetPdf()`, `customGeninvUpdatestock()`.
- Vue Blade `resources/views/filament/pages/documentation.blade.php` créée. - Page `Documents.php` réécrite avec 9 endpoints (7 lecture + 2 écriture), toggle Lecture/Ecriture, méthode `splitCsv()`.
- Documentation markdown réécrite intégralement avec accents français. - Pages `Articles.php`, `Journaux.php`, `Tiers.php` : ajout de la propriété `$mode` et du toggle Lecture/Ecriture.
- Styles CSS `.documentation-prose` ajoutés dans `theme.css` (titres, tableaux, code, liens, listes). - Page `Divers.php` créée avec 3 endpoints (getserialnumber, codes_list, custom_geninv_updatestock).
- Plugin `@tailwindcss/typography` activé dans le thème Filament. - 7 vues Blade mises à jour ou créées : documents, articles, journaux, tiers, divers.
- Route PDF (`documentation.download-pdf`) ajoutée dans `routes/web.php`. - Composant `form-field.blade.php` : ajout de `pl-3`.
- Template PDF (`resources/views/pdf/documentation.blade.php`) créé. - 23 nouveaux tests Pest : `DocumentsPageTest.php` (13 tests), `DiversPageTest.php` (8 tests), 2 tests ajoutés dans `LogisticsServiceTest.php`.
- 5 tests Pest créés pour la page Documentation (`tests/Feature/DocumentationTest.php`). - Documentation API mise à jour (références service pour les endpoints non fonctionnels).
- Règle Cursor `update-documentation.mdc` créée. - Total : 84 tests passent (205 assertions), 1 test pré-existant en échec (FilamentDashboardTest > it displays project statistics).
- Memory bank entièrement réécrit avec accents français.
- Total : 61 tests passent (165 assertions).
## Historique ## Historique
### 2026-02-20 (session documentation)
- Page Filament `Documentation` créée.
- Documentation markdown réécrite intégralement avec accents français.
- Styles CSS `.documentation-prose` ajoutés dans `theme.css`.
- Plugin `@tailwindcss/typography` activé.
- Route PDF + template PDF créés.
- 5 tests Pest pour la page Documentation.
- Règle Cursor `update-documentation.mdc` créée.
- Memory bank réécrit avec accents français.
### 2026-02-20 (session design) ### 2026-02-20 (session design)
- 10 composants Blade créés dans `resources/views/components/logistics/`. - 10 composants Blade créés dans `resources/views/components/logistics/`.
- Convention de design documentée dans `.cursor/rules/design-system.mdc`. - Convention de design documentée dans `.cursor/rules/design-system.mdc`.
- 5 pages Filament refactorisées pour utiliser les composants du système de design. - 5 pages Filament refactorisées pour utiliser les composants du système de design.
- `TablesExplorer.php` amélioré : `selectTable()`, déduplication, filtre, computed property, métadonnées, labels de types. - TablesExplorer amélioré.
- Thème Filament personnalisé créé (`resources/css/filament/admin/theme.css`). - Thème Filament personnalisé créé.
- `AdminPanelProvider.php` mis à jour avec `->viteTheme()`.
- 6 tests Pest créés pour `TablesExplorer` (Livewire).
### 2026-02-20 (session robustesse) ### 2026-02-20 (session robustesse)
- `LogisticsService` mis à jour : timeout, connectTimeout, retry, gestion d'erreur avec `LogisticsApiException`. - LogisticsService mis à jour : timeout, connectTimeout, retry, gestion d'erreur.
- `LogisticsApiException` créée dans `app/Exceptions/`. - LogisticsApiException créée.
- `config/logistics.php` étendu avec timeout et retry.
- `.env` et `.env.example` complétés avec les nouvelles variables.
- Les 5 pages Filament mises à jour pour attraper `LogisticsApiException`.
- Tests passés de 8 à 12. - Tests passés de 8 à 12.
### 2026-02-19 ### 2026-02-19
- Installation de Filament v5.0.0 (31 packages ajoutés). - Installation de Filament v5.0.0.
- 5 pages Filament créées : TablesExplorer, Articles, Documents, Journaux, Tiers. - 5 pages Filament créées.
- `LogisticsService` créé avec 17 endpoints. - LogisticsService créé avec 17 endpoints.
- Migration `api_request_logs` créée. - Migration api_request_logs créée.
- 8 tests Pest écrits et validés.
## Prochaines étapes ## Prochaines étapes
- Corriger le test pré-existant `FilamentDashboardTest > it displays project statistics`.
- Tester toutes les pages avec de vraies données API et vérifier le rendu visuel. - Tester toutes les pages avec de vraies données API et vérifier le rendu visuel.
- Éventuellement : ajouter des pages pour les endpoints d'écriture (document_add, document_mod).
- Éventuellement : ajouter de la pagination ou du tri côté client pour les grands tableaux. - Éventuellement : ajouter de la pagination ou du tri côté client pour les grands tableaux.
- Éventuellement : ajouter une page de consultation des logs API. - Éventuellement : ajouter une page de consultation des logs API.

View File

@@ -1,6 +1,6 @@
# Product Context # Product Context
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
## Pourquoi ce projet existe ## Pourquoi ce projet existe
@@ -12,22 +12,26 @@ L'API Logistics (Flex/ESI Gescom) est un système de gestion commerciale accessi
## Problèmes résolus ## Problèmes résolus
- **Exploration de l'API** : Le dashboard permet de tester chaque endpoint avec des paramètres personnalisables, sans avoir besoin de Postman. - **Exploration de l'API** : Le dashboard permet de tester chaque endpoint (lecture et écriture) avec des paramètres personnalisables, sans avoir besoin de Postman.
- **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.
- **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).
## Expérience utilisateur ## Expérience utilisateur
L'utilisateur accède au dashboard Filament sur `http://api-logistics.test/admin`. La navigation latérale propose 6 pages : L'utilisateur accède au dashboard Filament sur `http://api-logistics.test/admin`. La navigation latérale propose 7 pages :
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. Rendu avec typographie soignée (titres hiérarchisés, tableaux bordés, blocs de code colorés, liens bleus). 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 (Caractère, Numérique, Date/Heure, Logique, Mémo). 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** : Formulaire de recherche (search, select, results) + vérification du stock d'un article par son ARTID. Résultats en tableau structuré, stock en JSON formaté. 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).
4. **Documents** : Recherche par tiers (thirdid) + consultation du détail d'un document (jnl + number). Détail en JSON formaté. 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** : Recherche par type de journal (TYPE). Résultats en tableau structuré. 5. **Journaux** : Toggle Lecture/Ecriture. En lecture : recherche par type de journal (TYPE). En écriture : état vide.
6. **Tiers** : Recherche de tiers (search obligatoire) + historique des articles d'un tiers. Historique en JSON formaté. 6. **Tiers** : Toggle Lecture/Ecriture. En lecture : recherche de tiers (search obligatoire) + historique des articles d'un tiers. En écriture : état vide.
7. **Divers** : Toggle Lecture/Ecriture. En lecture : getserialnumber (numéro de série), codes_list (données par code interne). En écriture : custom_geninv_updatestock (avec bandeau d'avertissement, endpoint non fonctionnel).
Toutes les pages utilisent le même système de design : cartes à en-tête séparé, badges de compteur, indicateurs de chargement sur les actions réseau, et états vides avec icônes explicatives. En cas d'erreur API, un bandeau rouge s'affiche avec le message en français. ### Toggle Lecture / Ecriture
Toutes les pages entité disposent d'un sélecteur en haut de page avec deux boutons : "Lecture" et "Ecriture". Le bouton actif est mis en surbrillance. Le changement de mode affiche instantanément les formulaires correspondants sans rechargement de page.

View File

@@ -1,6 +1,6 @@
# Progress # Progress
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
## Ce qui fonctionne ## Ce qui fonctionne
@@ -10,46 +10,51 @@ Dernière mise à jour : 2026-02-20
- [x] Documentation API rédigée avec accents (`documentation/documentation_api_logistics.md`) - [x] Documentation API rédigée avec accents (`documentation/documentation_api_logistics.md`)
- [x] Memory bank créé et structuré (`memory-bank/`, `.cursor/rules/memory-bank.mdc`) - [x] Memory bank créé et structuré (`memory-bank/`, `.cursor/rules/memory-bank.mdc`)
- [x] Configuration API Logistics (`.env`, `config/logistics.php`) avec timeout et retry - [x] Configuration API Logistics (`.env`, `config/logistics.php`) avec timeout et retry
- [x] `LogisticsService` créé (`app/Services/LogisticsService.php`) avec 17 méthodes, timeout, retry, gestion d'erreur - [x] `LogisticsService` créé (`app/Services/LogisticsService.php`) avec 19 méthodes, timeout, retry, gestion d'erreur
- [x] `LogisticsApiException` créée (`app/Exceptions/LogisticsApiException.php`) avec messages français - [x] `LogisticsApiException` créée (`app/Exceptions/LogisticsApiException.php`) avec messages français
- [x] Migration `api_request_logs` créée - [x] Migration `api_request_logs` créée
- [x] Filament v5.0.0 installé et configuré sans authentification - [x] Filament v5.0.0 installé et configuré sans authentification
- [x] 6 pages Filament créées : Documentation, TablesExplorer, Articles, Documents, Journaux, Tiers - [x] 7 pages Filament créées : Documentation, TablesExplorer, Articles, Documents, Journaux, Tiers, Divers
- [x] 6 vues Blade associées dans `resources/views/filament/pages/` - [x] 7 vues Blade associées dans `resources/views/filament/pages/`
- [x] Toggle Lecture/Ecriture sur toutes les pages entité (Articles, Documents, Journaux, Tiers, Divers)
- [x] Page Documents : 9 endpoints couverts (7 lecture + 2 écriture)
- [x] Page Divers : 3 endpoints couverts (2 lecture + 1 écriture non fonctionnel)
- [x] Gestion d'erreur dans toutes les pages Filament (LogisticsApiException + Throwable) - [x] Gestion d'erreur dans toutes les pages Filament (LogisticsApiException + Throwable)
- [x] Logging des requêtes API réussies et échouées dans `api_request_logs` - [x] Logging des requêtes API réussies et échouées dans `api_request_logs`
- [x] Système de design unifié : 10 composants Blade dans `resources/views/components/logistics/` - [x] Système de design unifié : 10 composants Blade dans `resources/views/components/logistics/`
- [x] Convention de design documentée dans `.cursor/rules/design-system.mdc` - [x] Convention de design documentée dans `.cursor/rules/design-system.mdc`
- [x] Toutes les pages Filament refactorisées avec les composants `x-logistics.*` - [x] Toutes les pages Filament utilisent les composants `x-logistics.*`
- [x] Thème Filament personnalisé (`resources/css/filament/admin/theme.css`) - [x] Thème Filament personnalisé (`resources/css/filament/admin/theme.css`)
- [x] Plugin `@tailwindcss/typography` activé pour le rendu prose - [x] Plugin `@tailwindcss/typography` activé pour le rendu prose
- [x] Styles `.documentation-prose` personnalisés pour le dark mode (titres, tableaux, code, liens) - [x] Styles `.documentation-prose` personnalisés pour le dark mode
- [x] TablesExplorer amélioré : selectTable, déduplication colonnes, filtre, badges de types - [x] TablesExplorer amélioré : selectTable, déduplication colonnes, filtre, badges de types
- [x] Page Documentation avec rendu markdown stylisé et export PDF - [x] Page Documentation avec rendu markdown stylisé et export PDF
- [x] Connectivité API fonctionnelle (serveur `tse-10-test.esi.local`) - [x] Connectivité API fonctionnelle (serveur `tse-10-test.esi.local`)
- [x] Convention d'écriture avec accents français appliquée - [x] Convention d'écriture avec accents français appliquée
- [x] 61 tests Pest (tous passent) - [x] 84 tests Pest passent (205 assertions)
- [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)
## Ce qui reste à faire ## Ce qui reste à faire
- [ ] Corriger le test pré-existant `FilamentDashboardTest > it displays project statistics`
- [ ] Vérifier le rendu visuel de toutes les pages avec de vraies données API - [ ] Vérifier le rendu visuel de toutes les pages avec de vraies données API
- [ ] Éventuellement : pages d'écriture (document_add, document_mod)
- [ ] Éventuellement : pagination / tri côté client pour les grands tableaux - [ ] Éventuellement : pagination / tri côté client pour les grands tableaux
- [ ] Éventuellement : page de consultation des logs API - [ ] Éventuellement : page de consultation des logs API
## Problèmes connus ## Problèmes connus
- 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.
## Métriques ## Métriques
- Tests : 61 (tous passent, 165 assertions) - Tests : 84 passent, 1 en échec pré-existant (205 assertions)
- Pages Filament : 6 (Documentation, TablesExplorer, Articles, Documents, Journaux, Tiers) - 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 : 17 - Endpoints API couverts par LogisticsService : 19
- 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)

View File

@@ -1,6 +1,6 @@
# Project Brief # Project Brief
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
## Vision ## Vision
@@ -15,13 +15,15 @@ Application Laravel de test dont l'objectif est de comprendre le fonctionnement
## Périmètre fonctionnel ## Périmètre fonctionnel
- Dashboard Filament v5 accessible sans authentification sur `/admin`. - Dashboard Filament v5 accessible sans authentification sur `/admin`.
- Pages de consultation pour les principales entités de l'API : tables, articles, documents, journaux, tiers. - Pages de consultation et d'envoi pour les principales entités de l'API : tables, articles, documents, journaux, tiers, divers.
- Toggle Lecture/Ecriture sur chaque page entité pour basculer entre les endpoints de récupération et d'envoi de données.
- Page de documentation intégrée avec rendu stylisé du markdown et export PDF. - Page de documentation intégrée avec rendu stylisé du markdown et export PDF.
- Formulaires de recherche paramétrables pour chaque endpoint. - Formulaires de recherche et d'envoi paramétrables pour chaque endpoint (19 au total).
- 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).
- Avertissements visuels pour les endpoints non fonctionnels (Document_GetPDF, custom_geninv_updatestock).
## Contraintes ## Contraintes
@@ -39,4 +41,5 @@ Tous les contenus rédigés en français (documentation, memory bank, règles Cu
- Documentation Postman : https://documenter.getpostman.com/view/40440561/2sB2qaj2Pz - Documentation Postman : https://documenter.getpostman.com/view/40440561/2sB2qaj2Pz
- Documentation interne : `documentation/documentation_api_logistics.md` - Documentation interne : `documentation/documentation_api_logistics.md`
- Documentation originale fournisseur : `documentation/WEB-A-1 (3).md`
- Fichier projet : `project.md` - Fichier projet : `project.md`

View File

@@ -1,6 +1,6 @@
# System Patterns # System Patterns
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
## Architecture applicative ## Architecture applicative
@@ -9,6 +9,8 @@ Utilisateur --> Filament Dashboard (/admin)
| |
v v
Pages Filament (Livewire) Pages Filament (Livewire)
|
+---> Toggle Lecture / Ecriture (propriété $mode)
| |
v v
LogisticsService (app/Services/) LogisticsService (app/Services/)
@@ -26,7 +28,7 @@ Utilisateur --> Filament Dashboard (/admin)
### Service centralisé ### Service centralisé
`App\Services\LogisticsService` encapsule tous les appels HTTP vers l'API Logistics. Chaque méthode publique correspond à un endpoint. Le service : `App\Services\LogisticsService` encapsule tous les appels HTTP vers l'API Logistics. Chaque méthode publique correspond à un endpoint (19 méthodes). Le service :
- Construit l'URL à partir de `config('logistics.base_url')`, `config('logistics.folder')` et le nom de l'endpoint. - Construit l'URL à partir de `config('logistics.base_url')`, `config('logistics.folder')` et le nom de l'endpoint.
- Ajoute automatiquement le header `X-API-KEY`. - Ajoute automatiquement le header `X-API-KEY`.
@@ -50,10 +52,28 @@ L'application n'utilise pas de Resources Filament (pas de CRUD local). Chaque pa
- Étend `Filament\Pages\Page`. - Étend `Filament\Pages\Page`.
- Utilise des propriétés Livewire publiques pour les champs de formulaire. - Utilise des propriétés Livewire publiques pour les champs de formulaire.
- Appelle `LogisticsService` via `app(LogisticsService::class)` dans des méthodes d'action (ex: `searchArticles()`). - Possède une propriété `$mode` (string : `'read'` ou `'write'`) pour le toggle Lecture/Ecriture (sauf Dashboard, Documentation, TablesExplorer).
- Appelle `LogisticsService` via `app(LogisticsService::class)` dans des méthodes d'action.
- Attrape `LogisticsApiException` en premier, puis `\Throwable` en fallback. - Attrape `LogisticsApiException` en premier, puis `\Throwable` en fallback.
- Affiche les résultats via les composants du système de design `x-logistics.*`. - Affiche les résultats via les composants du système de design `x-logistics.*`.
### Toggle Lecture / Ecriture
Les pages entité (Articles, Documents, Journaux, Tiers, Divers) intègrent un toggle permettant de basculer entre :
- **Lecture** (`$mode = 'read'`) : formulaires pour les endpoints de récupération de données.
- **Ecriture** (`$mode = 'write'`) : formulaires pour les endpoints d'envoi de données.
Le toggle est implémenté avec deux boutons et un `wire:click="$set('mode', 'read')"` / `wire:click="$set('mode', 'write')"`. Le rendu conditionnel utilise `@if ($mode === 'read')` / `@else`. Les pages sans endpoint d'écriture affichent un état vide (`<x-logistics.empty-state>`) en mode écriture.
### Conversion CSV vers tableaux (splitCsv)
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"]`.
### 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.
### Page Documentation ### Page Documentation
`App\Filament\Pages\Documentation` est une page spéciale qui : `App\Filament\Pages\Documentation` est une page spéciale qui :
@@ -77,7 +97,7 @@ Convention documentée dans `.cursor/rules/design-system.mdc`. Tous les composan
| `<x-logistics.data-table>` | Tableau dynamique avec en-têtes auto-détectés et état vide | | `<x-logistics.data-table>` | Tableau dynamique avec en-têtes auto-détectés et état vide |
| `<x-logistics.empty-state>` | État vide centré (icône + titre + description) | | `<x-logistics.empty-state>` | État vide centré (icône + titre + description) |
| `<x-logistics.search-input>` | Champ de recherche avec icône loupe intégrée | | `<x-logistics.search-input>` | Champ de recherche avec icône loupe intégrée |
| `<x-logistics.form-field>` | Champ de formulaire (label + input) à espacement homogène | | `<x-logistics.form-field>` | Champ de formulaire (label + input + padding gauche) à espacement homogène |
| `<x-logistics.json-block>` | Bloc JSON formaté avec bordure et fond adapté | | `<x-logistics.json-block>` | Bloc JSON formaté avec bordure et fond adapté |
Règles : Règles :
@@ -93,7 +113,7 @@ Le panel Filament utilise un thème CSS personnalisé (`resources/css/filament/a
- Importe le CSS de base Filament. - Importe le CSS de base Filament.
- Active le plugin `@tailwindcss/typography` pour les classes `prose`. - Active le plugin `@tailwindcss/typography` pour les classes `prose`.
- Scanne les fichiers `app/Filament/**/*`, `resources/views/filament/**/*`, et `resources/views/components/logistics/**/*` pour inclure les classes Tailwind utilisées dans les composants. - Scanne les fichiers `app/Filament/**/*`, `resources/views/filament/**/*`, et `resources/views/components/logistics/**/*` pour inclure les classes Tailwind utilisées dans les composants.
- Contient des styles CSS personnalisés pour la classe `.documentation-prose` : hiérarchie de titres (h1-h4 avec bordures), tableaux avec bordures et en-têtes stylisés, blocs de code avec fond sombre, code inline avec fond distinct, liens colorés, listes avec marqueurs, séparateurs horizontaux visibles. - Contient des styles CSS personnalisés pour la classe `.documentation-prose`.
Après tout ajout de nouvelles classes Tailwind dans ces fichiers, il faut exécuter `npm run build`. Après tout ajout de nouvelles classes Tailwind dans ces fichiers, il faut exécuter `npm run build`.
@@ -109,13 +129,14 @@ app/
LogisticsApiException.php # Exception dédiée API LogisticsApiException.php # Exception dédiée API
Filament/ Filament/
Pages/ Pages/
Articles.php # Recherche articles + stock Articles.php # Recherche articles + stock (toggle lecture/écriture)
Dashboard.php # Page d'accueil Dashboard.php # Page d'accueil
Divers.php # Endpoints divers : getserialnumber, codes_list, custom_geninv_updatestock (toggle)
Documentation.php # Documentation API (markdown -> HTML) Documentation.php # Documentation API (markdown -> HTML)
Documents.php # Recherche documents + détail Documents.php # 9 endpoints documents (toggle lecture/écriture, splitCsv)
Journaux.php # Recherche journaux Journaux.php # Recherche journaux (toggle lecture/écriture)
TablesExplorer.php # Exploration tables + colonnes (filtre, déduplication, types) TablesExplorer.php # Exploration tables + colonnes (filtre, déduplication, types)
Tiers.php # Recherche tiers + historique Tiers.php # Recherche tiers + historique (toggle lecture/écriture)
Models/ Models/
User.php # Modèle utilisateur (Fortify) User.php # Modèle utilisateur (Fortify)
Providers/ Providers/
@@ -124,13 +145,14 @@ app/
FortifyServiceProvider.php # Authentification Fortify FortifyServiceProvider.php # Authentification Fortify
AppServiceProvider.php # Config globale (CarbonImmutable, DB safety) AppServiceProvider.php # Config globale (CarbonImmutable, DB safety)
Services/ Services/
LogisticsService.php # Service centralisé API Logistics LogisticsService.php # Service centralisé API Logistics (19 méthodes)
config/ config/
logistics.php # Configuration API Logistics (URL, clé, timeout, retry) logistics.php # Configuration API Logistics (URL, clé, timeout, retry)
documentation/ documentation/
documentation_api_logistics.md # Documentation complète de l'API (avec accents) documentation_api_logistics.md # Documentation complète de l'API (avec accents)
WEB-A-1 (3).md # Documentation originale du fournisseur
resources/ resources/
css/ css/
@@ -139,7 +161,7 @@ resources/
theme.css # Thème Filament personnalisé (Tailwind 4 + Typography + prose) theme.css # Thème Filament personnalisé (Tailwind 4 + Typography + prose)
views/ views/
components/logistics/ # 10 composants du système de design components/logistics/ # 10 composants du système de design
filament/pages/ # 6 vues de pages Filament filament/pages/ # 7 vues de pages Filament
pdf/ pdf/
documentation.blade.php # Template PDF pour la documentation documentation.blade.php # Template PDF pour la documentation
@@ -153,9 +175,11 @@ database/
tests/Feature/ tests/Feature/
DocumentationTest.php # 5 tests page Documentation (Livewire + PDF) DocumentationTest.php # 5 tests page Documentation (Livewire + PDF)
LogisticsServiceTest.php # 12 tests service API (mocks HTTP) DocumentsPageTest.php # 13 tests page Documents (toggle, 9 endpoints, erreurs)
DiversPageTest.php # 8 tests page Divers (toggle, 3 endpoints, erreurs)
LogisticsServiceTest.php # 14 tests service API (mocks HTTP)
TablesExplorerTest.php # 6 tests page TablesExplorer (Livewire) TablesExplorerTest.php # 6 tests page TablesExplorer (Livewire)
FilamentDashboardTest.php # Tests dashboard Filament 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
@@ -180,3 +204,4 @@ routes/
- Toutes les vues Filament utilisent les composants `x-logistics.*` du système de design. - Toutes les vues Filament utilisent les composants `x-logistics.*` du système de design.
- Après modification des vues ou composants, exécuter `npm run build` pour recompiler le thème. - Après modification des vues ou composants, exécuter `npm run build` pour recompiler le thème.
- Tous les contenus rédigés en français doivent utiliser les accents appropriés. - Tous les contenus rédigés en français doivent utiliser les accents appropriés.
- Les endpoints non fonctionnels sont présents dans l'interface avec un bandeau d'avertissement.

View File

@@ -1,6 +1,6 @@
# Tech Context # Tech Context
Dernière mise à jour : 2026-02-20 Dernière mise à jour : 2026-02-21
## Stack technique ## Stack technique
@@ -92,27 +92,31 @@ Fichier de config : `config/logistics.php`
Réponse `tables_list` : chaque table a `name` et `columnCount`. Réponse `tables_list` : chaque table a `name` et `columnCount`.
Réponse `column_list` : chaque colonne a `name`, `dataType` (C/N/T/D/L/M), `length`, `precision`. Les colonnes sont retournées en double par l'API (dédupliquées côté client). Réponse `column_list` : chaque colonne a `name`, `dataType` (C/N/T/D/L/M), `length`, `precision`. Les colonnes sont retournées en double par l'API (dédupliquées côté client).
### Endpoints ### Endpoints (19 méthodes dans LogisticsService)
| Endpoint | Description | Paramètres principaux | | Endpoint | Méthode service | Type | Description | Paramètres principaux |
|----------|-------------|-----------------------| |----------|----------------|------|-------------|-----------------------|
| `tables_list` | Liste des tables | - | | `tables_list` | `tablesList()` | Lecture | Liste des tables | - |
| `column_list/{table}` | Colonnes d'une table | table (URL) | | `column_list/{table}` | `columnList(string)` | Lecture | Colonnes d'une table | table (URL) |
| `art_list` | Liste d'articles | select, results, search, barcode | | `art_list` | `artList(array)` | Lecture | Liste d'articles | select, results, search, barcode |
| `art_getstk` | Stock d'un article | ARTID | | `art_getstk` | `artGetStock(string)` | Lecture | Stock d'un article | ARTID |
| `jnl_list` | Liste des journaux | select, results, TYPE | | `jnl_list` | `jnlList(array)` | Lecture | Liste des journaux | select, results, TYPE |
| `document_list` | Liste des documents | select, thirdid | | `document_list` | `documentList(array)` | Lecture | Liste des documents | select, thirdid |
| `document_detail` | Détail d'un document | jnl, number | | `document_detail` | `documentDetail(string, string)` | Lecture | Détail d'un document | jnl, number |
| `document_add` | Ajout d'un document | ThirdId, Date, Artid[], Qty[], Saleprice[], JNL, ... | | `Document_GetStatusList` | `documentGetStatusList(string)` | Lecture | Statuts d'un journal | jnl |
| `document_mod` | Modification d'un document | number, Thirdid, Artid[], Qty[], Saleprice[], JNL, ... | | `Document_GetUnitPriceAndVat` | `documentGetUnitPriceAndVat(array)` | Lecture | Prix et TVA | ARTID, QTY, JNL, THIRDID, DATE |
| `Document_GetStatusList` | Statuts d'un journal | jnl | | `Document_GetDueDate` | `documentGetDueDate(string, string)` | Lecture | Échéance | paydelay, date |
| `Document_GetUnitPriceAndVat` | Prix et TVA | ARTID, QTY, JNL, THIRDID, DATE | | `Document_GetAttachListThumbnail` | `documentGetAttachListThumbnail(string, string)` | Lecture | Miniatures annexes | JNL, NUMBER |
| `Document_GetDueDate` | Échéance | paydelay, date | | `Document_GetPDF` | `documentGetPdf(string, string, string)` | Lecture | Génération PDF | JNL, NUMBER, LAYOUT |
| `Document_GetAttachListThumbnail` | Miniatures annexes | JNL, NUMBER | | `third_list` | `thirdList(array)` | Lecture | Liste des tiers | select, results, search |
| `third_list` | Liste des tiers | select, results, search | | `third_GetArtHistory` | `thirdGetArtHistory(string)` | Lecture | Historique articles tiers | thirdid |
| `third_GetArtHistory` | Historique articles tiers | thirdid | | `getserialnumber` | `getSerialNumber()` | Lecture | Numéro de série | - |
| `getserialnumber` | Numéro de série | - | | `codes_list` | `codesList(array)` | Lecture | Données par code | code |
| `codes_list` | Données par code | code | | `document_add` | `documentAdd(array)` | Écriture | Ajout d'un document | ThirdId, Date, Artid[], Qty[], Saleprice[], JNL, ... |
| `document_mod` | `documentMod(array)` | Écriture | Modification d'un document | number, Thirdid, Artid[], Qty[], Saleprice[], JNL, ... |
| `custom_geninv_updatestock` | `customGeninvUpdatestock(array)` | Écriture | Mise à jour inventaire | ARTID, STKID, QTY, ... |
**Endpoints non fonctionnels** : `Document_GetPDF` (paramètre LAYOUT inconnu), `custom_geninv_updatestock` (paramètre STKID inconnu, signification de TOCHECK/TOCHECKDETAIL/MODE à clarifier).
### Tables accessibles ### Tables accessibles

View File

@@ -3,7 +3,7 @@
<div> <div>
<label for="{{ $id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ $label }}</label> <label for="{{ $id }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ $label }}</label>
<input <input
{{ $attributes->class(['mt-1.5 w-full rounded-lg border-gray-300 py-2 text-sm shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-white/10 dark:bg-white/5 dark:text-white']) }} {{ $attributes->class(['mt-1.5 w-full rounded-lg border-gray-300 py-2 pl-3 text-sm shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-white/10 dark:bg-white/5 dark:text-white']) }}
type="{{ $type }}" type="{{ $type }}"
id="{{ $id }}" id="{{ $id }}"
placeholder="{{ $placeholder }}" placeholder="{{ $placeholder }}"

View File

@@ -1,89 +1,110 @@
<x-filament-panels::page> <x-filament-panels::page>
<x-logistics.error-banner :message="$errorMessage" /> <x-logistics.error-banner :message="$errorMessage" />
{{-- Formulaire de recherche --}} {{-- Toggle Lecture / Ecriture --}}
<x-logistics.card> <div class="flex gap-2">
<x-logistics.section-header title="Rechercher des articles" /> <x-filament::button wire:click="$set('mode', 'read')" :color="$mode === 'read' ? 'primary' : 'gray'" icon="heroicon-o-arrow-down-tray">
<div class="p-6"> Lecture
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> </x-filament::button>
<x-logistics.form-field <x-filament::button wire:click="$set('mode', 'write')" :color="$mode === 'write' ? 'primary' : 'gray'" icon="heroicon-o-arrow-up-tray">
wire:model="search" Ecriture
label="Recherche" </x-filament::button>
id="search" </div>
placeholder="Filtre de recherche..."
/>
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="artid,artname"
/>
<x-logistics.form-field
wire:model="results"
label="Nombre de résultats"
id="results"
type="number"
min="1"
max="100"
/>
</div>
<div class="mt-4 flex items-center gap-3"> @if ($mode === 'read')
<x-filament::button wire:click="searchArticles" icon="heroicon-o-magnifying-glass"> {{-- art_list --}}
Rechercher <x-logistics.card>
</x-filament::button> <x-logistics.section-header title="art_list" description="Rechercher des articles" />
<div wire:loading wire:target="searchArticles" class="flex items-center gap-2"> <div class="p-6">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" /> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<span class="text-sm text-gray-500">Recherche en cours...</span> <x-logistics.form-field
wire:model="search"
label="Recherche (search)"
id="search"
placeholder="Filtre de recherche..."
/>
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="artid,artname"
/>
<x-logistics.form-field
wire:model="results"
label="Nombre de résultats (results)"
id="results"
type="number"
min="1"
max="100"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="searchArticles" icon="heroicon-o-magnifying-glass">
Rechercher
</x-filament::button>
<div wire:loading wire:target="searchArticles" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Recherche en cours...</span>
</div>
</div> </div>
</div> </div>
</div> </x-logistics.card>
</x-logistics.card>
{{-- Resultats --}} @if (count($data) > 0)
@if (count($data) > 0) <x-logistics.card>
<x-logistics.section-header title="Résultats art_list">
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6">
<x-logistics.data-table :data="$data" />
</div>
</x-logistics.card>
@endif
{{-- art_getstk --}}
<x-logistics.card> <x-logistics.card>
<x-logistics.section-header title="Résultats"> <x-logistics.section-header title="art_getstk" description="Vérifier le stock d'un article" />
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6"> <div class="p-6">
<x-logistics.data-table :data="$data" /> <div class="flex items-end gap-4">
<div class="flex-1">
<x-logistics.form-field
wire:model="stockArticleId"
label="Identifiant article (ARTID)"
id="stockArticleId"
placeholder="Ex: ART001"
/>
</div>
<x-filament::button wire:click="getStock" icon="heroicon-o-cube">
Vérifier le stock
</x-filament::button>
</div>
<div wire:loading wire:target="getStock" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getStock" class="mt-4">
<x-logistics.json-block :data="$stockData" />
</div>
</div>
</x-logistics.card>
@else
<x-logistics.card>
<div class="p-6">
<x-logistics.empty-state
icon="heroicon-o-arrow-up-tray"
title="Aucun endpoint d'écriture disponible"
description="La section Articles ne dispose pas d'endpoints d'envoi de données."
/>
</div> </div>
</x-logistics.card> </x-logistics.card>
@endif @endif
{{-- Stock d'un article --}}
<x-logistics.card>
<x-logistics.section-header title="Vérifier le stock d'un article" />
<div class="p-6">
<div class="flex items-end gap-4">
<div class="flex-1">
<x-logistics.form-field
wire:model="stockArticleId"
label="Identifiant article (ARTID)"
id="stockArticleId"
placeholder="Ex: ART001"
/>
</div>
<x-filament::button wire:click="getStock" icon="heroicon-o-cube">
Vérifier le stock
</x-filament::button>
</div>
<div wire:loading wire:target="getStock" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getStock" class="mt-4">
<x-logistics.json-block :data="$stockData" />
</div>
</div>
</x-logistics.card>
</x-filament-panels::page> </x-filament-panels::page>

View File

@@ -0,0 +1,126 @@
<x-filament-panels::page>
<x-logistics.error-banner :message="$errorMessage" />
{{-- Toggle Lecture / Ecriture --}}
<div class="flex gap-2">
<x-filament::button wire:click="$set('mode', 'read')" :color="$mode === 'read' ? 'primary' : 'gray'" icon="heroicon-o-arrow-down-tray">
Lecture
</x-filament::button>
<x-filament::button wire:click="$set('mode', 'write')" :color="$mode === 'write' ? 'primary' : 'gray'" icon="heroicon-o-arrow-up-tray">
Ecriture
</x-filament::button>
</div>
@if ($mode === 'read')
{{-- getserialnumber --}}
<x-logistics.card>
<x-logistics.section-header title="getserialnumber" description="Numéro de série du dossier comptable" />
<div class="p-6">
<div class="flex items-center gap-3">
<x-filament::button wire:click="getSerialNumber" icon="heroicon-o-finger-print">
Obtenir le numéro de série
</x-filament::button>
<div wire:loading wire:target="getSerialNumber" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
</div>
<div wire:loading.remove wire:target="getSerialNumber" class="mt-4">
<x-logistics.json-block :data="$serialData" />
</div>
</div>
</x-logistics.card>
{{-- codes_list --}}
<x-logistics.card>
<x-logistics.section-header title="codes_list" description="Données associées à un code interne (table incodes)" />
<div class="p-6">
<div class="flex items-end gap-4">
<div class="flex-1">
<x-logistics.form-field
wire:model="code"
label="Début de code (code)"
id="code"
placeholder="Ex: PAY"
/>
</div>
<x-filament::button wire:click="searchCodes" icon="heroicon-o-magnifying-glass">
Rechercher
</x-filament::button>
</div>
<div wire:loading wire:target="searchCodes" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="searchCodes" class="mt-4">
<x-logistics.json-block :data="$codesData" />
</div>
</div>
</x-logistics.card>
@else
{{-- custom_geninv_updatestock --}}
<x-logistics.card>
<x-logistics.section-header title="custom_geninv_updatestock" description="Mise à jour de l'inventaire" />
<div class="p-6">
<div class="mb-4 rounded-lg bg-amber-50 p-3 text-sm text-amber-700 dark:bg-amber-400/10 dark:text-amber-400">
Endpoint non fonctionnel -- la valeur attendue de STKID et la signification de certains paramètres (TOCHECK, TOCHECKDETAIL, MODE) sont inconnues.
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="stkArtId"
label="Article (ARTID)"
id="stkArtId"
placeholder="Ex: ART001"
/>
<x-logistics.form-field
wire:model="stkId"
label="Identifiant stock (STKID)"
id="stkId"
placeholder="Valeur inconnue"
/>
<x-logistics.form-field
wire:model="stkQty"
label="Quantité (QTY)"
id="stkQty"
placeholder="Ex: 10"
/>
<x-logistics.form-field
wire:model="stkToCheck"
label="TOCHECK (signification inconnue)"
id="stkToCheck"
placeholder="Prix ?"
/>
<x-logistics.form-field
wire:model="stkToCheckDetail"
label="TOCHECKDETAIL (signification inconnue)"
id="stkToCheckDetail"
placeholder="Remarques ?"
/>
<x-logistics.form-field
wire:model="stkMode"
label="MODE (signification inconnue)"
id="stkMode"
placeholder="Valeur inconnue"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="updateStock" icon="heroicon-o-arrow-path" color="warning">
Mettre à jour le stock
</x-filament::button>
<div wire:loading wire:target="updateStock" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Envoi en cours...</span>
</div>
</div>
<div wire:loading.remove wire:target="updateStock" class="mt-4">
<x-logistics.json-block :data="$updateStockResult" />
</div>
</div>
</x-logistics.card>
@endif
</x-filament-panels::page>

View File

@@ -1,87 +1,438 @@
<x-filament-panels::page> <x-filament-panels::page>
<x-logistics.error-banner :message="$errorMessage" /> <x-logistics.error-banner :message="$errorMessage" />
{{-- Formulaire de recherche --}} {{-- Toggle Lecture / Ecriture --}}
<x-logistics.card> <div class="flex gap-2">
<x-logistics.section-header title="Rechercher des documents" /> <x-filament::button wire:click="$set('mode', 'read')" :color="$mode === 'read' ? 'primary' : 'gray'" icon="heroicon-o-arrow-down-tray">
<div class="p-6"> Lecture
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> </x-filament::button>
<x-logistics.form-field <x-filament::button wire:click="$set('mode', 'write')" :color="$mode === 'write' ? 'primary' : 'gray'" icon="heroicon-o-arrow-up-tray">
wire:model="select" Ecriture
label="Colonnes (select)" </x-filament::button>
id="select" </div>
placeholder="jnl,number,thirdid,date"
/>
<x-logistics.form-field
wire:model="thirdId"
label="Identifiant tiers (thirdid)"
id="thirdId"
placeholder="Ex: CUST001"
/>
</div>
<div class="mt-4 flex items-center gap-3"> @if ($mode === 'read')
<x-filament::button wire:click="searchDocuments" icon="heroicon-o-magnifying-glass"> {{-- ============================================================ --}}
Rechercher {{-- MODE LECTURE --}}
</x-filament::button> {{-- ============================================================ --}}
<div wire:loading wire:target="searchDocuments" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" /> {{-- document_list --}}
<span class="text-sm text-gray-500">Recherche en cours...</span> <x-logistics.card>
<x-logistics.section-header title="document_list" description="Rechercher des documents" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="jnl,number,thirdid,date"
/>
<x-logistics.form-field
wire:model="thirdId"
label="Identifiant tiers (thirdid)"
id="thirdId"
placeholder="Ex: CUST001"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="searchDocuments" icon="heroicon-o-magnifying-glass">
Rechercher
</x-filament::button>
<div wire:loading wire:target="searchDocuments" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Recherche en cours...</span>
</div>
</div> </div>
</div> </div>
</div> </x-logistics.card>
</x-logistics.card>
{{-- Resultats --}} @if (count($data) > 0)
@if (count($data) > 0) <x-logistics.card>
<x-logistics.section-header title="Résultats document_list">
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6">
<x-logistics.data-table :data="$data" />
</div>
</x-logistics.card>
@endif
{{-- document_detail --}}
<x-logistics.card> <x-logistics.card>
<x-logistics.section-header title="Résultats"> <x-logistics.section-header title="document_detail" description="Détail complet d'un document" />
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6"> <div class="p-6">
<x-logistics.data-table :data="$data" /> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="detailJnl"
label="Code journal (jnl)"
id="detailJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="detailNumber"
label="Numéro de document (number)"
id="detailNumber"
placeholder="Ex: 2026/0001"
/>
<div class="flex items-end">
<x-filament::button wire:click="getDocumentDetail" icon="heroicon-o-eye">
Voir le détail
</x-filament::button>
</div>
</div>
<div wire:loading wire:target="getDocumentDetail" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getDocumentDetail" class="mt-4">
<x-logistics.json-block :data="$detailData" />
</div>
</div>
</x-logistics.card>
{{-- Document_GetStatusList --}}
<x-logistics.card>
<x-logistics.section-header title="Document_GetStatusList" description="Liste des statuts disponibles pour un journal" />
<div class="p-6">
<div class="flex items-end gap-4">
<div class="flex-1">
<x-logistics.form-field
wire:model="statusJnl"
label="Code journal (jnl)"
id="statusJnl"
placeholder="Ex: VEN"
/>
</div>
<x-filament::button wire:click="getStatusList" icon="heroicon-o-list-bullet">
Obtenir les statuts
</x-filament::button>
</div>
<div wire:loading wire:target="getStatusList" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getStatusList" class="mt-4">
<x-logistics.json-block :data="$statusData" />
</div>
</div>
</x-logistics.card>
{{-- Document_GetUnitPriceAndVat --}}
<x-logistics.card>
<x-logistics.section-header title="Document_GetUnitPriceAndVat" description="Prix unitaire et TVA d'un article dans un contexte donné" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="priceArtId"
label="Article (ARTID)"
id="priceArtId"
placeholder="Ex: ART001"
/>
<x-logistics.form-field
wire:model="priceQty"
label="Quantité (QTY, format string)"
id="priceQty"
placeholder="Ex: 2"
/>
<x-logistics.form-field
wire:model="priceJnl"
label="Code journal (JNL)"
id="priceJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="priceThirdId"
label="Identifiant tiers (THIRDID)"
id="priceThirdId"
placeholder="Ex: CUST001"
/>
<x-logistics.form-field
wire:model="priceDate"
label="Date (DATE)"
id="priceDate"
placeholder="Ex: 2026-02-20"
/>
<div class="flex items-end">
<x-filament::button wire:click="getUnitPriceAndVat" icon="heroicon-o-calculator">
Calculer
</x-filament::button>
</div>
</div>
<div wire:loading wire:target="getUnitPriceAndVat" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getUnitPriceAndVat" class="mt-4">
<x-logistics.json-block :data="$priceData" />
</div>
</div>
</x-logistics.card>
{{-- Document_GetDueDate --}}
<x-logistics.card>
<x-logistics.section-header title="Document_GetDueDate" description="Calcul de la date d'échéance de paiement" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="payDelay"
label="Délai de paiement (paydelay)"
id="payDelay"
placeholder="Ex: 30J"
/>
<x-logistics.form-field
wire:model="dueDateInput"
label="Date de départ (date)"
id="dueDateInput"
placeholder="Ex: 2026-02-20"
/>
<div class="flex items-end">
<x-filament::button wire:click="getDueDate" icon="heroicon-o-calendar-days">
Calculer l'échéance
</x-filament::button>
</div>
</div>
<div wire:loading wire:target="getDueDate" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getDueDate" class="mt-4">
<x-logistics.json-block :data="$dueDateData" />
</div>
</div>
</x-logistics.card>
{{-- Document_GetAttachListThumbnail --}}
<x-logistics.card>
<x-logistics.section-header title="Document_GetAttachListThumbnail" description="Miniatures des fichiers images attachés à un document" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="attachJnl"
label="Code journal (JNL)"
id="attachJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="attachNumber"
label="Numéro de document (NUMBER)"
id="attachNumber"
placeholder="Ex: 2026/0001"
/>
<div class="flex items-end">
<x-filament::button wire:click="getAttachListThumbnail" icon="heroicon-o-photo">
Voir les miniatures
</x-filament::button>
</div>
</div>
<div wire:loading wire:target="getAttachListThumbnail" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getAttachListThumbnail" class="mt-4">
<x-logistics.json-block :data="$attachData" />
</div>
</div>
</x-logistics.card>
{{-- Document_GetPDF --}}
<x-logistics.card>
<x-logistics.section-header title="Document_GetPDF" description="Génération de PDF d'un document" />
<div class="p-6">
<div class="mb-4 rounded-lg bg-amber-50 p-3 text-sm text-amber-700 dark:bg-amber-400/10 dark:text-amber-400">
Endpoint non fonctionnel -- la valeur attendue du paramètre LAYOUT est inconnue.
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<x-logistics.form-field
wire:model="pdfJnl"
label="Code journal (JNL)"
id="pdfJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="pdfNumber"
label="Numéro de document (NUMBER)"
id="pdfNumber"
placeholder="Ex: 2026/0001"
/>
<x-logistics.form-field
wire:model="pdfLayout"
label="Mise en page (LAYOUT)"
id="pdfLayout"
placeholder="Valeur inconnue"
/>
<div class="flex items-end">
<x-filament::button wire:click="getPdf" icon="heroicon-o-document-arrow-down">
Générer le PDF
</x-filament::button>
</div>
</div>
<div wire:loading wire:target="getPdf" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getPdf" class="mt-4">
<x-logistics.json-block :data="$pdfData" />
</div>
</div>
</x-logistics.card>
@else
{{-- ============================================================ --}}
{{-- MODE ECRITURE --}}
{{-- ============================================================ --}}
{{-- document_add --}}
<x-logistics.card>
<x-logistics.section-header title="document_add" description="Créer un nouveau document commercial" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="addThirdId"
label="Identifiant tiers (ThirdId)"
id="addThirdId"
placeholder="Ex: CUST001"
/>
<x-logistics.form-field
wire:model="addDate"
label="Date d'encodage (Date)"
id="addDate"
placeholder="Ex: 2026-02-20"
/>
<x-logistics.form-field
wire:model="addJnl"
label="Code journal (JNL)"
id="addJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="addArtIds"
label="Articles (Artid, séparés par virgule)"
id="addArtIds"
placeholder="Ex: ART001,ART002"
/>
<x-logistics.form-field
wire:model="addQty"
label="Quantités (Qty, séparées par virgule)"
id="addQty"
placeholder="Ex: 2,5"
/>
<x-logistics.form-field
wire:model="addSalePrice"
label="Prix unitaires (Saleprice, séparés par virgule)"
id="addSalePrice"
placeholder="Ex: 10.00,25.50"
/>
<x-logistics.form-field
wire:model="addDiscount"
label="Réductions (Discount, optionnel)"
id="addDiscount"
placeholder="Ex: 0,10"
/>
<x-logistics.form-field
wire:model="addVatId"
label="Codes TVA (Vatid, optionnel)"
id="addVatId"
placeholder="Ex: TVA21,TVA21"
/>
<x-logistics.form-field
wire:model="addVatPc"
label="% TVA (Vatpc, optionnel)"
id="addVatPc"
placeholder="Ex: 21,21"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="addDocument" icon="heroicon-o-plus-circle" color="success">
Créer le document
</x-filament::button>
<div wire:loading wire:target="addDocument" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Envoi en cours...</span>
</div>
</div>
<div wire:loading.remove wire:target="addDocument" class="mt-4">
<x-logistics.json-block :data="$addResult" />
</div>
</div>
</x-logistics.card>
{{-- document_mod --}}
<x-logistics.card>
<x-logistics.section-header title="document_mod" description="Modifier un document existant" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="modNumber"
label="Numéro de document (number)"
id="modNumber"
placeholder="Ex: 2026/0001"
/>
<x-logistics.form-field
wire:model="modJnl"
label="Code journal (JNL)"
id="modJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="modThirdId"
label="Identifiant tiers (Thirdid, optionnel)"
id="modThirdId"
placeholder="Ex: CUST001"
/>
<x-logistics.form-field
wire:model="modArtIds"
label="Articles (Artid, séparés par virgule, optionnel)"
id="modArtIds"
placeholder="Ex: ART001,ART003"
/>
<x-logistics.form-field
wire:model="modQty"
label="Quantités (Qty, séparées par virgule, optionnel)"
id="modQty"
placeholder="Ex: 3,1"
/>
<x-logistics.form-field
wire:model="modSalePrice"
label="Prix unitaires (Saleprice, séparés par virgule, optionnel)"
id="modSalePrice"
placeholder="Ex: 10.00,50.00"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="modDocument" icon="heroicon-o-pencil-square" color="warning">
Modifier le document
</x-filament::button>
<div wire:loading wire:target="modDocument" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Envoi en cours...</span>
</div>
</div>
<div wire:loading.remove wire:target="modDocument" class="mt-4">
<x-logistics.json-block :data="$modResult" />
</div>
</div> </div>
</x-logistics.card> </x-logistics.card>
@endif @endif
{{-- Detail d'un document --}}
<x-logistics.card>
<x-logistics.section-header title="Détail d'un document" />
<div class="p-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-logistics.form-field
wire:model="detailJnl"
label="Code journal (jnl)"
id="detailJnl"
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="detailNumber"
label="Numéro de document"
id="detailNumber"
placeholder="Ex: 1"
/>
<div class="flex items-end">
<x-filament::button wire:click="getDocumentDetail" icon="heroicon-o-eye">
Voir le détail
</x-filament::button>
</div>
</div>
<div wire:loading wire:target="getDocumentDetail" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getDocumentDetail" class="mt-4">
<x-logistics.json-block :data="$detailData" />
</div>
</div>
</x-logistics.card>
</x-filament-panels::page> </x-filament-panels::page>

View File

@@ -1,59 +1,80 @@
<x-filament-panels::page> <x-filament-panels::page>
<x-logistics.error-banner :message="$errorMessage" /> <x-logistics.error-banner :message="$errorMessage" />
{{-- Formulaire de recherche --}} {{-- Toggle Lecture / Ecriture --}}
<x-logistics.card> <div class="flex gap-2">
<x-logistics.section-header title="Rechercher des journaux" /> <x-filament::button wire:click="$set('mode', 'read')" :color="$mode === 'read' ? 'primary' : 'gray'" icon="heroicon-o-arrow-down-tray">
<div class="p-6"> Lecture
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> </x-filament::button>
<x-logistics.form-field <x-filament::button wire:click="$set('mode', 'write')" :color="$mode === 'write' ? 'primary' : 'gray'" icon="heroicon-o-arrow-up-tray">
wire:model="type" Ecriture
label="Type de journal (TYPE)" </x-filament::button>
id="type" </div>
placeholder="Ex: VEN"
/>
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="Ex: jnlid,jnlname"
/>
<x-logistics.form-field
wire:model="results"
label="Nombre de résultats"
id="results"
type="number"
min="1"
max="100"
/>
</div>
<div class="mt-4 flex items-center gap-3"> @if ($mode === 'read')
<x-filament::button wire:click="searchJournaux" icon="heroicon-o-magnifying-glass"> {{-- jnl_list --}}
Rechercher <x-logistics.card>
</x-filament::button> <x-logistics.section-header title="jnl_list" description="Rechercher des journaux" />
<div wire:loading wire:target="searchJournaux" class="flex items-center gap-2"> <div class="p-6">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" /> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<span class="text-sm text-gray-500">Recherche en cours...</span> <x-logistics.form-field
wire:model="type"
label="Type de journal (TYPE)"
id="type"
placeholder="Ex: V"
/>
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="Ex: jnlid,jnlname"
/>
<x-logistics.form-field
wire:model="results"
label="Nombre de résultats (results)"
id="results"
type="number"
min="1"
max="100"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="searchJournaux" icon="heroicon-o-magnifying-glass">
Rechercher
</x-filament::button>
<div wire:loading wire:target="searchJournaux" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Recherche en cours...</span>
</div>
</div> </div>
</div> </div>
</div> </x-logistics.card>
</x-logistics.card>
{{-- Resultats --}} @if (count($data) > 0)
@if (count($data) > 0) <x-logistics.card>
<x-logistics.section-header title="Résultats jnl_list">
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6">
<x-logistics.data-table :data="$data" />
</div>
</x-logistics.card>
@endif
@else
<x-logistics.card> <x-logistics.card>
<x-logistics.section-header title="Résultats">
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6"> <div class="p-6">
<x-logistics.data-table :data="$data" /> <x-logistics.empty-state
icon="heroicon-o-arrow-up-tray"
title="Aucun endpoint d'écriture disponible"
description="La section Journaux ne dispose pas d'endpoints d'envoi de données."
/>
</div> </div>
</x-logistics.card> </x-logistics.card>
@endif @endif

View File

@@ -1,89 +1,110 @@
<x-filament-panels::page> <x-filament-panels::page>
<x-logistics.error-banner :message="$errorMessage" /> <x-logistics.error-banner :message="$errorMessage" />
{{-- Formulaire de recherche --}} {{-- Toggle Lecture / Ecriture --}}
<x-logistics.card> <div class="flex gap-2">
<x-logistics.section-header title="Rechercher des tiers" /> <x-filament::button wire:click="$set('mode', 'read')" :color="$mode === 'read' ? 'primary' : 'gray'" icon="heroicon-o-arrow-down-tray">
<div class="p-6"> Lecture
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> </x-filament::button>
<x-logistics.form-field <x-filament::button wire:click="$set('mode', 'write')" :color="$mode === 'write' ? 'primary' : 'gray'" icon="heroicon-o-arrow-up-tray">
wire:model="search" Ecriture
label="Recherche (obligatoire)" </x-filament::button>
id="search" </div>
placeholder="Filtre de recherche..."
/>
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="custid,custname"
/>
<x-logistics.form-field
wire:model="results"
label="Nombre de résultats"
id="results"
type="number"
min="1"
max="100"
/>
</div>
<div class="mt-4 flex items-center gap-3"> @if ($mode === 'read')
<x-filament::button wire:click="searchTiers" icon="heroicon-o-magnifying-glass"> {{-- third_list --}}
Rechercher <x-logistics.card>
</x-filament::button> <x-logistics.section-header title="third_list" description="Rechercher des tiers" />
<div wire:loading wire:target="searchTiers" class="flex items-center gap-2"> <div class="p-6">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" /> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<span class="text-sm text-gray-500">Recherche en cours...</span> <x-logistics.form-field
wire:model="search"
label="Recherche (search, obligatoire)"
id="search"
placeholder="Filtre de recherche..."
/>
<x-logistics.form-field
wire:model="select"
label="Colonnes (select)"
id="select"
placeholder="custid,custname"
/>
<x-logistics.form-field
wire:model="results"
label="Nombre de résultats (results)"
id="results"
type="number"
min="1"
max="100"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<x-filament::button wire:click="searchTiers" icon="heroicon-o-magnifying-glass">
Rechercher
</x-filament::button>
<div wire:loading wire:target="searchTiers" class="flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Recherche en cours...</span>
</div>
</div> </div>
</div> </div>
</div> </x-logistics.card>
</x-logistics.card>
{{-- Resultats --}} @if (count($data) > 0)
@if (count($data) > 0) <x-logistics.card>
<x-logistics.section-header title="Résultats third_list">
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6">
<x-logistics.data-table :data="$data" />
</div>
</x-logistics.card>
@endif
{{-- third_GetArtHistory --}}
<x-logistics.card> <x-logistics.card>
<x-logistics.section-header title="Résultats"> <x-logistics.section-header title="third_GetArtHistory" description="Historique des articles d'un tiers" />
<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">
{{ $metadata['rowcount'] ?? 0 }} résultat(s)
</span>
@endif
</x-slot:actions>
</x-logistics.section-header>
<div class="p-6"> <div class="p-6">
<x-logistics.data-table :data="$data" /> <div class="flex items-end gap-4">
<div class="flex-1">
<x-logistics.form-field
wire:model="historyThirdId"
label="Identifiant tiers (thirdid)"
id="historyThirdId"
placeholder="Ex: CUST001"
/>
</div>
<x-filament::button wire:click="getArtHistory" icon="heroicon-o-clock">
Voir l'historique
</x-filament::button>
</div>
<div wire:loading wire:target="getArtHistory" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getArtHistory" class="mt-4">
<x-logistics.json-block :data="$historyData" />
</div>
</div>
</x-logistics.card>
@else
<x-logistics.card>
<div class="p-6">
<x-logistics.empty-state
icon="heroicon-o-arrow-up-tray"
title="Aucun endpoint d'écriture disponible"
description="La section Tiers ne dispose pas d'endpoints d'envoi de données."
/>
</div> </div>
</x-logistics.card> </x-logistics.card>
@endif @endif
{{-- Historique des articles d'un tiers --}}
<x-logistics.card>
<x-logistics.section-header title="Historique des articles d'un tiers" />
<div class="p-6">
<div class="flex items-end gap-4">
<div class="flex-1">
<x-logistics.form-field
wire:model="historyThirdId"
label="Identifiant tiers (thirdid)"
id="historyThirdId"
placeholder="Ex: CUST001"
/>
</div>
<x-filament::button wire:click="getArtHistory" icon="heroicon-o-clock">
Voir l'historique
</x-filament::button>
</div>
<div wire:loading wire:target="getArtHistory" class="mt-4 flex items-center gap-2">
<x-filament::loading-indicator class="h-4 w-4 text-primary-500" />
<span class="text-sm text-gray-500">Chargement...</span>
</div>
<div wire:loading.remove wire:target="getArtHistory" class="mt-4">
<x-logistics.json-block :data="$historyData" />
</div>
</div>
</x-logistics.card>
</x-filament-panels::page> </x-filament-panels::page>

View File

@@ -0,0 +1,129 @@
<?php
use App\Filament\Pages\Divers;
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(Divers::class)
->assertSet('mode', 'read');
});
it('can switch between read and write modes', function () {
Livewire::test(Divers::class)
->set('mode', 'write')
->assertSet('mode', 'write')
->set('mode', 'read')
->assertSet('mode', 'read');
});
it('gets the serial number', function () {
Http::fake([
'*/getserialnumber' => Http::response([
'data' => 'SN-12345',
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Divers::class)
->call('getSerialNumber')
->assertSet('serialData', ['value' => 'SN-12345']);
});
it('searches codes by prefix', function () {
Http::fake([
'*/codes_list' => Http::response([
'data' => [['code' => 'PAY01', 'vala1' => 'Comptant']],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Divers::class)
->set('code', 'PAY')
->call('searchCodes')
->assertSet('codesData', [['code' => 'PAY01', 'vala1' => 'Comptant']]);
Http::assertSent(function ($request) {
return str_contains($request->url(), 'codes_list')
&& $request->data()['code'] === 'PAY';
});
});
it('does not call searchCodes when code is empty', function () {
Http::fake();
Livewire::test(Divers::class)
->call('searchCodes')
->assertSet('codesData', []);
Http::assertNothingSent();
});
it('calls custom_geninv_updatestock endpoint', function () {
Http::fake([
'*/custom_geninv_updatestock' => Http::response([
'data' => null,
'metadata' => ['rowcount' => 0, 'issuccess' => false],
'error' => 'Unknown STKID',
]),
]);
Livewire::test(Divers::class)
->set('mode', 'write')
->set('stkArtId', 'ART001')
->set('stkId', 'STK1')
->set('stkQty', '10')
->call('updateStock')
->assertSet('errorMessage', 'Unknown STKID');
Http::assertSent(function ($request) {
$body = $request->data();
return str_contains($request->url(), 'custom_geninv_updatestock')
&& $body['ARTID'] === 'ART001'
&& $body['STKID'] === 'STK1';
});
});
it('does not call updateStock when required fields are empty', function () {
Http::fake();
Livewire::test(Divers::class)
->set('mode', 'write')
->call('updateStock')
->assertSet('updateStockResult', []);
Http::assertNothingSent();
});
it('displays error message on API failure', function () {
Http::fake([
'*/codes_list' => Http::response([
'data' => null,
'metadata' => ['rowcount' => 0, 'issuccess' => false],
'error' => 'Invalid API key',
]),
]);
Livewire::test(Divers::class)
->set('code', 'TEST')
->call('searchCodes')
->assertSet('errorMessage', 'Invalid API key');
});

View File

@@ -0,0 +1,234 @@
<?php
use App\Filament\Pages\Documents;
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 () {
Http::fake(['*' => Http::response(['data' => [], 'metadata' => [], 'error' => null])]);
Livewire::test(Documents::class)
->assertSet('mode', 'read');
});
it('can switch between read and write modes', function () {
Http::fake(['*' => Http::response(['data' => [], 'metadata' => [], 'error' => null])]);
Livewire::test(Documents::class)
->set('mode', 'write')
->assertSet('mode', 'write')
->set('mode', 'read')
->assertSet('mode', 'read');
});
it('searches documents via document_list', function () {
Http::fake([
'*/document_list' => Http::response([
'data' => [['jnl' => 'VEN', 'number' => '1']],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('select', 'jnl,number')
->set('thirdId', 'CUST001')
->call('searchDocuments')
->assertSet('data', [['jnl' => 'VEN', 'number' => '1']]);
});
it('gets document detail', function () {
Http::fake([
'*/document_detail' => Http::response([
'data' => ['jnl' => 'VEN', 'number' => '1', 'thirdid' => 'CUST001'],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('detailJnl', 'VEN')
->set('detailNumber', '1')
->call('getDocumentDetail')
->assertSet('detailData', ['jnl' => 'VEN', 'number' => '1', 'thirdid' => 'CUST001']);
});
it('gets status list for a journal', function () {
Http::fake([
'*/Document_GetStatusList' => Http::response([
'data' => [['status' => 'DRAFT'], ['status' => 'VALID']],
'metadata' => ['rowcount' => 2, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('statusJnl', 'VEN')
->call('getStatusList')
->assertSet('statusData', [['status' => 'DRAFT'], ['status' => 'VALID']]);
});
it('gets unit price and vat', function () {
Http::fake([
'*/Document_GetUnitPriceAndVat' => Http::response([
'data' => ['price' => '10.00', 'vat' => '21'],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('priceArtId', 'ART001')
->set('priceQty', '2')
->set('priceJnl', 'VEN')
->set('priceThirdId', 'CUST001')
->set('priceDate', '2026-02-20')
->call('getUnitPriceAndVat')
->assertSet('priceData', ['price' => '10.00', 'vat' => '21']);
});
it('gets due date', function () {
Http::fake([
'*/Document_GetDueDate' => Http::response([
'data' => ['duedate' => '2026-03-22'],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('payDelay', '30J')
->set('dueDateInput', '2026-02-20')
->call('getDueDate')
->assertSet('dueDateData', ['duedate' => '2026-03-22']);
});
it('gets attach list thumbnail', function () {
Http::fake([
'*/Document_GetAttachListThumbnail' => Http::response([
'data' => [['filename' => 'photo.jpg']],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('attachJnl', 'VEN')
->set('attachNumber', '2026/0001')
->call('getAttachListThumbnail')
->assertSet('attachData', [['filename' => 'photo.jpg']]);
});
it('calls Document_GetPDF endpoint', function () {
Http::fake([
'*/Document_GetPDF' => Http::response([
'data' => null,
'metadata' => ['rowcount' => 0, 'issuccess' => false],
'error' => 'Layout not found',
]),
]);
Livewire::test(Documents::class)
->set('pdfJnl', 'VEN')
->set('pdfNumber', '2026/0001')
->set('pdfLayout', 'DEFAULT')
->call('getPdf')
->assertSet('errorMessage', 'Layout not found');
});
it('adds a document via document_add', function () {
Http::fake([
'*/document_add' => Http::response([
'data' => ['number' => '2026/0002'],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('mode', 'write')
->set('addThirdId', 'CUST001')
->set('addDate', '2026-02-20')
->set('addArtIds', 'ART001,ART002')
->set('addQty', '2,5')
->set('addSalePrice', '10.00,25.50')
->set('addJnl', 'VEN')
->call('addDocument')
->assertSet('addResult', ['number' => '2026/0002']);
Http::assertSent(function ($request) {
$body = $request->data();
return str_contains($request->url(), 'document_add')
&& $body['Artid'] === ['ART001', 'ART002']
&& $body['Qty'] === ['2', '5']
&& $body['Saleprice'] === ['10.00', '25.50'];
});
});
it('modifies a document via document_mod', function () {
Http::fake([
'*/document_mod' => Http::response([
'data' => ['updated' => true],
'metadata' => ['rowcount' => 1, 'issuccess' => true],
'error' => null,
]),
]);
Livewire::test(Documents::class)
->set('mode', 'write')
->set('modNumber', '2026/0001')
->set('modJnl', 'VEN')
->set('modArtIds', 'ART001,ART003')
->set('modQty', '3,1')
->set('modSalePrice', '10.00,50.00')
->call('modDocument')
->assertSet('modResult', ['updated' => true]);
Http::assertSent(function ($request) {
$body = $request->data();
return str_contains($request->url(), 'document_mod')
&& $body['number'] === '2026/0001'
&& $body['JNL'] === 'VEN';
});
});
it('does not call getStatusList when statusJnl is empty', function () {
Http::fake();
Livewire::test(Documents::class)
->call('getStatusList')
->assertSet('statusData', []);
Http::assertNothingSent();
});
it('displays error message on API failure', function () {
Http::fake([
'*/document_list' => Http::response([
'data' => null,
'metadata' => ['rowcount' => 0, 'issuccess' => false],
'error' => 'Invalid API key',
]),
]);
Livewire::test(Documents::class)
->call('searchDocuments')
->assertSet('errorMessage', 'Invalid API key');
});

View File

@@ -168,6 +168,43 @@ it('logs failed requests to api_request_logs as valid JSON', function () {
->and($decoded)->toHaveKey('error'); ->and($decoded)->toHaveKey('error');
}); });
it('sends correct parameters for Document_GetPDF', function () {
Http::fake([
'*' => Http::response(['data' => null, 'metadata' => ['rowcount' => 0, 'issuccess' => false], 'error' => 'Layout not found']),
]);
$service = app(LogisticsService::class);
$service->documentGetPdf('VEN', '2026/0001', 'DEFAULT');
Http::assertSent(function ($request) {
$body = $request->data();
return str_contains($request->url(), 'Document_GetPDF')
&& $body['JNL'] === 'VEN'
&& $body['NUMBER'] === '2026/0001'
&& $body['LAYOUT'] === 'DEFAULT';
});
});
it('sends correct parameters for custom_geninv_updatestock', function () {
Http::fake([
'*' => Http::response(['data' => null, 'metadata' => ['rowcount' => 0, 'issuccess' => false], 'error' => 'Unknown STKID']),
]);
$service = app(LogisticsService::class);
$params = ['ARTID' => 'ART001', 'STKID' => 'STK1', 'QTY' => '10', 'TOCHECK' => '5', 'TOCHECKDETAIL' => 'test', 'MODE' => '1'];
$service->customGeninvUpdatestock($params);
Http::assertSent(function ($request) {
$body = $request->data();
return str_contains($request->url(), 'custom_geninv_updatestock')
&& $body['ARTID'] === 'ART001'
&& $body['STKID'] === 'STK1'
&& $body['QTY'] === '10';
});
});
it('includes endpoint info in LogisticsApiException', function () { it('includes endpoint info in LogisticsApiException', function () {
Http::fake(fn () => throw new ConnectionException('Connection timed out')); Http::fake(fn () => throw new ConnectionException('Connection timed out'));