Merge branch 'dillon/122-1688-po-automation-project' into vapor/development

This commit is contained in:
Dillon Ngo
2025-12-27 15:00:12 +08:00
47 changed files with 64257 additions and 36 deletions
+2
View File
@@ -80,3 +80,5 @@ SENDING_EMAIL_WELCOME_VOUCHER_ENABLED=false
E_INVOICE_START_DATE="2025-07-01 00:00:00"
MAINTENANCE_MESSAGE_TITLE="We'll be back online on 00:00 1/7/2025"
MAINTENANCE_MESSAGE="Sorry for the inconvenience but we're performing some maintenance at the moment."
OPENAI_API_KEY=""
@@ -0,0 +1,21 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use App\Classes\ValueObjects\Constants\KVPKey;
use Illuminate\Database\Eloquent\Builder;
class ChatgptPrompt implements Filter
{
public static function apply(Builder $builder, $value)
{
return $builder->where(
'key',
'LIKE',
KVPKey::CHATGPT_PROMPT_PREFIX . '%'
);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Classes\Jobs\Commands\V2;
use App\Classes\Modules\Transactions\Processors\ETLPurchaseOrderTransactionProcessor;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ETLPurchaseOrderTransactionV2CommandJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var array */
private $products;
/** @var array */
private $productsMetadata;
/** @var int|null */
private $bookingId;
/**
* ETLPurchaseOrderTransactionV2CommandJob constructor.
* @param array $products
* @param array $productsMetadata
* @param int $bookingId
*/
public function __construct(array $products, array $productsMetadata, ?int $bookingId)
{
$this->products = $products;
$this->productsMetadata = $productsMetadata;
$this->bookingId = $bookingId;
}
public function handle()
{
Log::info(Carbon::now() . ': Start job - ETL Purchase Order Transaction.');
$start = new Carbon();
(App()->make(ETLPurchaseOrderTransactionProcessor::class))->execute($this->products, $this->productsMetadata, $this->bookingId);
$end = new Carbon();
$elapsedTime = $start->diff($end)->format('%H:%I:%S');
Log::info(Carbon::now() . ': End job - ETL Purchase Order Transaction. ElapsedTime: ' . $elapsedTime . '.');
}
}
@@ -17,12 +17,19 @@ class CreatesKeyValuePair extends AbstractUpdateRelationshipRecord
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute(KeyValueInterface $kv, KeyValuePairObject $object) {
public function execute(?KeyValueInterface $kv, KeyValuePairObject $object) {
$model = new KeyValuePair();
$model->key = $object->getKey();
$model->value = $object->getValue();
return $this->handler($kv->attributesKVP(), $model);
// Owner-less
if (is_null($kv)) {
$model->owner_type = null;
$model->owner_id = null;
$model->save();
return $model;
}
return $this->handler($kv->attributesKVP(), $model);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\Accounts\Services;
use App\Classes\General\Eloquent\AbstractFetchRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\KeyValuePair;
class FetchesKeyValuePair extends AbstractFetchRecord
{
/** @var KeyValuePair */
private $repository;
/**
* FetchesKeyValuePair constructor.
* @param KeyValuePair $repository
*/
public function __construct(KeyValuePair $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Accounts\DataTransferObjects\KeyValuePairObject;
use App\Classes\Modules\Accounts\Services\CreatesKeyValuePair;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\CreateChatGPTPromptDTO;
use App\Classes\Modules\KeyValuePairs\Standards\Rules\CanCreateChatGPTPrompt;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Http\Resources\ChatGPTPromptResource;
use ErrorException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateChatGPTPromptKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Created ChatGPT Prompt',
'message' => 'You have successfully created a new ChatGPT Prompt'
];
}
/** @var CanCreateChatGPTPrompt */
private $canCreateChatGPTPrompt;
/** @var CreatesKeyValuePair */
private $createsKeyValuePair;
/**
* CreateChatGPTPromptKVPLogic constructor.
* @param CanCreateChatGPTPrompt $canCreateChatGPTPrompt
* @param CreatesKeyValuePair $createsKeyValuePair
*/
public function __construct(
CanCreateChatGPTPrompt $canCreateChatGPTPrompt,
CreatesKeyValuePair $createsKeyValuePair
) {
$this->canCreateChatGPTPrompt = $canCreateChatGPTPrompt;
$this->createsKeyValuePair = $createsKeyValuePair;
}
/**
* @param Request $request
* @return JsonResponse
* @throws \App\Classes\Exceptions\AccessForbiddenException
* @throws \App\Classes\Exceptions\MalformedRequestException
* @throws \App\Classes\Exceptions\RequestValidationException
*/
public function logic(Request $request): JsonResponse
{
$dto = new CreateChatGPTPromptDTO($request->all());
$this->canCreateChatGPTPrompt->passes($dto);
$key = KVPKey::CHATGPT_PROMPT_PREFIX . strtoupper(str_replace(' ', '_', $dto->getTitle()));
$keyValuePairObject = new KeyValuePairObject($key, $dto->getPrompt());
$kvp = $this->createsKeyValuePair->execute(null, $keyValuePairObject);
return $this->resourceResponse(new ChatGPTPromptResource($kvp));
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Accounts\Services\DeletesKeyValuePair;
use App\Classes\Modules\Accounts\Services\FetchesKeyValuePair;
use App\Classes\Modules\KeyValuePairs\Standards\Rules\CanDeleteChatGPTPrompt;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DeleteChatGPTPromptKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Delete ChatGPT Prompt',
'message' => 'You have successfully deleted the ChatGPT Prompt'
];
}
/** @var CanDeleteChatGPTPrompt */
private $canDeleteChatGPTPrompt;
/** @var FetchesKeyValuePair */
private $fetchesKeyValuePair;
/** @var DeletesKeyValuePair */
private $deletesKeyValuePair;
/**
* DeleteChatGPTPromptKVPLogic constructor.
* @param CanDeleteChatGPTPrompt $canDeleteChatGPTPrompt
* @param FetchesKeyValuePair $fetchesKeyValuePair
* @param DeletesKeyValuePair $deletesKeyValuePair
*/
public function __construct(
CanDeleteChatGPTPrompt $canDeleteChatGPTPrompt,
FetchesKeyValuePair $fetchesKeyValuePair,
DeletesKeyValuePair $deletesKeyValuePair
) {
$this->canDeleteChatGPTPrompt = $canDeleteChatGPTPrompt;
$this->fetchesKeyValuePair = $fetchesKeyValuePair;
$this->deletesKeyValuePair = $deletesKeyValuePair;
}
/**
* @param Request $request
* @return JsonResponse
* @throws \App\Classes\Exceptions\AccessForbiddenException
* @throws \App\Classes\Exceptions\MalformedRequestException
* @throws \App\Classes\Exceptions\RequestValidationException
*/
public function logic(Request $request): JsonResponse
{
$this->canDeleteChatGPTPrompt->passes();
$kvp = $this->fetchesKeyValuePair->execute(['id' => $request->route('id')]);
$this->deletesKeyValuePair->execute($kvp);
return $this->response([]);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\KeyValuePairs\Services\ListsKeyValuePairs;
use App\Classes\Modules\KeyValuePairs\Standards\Rules\CanListChatGPTPrompts;
use App\Http\Resources\ChatGPTPromptResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListChatGPTPromptsKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Retrieve ChatGPT Prompts',
'message' => 'You have successfully retrieved a list of ChatGPT Prompts'
];
}
/** @var CanListChatGPTPrompts */
private $canListChatGPTPrompts;
/** @var ListsKeyValuePairs */
private $listsKeyValuePairs;
/**
* ListChatGPTPromptsKVPLogic constructor.
* @param CanListChatGPTPrompts $canListChatGPTPrompts
* @param ListsKeyValuePairs $listsKeyValuePairs
*/
public function __construct(CanListChatGPTPrompts $canListChatGPTPrompts, ListsKeyValuePairs $listsKeyValuePairs)
{
$this->canListChatGPTPrompts = $canListChatGPTPrompts;
$this->listsKeyValuePairs = $listsKeyValuePairs;
}
/**
* @param Request $request
* @return JsonResponse
* @throws \App\Classes\Exceptions\AccessForbiddenException
* @throws \App\Classes\Exceptions\MalformedRequestException
* @throws \App\Classes\Exceptions\RequestValidationException
*/
public function logic(Request $request): JsonResponse
{
$this->canListChatGPTPrompts->passes();
$prompts = $this->listsKeyValuePairs->execute(array_merge($this->listsKeyValuePairs->deserializeFilters($request->input('filters')), ['chatgpt_prompt' => false]));
return $this->collectionResponse(ChatGPTPromptResource::collection($prompts));
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Accounts\DataTransferObjects\KeyValuePairObject;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\UpdateChatGPTPromptDTO;
use App\Classes\Modules\KeyValuePairs\Standards\Rules\CanUpdateChatGPTPrompt;
use App\Classes\Modules\Accounts\Services\FetchesKeyValuePair;
use App\Classes\Modules\Accounts\Services\UpdatesKeyValuePair;
use App\Http\Resources\ChatGPTPromptResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateChatGPTPromptKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => 'Updated ChatGPT Prompt',
'message' => 'You have successfully updated the ChatGPT prompt'
];
}
/** @var CanUpdateChatGPTPrompt */
private $canUpdateChatGPTPrompt;
/** @var UpdatesKeyValuePair */
private $updatesKeyValuePair;
/** @var FetchesKeyValuePair */
private $fetchesKeyValuePair;
/**
* UpdateChatGPTPromptKVPLogic constructor.
* @param CanUpdateChatGPTPrompt $canUpdateChatGPTPrompt
* @param UpdatesKeyValuePair $updatesKeyValuePair
* @param FetchesKeyValuePair $fetchesKeyValuePair
*/
public function __construct(CanUpdateChatGPTPrompt $canUpdateChatGPTPrompt, UpdatesKeyValuePair $updatesKeyValuePair, FetchesKeyValuePair $fetchesKeyValuePair)
{
$this->canUpdateChatGPTPrompt = $canUpdateChatGPTPrompt;
$this->updatesKeyValuePair = $updatesKeyValuePair;
$this->fetchesKeyValuePair = $fetchesKeyValuePair;
}
/**
* @param Request $request
* @return JsonResponse
* @throws \App\Classes\Exceptions\AccessForbiddenException
* @throws \App\Classes\Exceptions\MalformedRequestException
* @throws \App\Classes\Exceptions\RequestValidationException
*/
public function logic(Request $request) : JsonResponse
{
$dto = new UpdateChatGPTPromptDTO($request->all());
$this->canUpdateChatGPTPrompt->passes($dto);
$kvp = $this->fetchesKeyValuePair->execute(['id' => $dto->getId()]);
$keyValuePairObject = new KeyValuePairObject($kvp->key, $dto->getPrompt());
$this->updatesKeyValuePair->execute($kvp, $keyValuePairObject);
return $this->resourceResponse(new ChatGPTPromptResource($kvp));
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class CreateChatGPTPromptDTO implements DataTransferObject
{
public string $title;
public string $prompt;
public function __construct(array $data)
{
$this->title = (string) ($data['key'] ?? '');
$this->prompt = (string) ($data['value'] ?? '');
}
public function toArray(): array
{
return [
'title' => $this->title,
'prompt' => $this->prompt,
];
}
public function getTitle(): string
{
return $this->title;
}
public function getPrompt(): string
{
return $this->prompt;
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class UpdateChatGPTPromptDTO implements DataTransferObject
{
public int $id;
public string $title;
public string $prompt;
public function __construct(array $data)
{
$this->id = (int) ($data['id'] ?? 0);
$this->title = (string) ($data['key'] ?? '');
$this->prompt = (string) ($data['value'] ?? '');
}
public function toArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'prompt' => $this->prompt,
];
}
public function getId(): int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function getPrompt(): string
{
return $this->prompt;
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Services;
use App\Classes\General\Eloquent\AbstractListRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\KeyValuePair;
class ListsKeyValuePairs extends AbstractListRecord
{
/** @var KeyValuePair */
private $repository;
/**
* ListsKeyValuePairs constructor.
* @param KeyValuePair $repository
*/
public function __construct(KeyValuePair $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\KeyValuePairs\Standards\Validators\CreateChatGPTPromptValidation;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\CreateChatGPTPromptDTO;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanCreateChatGPTPrompt extends AbstractRule
{
/** @var CreateChatGPTPromptValidation */
private $validation;
/**
* CanCreateChatGPTPrompt constructor.
* @param CreateChatGPTPromptValidation $validation
*/
public function __construct(CreateChatGPTPromptValidation $validation)
{
$this->validation = $validation;
}
/**
* @return bool
*/
protected function authorized($object): bool
{
if (in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES)) {
return true;
}
return false;
}
/**
* @param CreateChatGPTPromptDTO $object
* @return bool
* @throws \App\Classes\Exceptions\RequestValidationException
*/
protected function validators($object): bool
{
return $this->validation->validate($object);
}
/**
* @param CreateChatGPTPromptDTO $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanDeleteChatGPTPrompt extends AbstractRule
{
/**
* @return bool
*/
protected function authorized($object): bool
{
if (in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES)) {
return true;
}
return false;
}
/**
* @param $object
* @return bool
*/
protected function validators($object): bool
{
return true;
}
/**
* @param $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanListChatGPTPrompts extends AbstractRule
{
/**
* @return bool
*/
protected function authorized($object): bool
{
if (in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES)) {
return true;
}
return false;
}
/**
* @param $object
* @return bool
*/
protected function validators($object): bool
{
return true;
}
/**
* @param $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\UpdateChatGPTPromptDTO;
use App\Classes\Modules\KeyValuePairs\Standards\Validators\UpdateChatGPTPromptValidation;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanUpdateChatGPTPrompt extends AbstractRule
{
/** @var UpdateChatGPTPromptValidation */
private $validation;
/**
* CanUpdateChatGPTPrompt constructor.
* @param UpdateChatGPTPromptValidation $validation
*/
public function __construct(UpdateChatGPTPromptValidation $validation)
{
$this->validation = $validation;
}
/**
* @return bool
*/
protected function authorized($object): bool
{
if (in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES)) {
return true;
}
return false;
}
/**
* @param UpdateChatGPTPromptDTO $object
* @return bool
* @throws \App\Classes\Exceptions\RequestValidationException
*/
protected function validators($object): bool
{
return $this->validation->validate($object);
}
/**
* @param UpdateChatGPTPromptDTO $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Validators;
use App\Classes\General\Abstracts\AbstractValidation;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\UpdateChatGPTPromptDTO;
class CreateChatGPTPromptValidation extends AbstractValidation
{
/**
* @param UpdateChatGPTPromptDTO $object
* @return array
*/
protected function data($object): array
{
return [
'title' => $object->getTitle(),
'prompt' => $object->getPrompt(),
];
}
/**
* @return array
*/
protected function rules(): array
{
return [
'prompt' => 'required',
];
}
/**
* @return array
*/
protected function messages(): array
{
return [];
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Validators;
use App\Classes\General\Abstracts\AbstractValidation;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\UpdateChatGPTPromptDTO;
class UpdateChatGPTPromptValidation extends AbstractValidation
{
/**
* @param UpdateChatGPTPromptDTO $object
* @return array
*/
protected function data($object): array
{
return [
'title' => $object->getTitle(),
'prompt' => $object->getPrompt(),
];
}
/**
* @return array
*/
protected function rules(): array
{
return [
'prompt' => 'required',
];
}
/**
* @return array
*/
protected function messages(): array
{
return [];
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Classes\Modules\OpenAI\Services;
use Illuminate\Support\Facades\Http;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ConnectionErrorException;
use Illuminate\Support\Facades\Log;
class CreatesChatGPTResponse
{
/**
* @param string $userPrompt
* @return null|object
* @throws MalformedRequestException
*/
public function execute(string $userPrompt) {
try {
$data = [
'model' => 'gpt-4-turbo', //gpt-4-turbo, gpt-4o-mini
'messages' => [
[
'role' => 'user',
'content' => $userPrompt
]
]
];
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('openai.api_key')
])->post(config('openai.base_url') . '/v1/chat/completions', $data);
if ($response->successful()) {
$data = $response->json();
return $data;
} else {
Log::info('CreatesChatGPTResponse: ' . $response);
return null;
}
}
catch (\Illuminate\Http\Client\ConnectionException $exception) {
$error = 'Failed to connect to ChatGPT API';
throw new ConnectionErrorException($error, $exception->getMessage(), $userPrompt, $exception->getTraceAsString());
}
catch (\Exception $exception) {
throw new MalformedRequestException('Unable to get correct response from ChatGPT API: ' . $exception->getMessage());
}
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Classes\Modules\Transactions\ControllersLogic;
use App\Classes\Exceptions\InternalServerErrorException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Transactions\Processors\ETLPurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\Standards\Rules\CanETLPurchaseOrder;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ETLPurchaseOrderTransactionLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'ETL Purchase Order',
'message' => 'You have successfully etl purchase order document'
];
}
/** @var CanETLPurchaseOrder */
private $canETLPurchaseOrder;
/** @var ETLPurchaseOrderTransactionProcessor */
private $etlPurchaseOrderTransactionProcessor;
/**
* ETLPurchaseOrderTransactionLogic constructor.
* @param CanETLPurchaseOrder $canETLPurchaseOrder
* @param ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor
*/
public function __construct(CanETLPurchaseOrder $canETLPurchaseOrder, ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor)
{
$this->canETLPurchaseOrder = $canETLPurchaseOrder;
$this->etlPurchaseOrderTransactionProcessor = $etlPurchaseOrderTransactionProcessor;
}
/**
* @param Request $request
* @param string $id
* @return JsonResponse
* @throws \App\Classes\Exceptions\MalformedRequestException
* @throws \App\Classes\Exceptions\InternalServerErrorException
*/
public function logic(Request $request, $id = ''): JsonResponse
{
$this->canETLPurchaseOrder->passes();
if ($request->has('products') && is_array($request->input('products'))) {
$products = $request->input('products');
$productsMetadata = $request->input('productsMetadata');
}
try{
$products = $this->etlPurchaseOrderTransactionProcessor->execute($products, $productsMetadata);
} catch (\Exception $exception) {
throw new InternalServerErrorException('Something went wrong!');
}
return $this->response($products);
}
}
@@ -1,14 +1,17 @@
<?php
namespace App\Classes\Modules\Transactions\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Jobs\Commands\V2\ETLPurchaseOrderTransactionV2CommandJob;
use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject;
use App\Classes\Modules\Transactions\Processors\CreatePurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\Processors\ETLPurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\Services\GeneratesTransactionBillNumber;
use App\Classes\Modules\Transactions\Standards\Rules\CanETLPurchaseOrder;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\TransactionType;
@@ -25,13 +28,16 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
/**
* @return array
*/
protected function notification():array {
protected function notification(): array
{
return [
'title' => 'Update Purchase Order',
'message' => 'You have successfully updated you booking\'s purchase order'
'message' => $this->notificationMessage ?? "You have successfully updated your booking's purchase order",
];
}
private ?string $notificationMessage = null;
/** @var FetchesBooking */
private $fetchesBooking;
@@ -41,17 +47,27 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
/** @var CreatePurchaseOrderTransactionProcessor */
private $createPurchaseOrderTransactionProcessor;
/** @var ETLPurchaseOrderTransactionProcessor */
private $etlPurchaseOrderTransactionProcessor;
/** @var CanETLPurchaseOrder */
private $canETLPurchaseOrder;
/**
* CreatePurchaseOrderTransactionLogic constructor.
* ImportPurchaseOrderTransactionLogic constructor.
* @param FetchesBooking $fetchesBooking
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
* @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor
* @param ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor
* @param CanETLPurchaseOrder $canETLPurchaseOrder
*/
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor)
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor, CanETLPurchaseOrder $canETLPurchaseOrder)
{
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
$this->etlPurchaseOrderTransactionProcessor = $etlPurchaseOrderTransactionProcessor;
$this->canETLPurchaseOrder = $canETLPurchaseOrder;
}
/**
@@ -60,34 +76,55 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
* @return JsonResponse
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function logic(Request $request, $id = '') : JsonResponse
public function logic(Request $request, $id = ''): JsonResponse
{
$files = $request->file('files');
$products = []; // Initialize
if ($request->has('products') && is_array($request->input('products'))) {
$this->canETLPurchaseOrder->passes();
$object = new DocumentObject('', $request->input('files'), '', ApprovalStatus::APPROVED, 'imports');
foreach ($object->getFiles() as $file){
$collection = Excel::toCollection(null, json_decode($file)->file_info->original->file, null, null, true);
$products = $request->input('products');
$productsMetadata = $request->input('productsMetadata');
$sheet = $collection->first()->skip(1);
$count = collect($products)
->filter(function ($product) {
return isset($product['description']) &&
preg_match('/\p{Han}+/u', $product['description']);
})
->count();
$products = $sheet->map(function ($row) {
Log::info($row);
$stockCode = $row[0];
$description = $row[1];
$quantity = $row[2];
$unit_price = $row[3];
if($count > 10){
ETLPurchaseOrderTransactionV2CommandJob::dispatch($products, $productsMetadata, $request->route('id'));
$this->notificationMessage = "Please refresh page in a few minutes";
return $this->response([]);
}
else{
$products = $this->etlPurchaseOrderTransactionProcessor->execute($products, $productsMetadata);
}
} else {
$object = new DocumentObject('', $request->input('files'), '', ApprovalStatus::APPROVED, 'imports');
foreach ($object->getFiles() as $file) {
$collection = Excel::toCollection(null, json_decode($file)->file_info->original->file, null, null, true);
$sheet = $collection->first()->skip(1);
$products = $sheet->map(function ($row) {
Log::info($row);
$stockCode = $row[0];
$description = $row[1];
$quantity = $row[2];
$unit_price = $row[3];
return [
'stockCode' => $stockCode,
'description' => $description,
'quantity' => $quantity,
'unit_price' => $unit_price
];
})->all();
}
return [
'stockCode' => $stockCode,
'description' => $description,
'quantity' => $quantity,
'unit_price' => $unit_price
];
})->all();
}
// dd($products);
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $request->route('id') ?? $id]);
@@ -0,0 +1,186 @@
<?php
namespace App\Classes\Modules\Transactions\Processors;
use App\Classes\Exceptions\ResourceNotFoundException;
use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Transactions\Services\GeneratesTransactionBillNumber;
use App\Classes\Modules\Transactions\Processors\CreatePurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject;
use App\Classes\Modules\OpenAI\Services\CreatesChatGPTResponse;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Models\KeyValuePair;
use App\Models\Booking;
use Illuminate\Support\Facades\Log;
class ETLPurchaseOrderTransactionProcessor
{
/** @var CreatesChatGPTResponse */
private $createsChatGPTResponse;
/** @var FetchesBooking */
private $fetchesBooking;
/** @var GeneratesTransactionBillNumber */
private $generatesTransactionBillNumber;
/** @var CreatePurchaseOrderTransactionProcessor */
private $createPurchaseOrderTransactionProcessor;
/**
* ETLPurchaseOrderTransactionProcessor constructor.
* @param FetchesBooking $fetchesBooking
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
* @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor
* @param CreatesChatGPTResponse $createsChatGPTResponse
*/
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, CreatesChatGPTResponse $createsChatGPTResponse)
{
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
$this->createsChatGPTResponse = $createsChatGPTResponse;
}
/**
* @param array $products
* @param array $productsMetadata
* @param int|null $bookingId
* @return array
* @throws \App\Classes\Exceptions\MalformedRequestException
* @throws \App\Classes\Exceptions\ResourceNotFoundException
* @throws \Exception
*/
public function execute(array $products, array $productsMetadata, ?int $bookingId = null)
{
foreach ($products as &$product) {
$product['quantity'] = (int) $product['quantity'];
$product['unit_price'] = (float) $product['unit_price'];
}
// Translate descriptions if they contain Chinese characters
$products = collect($products)->map(function ($product) {
if (isset($product['description']) && preg_match("/\p{Han}+/u", $product['description'])) {
$kvp = KeyValuePair::where('key', KVPKey::CHATGPT_PROMPT_PREFIX . "TRANSLATE")->first();
$userPrompt = '';
if($kvp){
$templateFromDb = $kvp->value;
$productsMetadataJson = json_encode($product['description'], JSON_UNESCAPED_UNICODE);
$userPrompt = str_replace('{{productDescription}}', $productsMetadataJson, $templateFromDb);
}
else{
throw new ResourceNotFoundException('AI Setup incomplete!');
}
$result = $this->createsChatGPTResponse->execute($userPrompt);
$content = $result['choices'][0]['message']['content'] ?? null;
// 1. Try raw JSON
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
$json = $content;
}
// 2. Fallback to fenced ```json block
elseif (preg_match('/```json\s*(\[[\s\S]*?\]|\{[\s\S]*?\})\s*```/i', $content, $matches)) {
$json = $matches[1];
}
else {
Log::warning('No JSON found in ChatGPT response', ['content' => $content]);
return;
}
$translations = json_decode($json, true);
if($translations){
$translatedText = $translations[0];
$product['description'] = $translatedText;
}
}
return $product;
})->toArray();
if (is_array($productsMetadata) && count($productsMetadata) > 0) {
$kvp = KeyValuePair::where('key', KVPKey::CHATGPT_PROMPT_PREFIX . "EXTRACT1")->first();
$prompt = '';
if($kvp){
$templateFromDb = $kvp->value;
$productsMetadataJson = json_encode($productsMetadata, JSON_UNESCAPED_UNICODE);
$prompt = str_replace('{{productsMetadata}}', $productsMetadataJson, $templateFromDb);
}
else{
throw new ResourceNotFoundException('AI Setup incomplete!');
}
$result = $this->createsChatGPTResponse->execute($prompt);
$content = $result['choices'][0]['message']['content'] ?? null;
$freight = 0;
$preferential = 0;
if ($content) {
if (preg_match('/```json\s*(\{.*?\})\s*```/s', $content, $matches)) { //If wrapped in ```json, extract it
$json = $matches[1];
} else {
$json = trim($content);
}
$fees = json_decode($json);
if (json_last_error() === JSON_ERROR_NONE) {
$freight = $fees->Freight ?? 0;
$preferential = $fees->Preferential ?? 0;
} else {
Log::error('JSON decode failed', [
'error' => json_last_error_msg(),
'content' => $content
]);
}
}
// Add Freight as a separate product
if($freight){
$products[] = [
'stockCode' => '',
'description' => 'First-Mile Delivery',
'quantity' => 1,
'unit_price' => $freight,
];
}
// Add Preferential as a separate product (as negative price if needed)
if($preferential){
$products[] = [
'stockCode' => '',
'description' => 'Discount',
'quantity' => 1,
'unit_price' => $preferential,
];
}
}
if($bookingId){
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $bookingId]);
$billNumber = $this->generatesTransactionBillNumber->execute('PO-');
$total = collect($products)->sum(function($product){
return $product['quantity'] * floatval(str_replace(',', '', $product['unit_price']));
});
$object = new TransactionObject($billNumber, TransactionType::PURCHASE_ORDER, $booking->company->id, 1,
1, PaymentMethodType::CASH,
$total, $total, $booking->fix_currency_id, $booking->fix_currency_id,
1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, $products);
$transaction = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object);
}
return $products;
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Classes\Modules\Transactions\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanETLPurchaseOrder extends AbstractRule
{
/**
* @return bool
*/
protected function authorized($object): bool
{
if (in_array(Auth::user()->type, RoleTypes::ADMIN_ROLES)) {
return true;
}
return false;
}
/**
* @param $object
* @return bool
* @throws \App\Classes\Exceptions\RequestValidationException
*/
protected function validators($object): bool
{
return true;
}
/**
* @param $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -38,4 +38,6 @@ class KVPKey
public const BOOKING_AMOUNT_UPDATE = 'BOOKING_AMOUNT_UPDATE';
public const CHATGPT_PROMPT_PREFIX = 'CHATGPT_PROMPT_';
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\KeyValuePairs;
use App\Classes\Modules\KeyValuePairs\ControllersLogic\CreateChatGPTPromptKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateChatGPTPromptKVPController
{
/**
* @param Request $request
* @param CreateChatGPTPromptKVPLogic $logic
* @return JsonResponse
*/
public function create(Request $request, CreateChatGPTPromptKVPLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\KeyValuePairs;
use App\Classes\Modules\KeyValuePairs\ControllersLogic\DeleteChatGPTPromptKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DeleteChatGPTPromptKVPController
{
/**
* @param Request $request
* @param DeleteChatGPTPromptKVPLogic $logic
* @return JsonResponse
*/
public function delete(Request $request, DeleteChatGPTPromptKVPLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\KeyValuePairs;
use App\Classes\Modules\KeyValuePairs\ControllersLogic\ListChatGPTPromptsKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListChatGPTPromptsKVPController
{
/**
* @param Request $request
* @param ListChatGPTPromptsKVPLogic $logic
* @return JsonResponse
*/
public function list(Request $request, ListChatGPTPromptsKVPLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\KeyValuePairs;
use App\Classes\Modules\KeyValuePairs\ControllersLogic\UpdateChatGPTPromptKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateChatGPTPromptKVPController
{
/**
* @param Request $request
* @param UpdateChatGPTPromptKVPLogic $logic
* @return JsonResponse
*/
public function update(Request $request, UpdateChatGPTPromptKVPLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Transactions;
use App\Classes\Modules\Transactions\ControllersLogic\ETLPurchaseOrderTransactionLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ETLPurchaseOrderTransactionController
{
/**
* @param Request $request
* @param ETLPurchaseOrderTransactionLogic $logic
* @return JsonResponse
*/
public function etl(Request $request, ETLPurchaseOrderTransactionLogic $logic) : JsonResponse {
return $logic->execute($request);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
class ChatGPTPromptResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'key' => $this->key,
'value' => $this->value,
'created_at' => Carbon::parse($this->created_at)->format('d-m-Y'),
];
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
return [
'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com'),
'api_key' => env('OPENAI_API_KEY', ''),
'is_enabled' => env('OPENAI_IS_ENABLED', 'true'),
];
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateValueColumnToLongtextInKeyValuePairsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('key_value_pairs', function (Blueprint $table) {
$table->longText('value')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('key_value_pairs', function (Blueprint $table) {
$table->string('value')->change();
});
}
}
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MakeOwnerNullableInKeyValuePairs extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('key_value_pairs', function (Blueprint $table) {
$table->string('owner_type')->nullable()->change();
$table->unsignedBigInteger('owner_id')->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('key_value_pairs', function (Blueprint $table) {
$table->string('owner_type')->nullable(false)->change();
$table->unsignedBigInteger('owner_id')->nullable(false)->change();
});
}
}
Vendored
+22 -2
View File
@@ -85,12 +85,32 @@ gulp.task('sourceCss', gulp.series('sourceImages', () => {
.pipe(gulp.dest(pkg.paths.build.css));
}));
// gulp.task('sourceJs', () => {
// fancyLog('Compiling source JS dependencies');
// return gulp.src(pkg.globs.sourceJs)
// .pipe(plugins.plumber({errorHandler: onError}))
// .pipe(plugins.print())
// .pipe(plugins.concat('site.js'))
// .pipe(plugins.uglifyEs())
// .pipe(gulp.dest(pkg.paths.build.js));
// });
const sourceJsKeepNames = [
'pdf.js',
'pdf-table-extractor.js',
'pdf.worker.js',
];
gulp.task('sourceJs', () => {
fancyLog('Compiling source JS dependencies');
return gulp.src(pkg.globs.sourceJs)
.pipe(plugins.plumber({errorHandler: onError}))
.pipe(plugins.plumber({ errorHandler: onError }))
.pipe(plugins.print())
.pipe(plugins.concat('site.js'))
.pipe(plugins.if(
file => !sourceJsKeepNames.includes(path.basename(file.path)),
plugins.concat('site.js')
))
.pipe(plugins.uglifyEs())
.pipe(gulp.dest(pkg.paths.build.js));
});
+499
View File
@@ -0,0 +1,499 @@
// modify from https://github.com/mozilla/pdf.js/blob/master/examples/node/pdf2svg.js
pdf_table_extractor_progress = function(result){
};
pdf_table_extractor = function(doc){
var numPages = doc.numPages;
var result = {};
result.pageTables = [];
result.numPages = numPages;
result.currentPages = 0;
var transform_fn = function(m1, m2) {
return [
m1[0] * m2[0] + m1[2] * m2[1],
m1[1] * m2[0] + m1[3] * m2[1],
m1[0] * m2[2] + m1[2] * m2[3],
m1[1] * m2[2] + m1[3] * m2[3],
m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
m1[1] * m2[4] + m1[3] * m2[5] + m1[5]
];
};
var applyTransform_fn = function(p, m) {
var xt = p[0] * m[0] + p[1] * m[2] + m[4];
var yt = p[0] * m[1] + p[1] * m[3] + m[5];
return [xt, yt];
};
var lastPromise = Promise.resolve(); // will be used to chain promises
var loadPage = function (pageNum) {
return doc.getPage(pageNum).then(function (page) {
var verticles = [];
var horizons = [];
var merges = {};
var merge_alias = {};
var transformMatrix = [1,0,0,1,0,0];;
var transformStack = [];
return page.getOperatorList().then(function (opList) {
// Get rectangle first
var showed = {};
var REVOPS = [];
for (var op in pdfjsLib.OPS) {
REVOPS[pdfjsLib.OPS[op]] = op;
}
var strokeRGBColor = null;
var fillRGBColor = null;
var current_x, current_y;
var edges = [];
var line_max_width = 2;
var lineWidth = null;
while (opList.fnArray.length) {
var fn = opList.fnArray.shift();
var args = opList.argsArray.shift();
if (pdfjsLib.OPS.constructPath == fn) {
while (args[0].length) {
op = args[0].shift();
if (op == pdfjsLib.OPS.rectangle) {
x = args[1].shift();
y = args[1].shift();
width = args[1].shift();
height = args[1].shift();
if (Math.min(width, height) < line_max_width) {
edges.push({y:y, x:x, width:width, height:height, transform: transformMatrix});
}
} else if (op == pdfjsLib.OPS.moveTo) {
current_x = args[1].shift();
current_y = args[1].shift();
} else if (op == pdfjsLib.OPS.lineTo) {
x = args[1].shift();
y = args[1].shift();
if(lineWidth == null) {
if (current_x == x) {
edges.push({
y: Math.min(y, current_y),
x: Math.min(x, current_x),
height: Math.abs(y - current_y),
transform: transformMatrix
});
} else if (current_y == y) {
edges.push({
x: Math.min(x, current_x),
y: Math.min(y, current_y),
width: Math.abs(x - current_x),
transform: transformMatrix
});
}
} else {
if (current_x == x) {
edges.push({y: Math.min(y, current_y), x: x - lineWidth / 2, width: lineWidth, height: Math.abs(y - current_y), transform: transformMatrix});
} else if (current_y == y) {
edges.push({x: Math.min(x, current_x), y: y - lineWidth / 2, height: lineWidth, width: Math.abs(x - current_x), transform: transformMatrix});
}
}
current_x = x;
current_y = y;
} else {
// throw ('constructPath ' + op);
}
}
} else if (pdfjsLib.OPS.save == fn) {
transformStack.push(transformMatrix);
} else if (pdfjsLib.OPS.restore == fn ){
transformMatrix = transformStack.pop();
} else if (pdfjsLib.OPS.transform == fn) {
transformMatrix = transform_fn(transformMatrix, args);
} else if (pdfjsLib.OPS.setStrokeRGBColor == fn) {
strokeRGBColor = args;
} else if (pdfjsLib.OPS.setFillRGBColor == fn) {
fillRGBColor = args;
} else if (pdfjsLib.OPS.setLineWidth == fn) {
lineWidth = args[0];
} else if (['eoFill'].indexOf(REVOPS[fn]) >= 0) {
} else if ('undefined' === typeof(showed[fn])) {
showed[fn] = REVOPS[fn];
} else {
}
}
edges = edges.map(function(edge){
var point1 = applyTransform_fn([edge.x, edge.y], edge.transform);
var point2 = applyTransform_fn([edge.x + edge.width, edge.y + edge.height], edge.transform);
return {
x: Math.min(point1[0], point2[0]),
y: Math.min(point1[1], point2[1]),
width: Math.abs(point1[0] - point2[0]),
height: Math.abs(point1[1] - point2[1]),
};
});
// merge rectangle to verticle lines and horizon lines
edges1 = JSON.parse(JSON.stringify(edges));
edges1.sort(function(a, b){ return (a.x - b.x) || (a.y - b.y); });
edges2 = JSON.parse(JSON.stringify(edges));
edges2.sort(function(a, b){ return (a.y - b.y) || (a.x - b.x); });
// get verticle lines
var current_x = null;
var current_y = null;
var current_height = 0;
var lines = [];
var lines_add_verticle = function(lines, top, bottom){
var hit = false;
for (var i = 0; i < lines.length; i ++) {
if (lines[i].bottom < top || lines[i].top > bottom) {
continue;
}
hit = true;
top = Math.min(lines[i].top, top);
bottom = Math.max(lines[i].bottom, bottom);
new_lines = [];
if (i > 1) {
news_lines = lines.slice(0, i - 1);
}
new_lines = new_lines.concat(lines.slice(i + 1));
lines = new_lines;
return lines_add_verticle(lines, top, bottom);
}
if (!hit) {
lines.push({top: top, bottom: bottom});
}
return lines;
};
while (edge = edges1.shift()) {
// skip horizon lines
if (edge.width > line_max_width) {
continue;
}
// new verticle lines
if (null === current_x || edge.x - current_x > line_max_width) {
if (current_height > line_max_width) {
lines = lines_add_verticle(lines, current_y, current_y + current_height);
}
if (null !== current_x && lines.length) {
verticles.push({x: current_x, lines: lines});
}
current_x = edge.x;
current_y = edge.y;
current_height = 0;
lines = [];
}
if (Math.abs(current_y + current_height - edge.y) < 10) {
current_height = edge.height + edge.y - current_y;
} else {
if (current_height > line_max_width) {
lines = lines_add_verticle(lines, current_y, current_y + current_height);
}
current_y = edge.y;
current_height = edge.height;
}
}
if (current_height > line_max_width) {
lines = lines_add_verticle(lines, current_y, current_y + current_height);
}
// no table
if (current_x === null || lines.length == 0) {
return {};
}
verticles.push({x: current_x, lines: lines});
// Get horizon lines
current_x = null;
current_y = null;
var current_width = 0;
var lines_add_horizon = function(lines, left, right){
var hit = false;
for (var i = 0; i < lines.length; i ++) {
if (lines[i].right < left || lines[i].left > right) {
continue;
}
hit = true;
left = Math.min(lines[i].left, left);
right = Math.max(lines[i].right, right);
new_lines = [];
if (i > 1) {
news_lines = lines.slice(0, i - 1);
}
new_lines = new_lines.concat(lines.slice(i + 1));
lines = new_lines;
return lines_add_horizon(lines, left, right);
}
if (!hit) {
lines.push({left: left, right: right});
}
return lines;
};
while (edge = edges2.shift()) {
if (edge.height > line_max_width) {
continue;
}
if (null === current_y || edge.y - current_y > line_max_width) {
if (current_width > line_max_width) {
lines = lines_add_horizon(lines, current_x, current_x + current_width);
}
if (null !== current_y && lines.length) {
horizons.push({y: current_y, lines: lines});
}
current_x = edge.x;
current_y = edge.y;
current_width = 0;
lines = [];
}
if (Math.abs(current_x + current_width - edge.x) < 10) {
current_width = edge.width + edge.x - current_x;
} else {
if (current_width > line_max_width) {
lines = lines_add_horizon(lines, current_x, current_x + current_width);
}
current_x = edge.x;
current_width = edge.width;
}
}
if (current_width > line_max_width) {
lines = lines_add_horizon(lines, current_x, current_x + current_width);
}
// no table
if (current_y === null || lines.length == 0) {
return {};
}
horizons.push({y: current_y, lines: lines});
var search_index = function(v, list) {
for (var i = 0; i < list.length; i ++) {
if (Math.abs(list[i] - v) < 5) {
return i;
}
}
return -1;
};
// handle merge cells
x_list = verticles.map(function(a){ return a.x; });
// check top_out and bottom_out
var y_list = horizons.map(function(a){ return a.y; }).sort(function(a, b) { return b - a; });
var y_max = verticles
.map(function(verticle) { return verticle.lines[0].bottom; })
.sort().reverse()[0];
var y_min = verticles
.map(function(verticle) { return verticle.lines[verticle.lines.length - 1].top; })
.sort()[0];
var top_out = search_index(y_min, y_list) == -1 ? 1 : 0;
var bottom_out = search_index(y_max, y_list) == -1 ? 1 : 0;
var verticle_merges = {};
// skip the 1st lines and final lines
for (var r = 0; r < horizons.length - 2 + top_out + bottom_out; r ++) {
hor = horizons[bottom_out + horizons.length - r - 2];
lines = hor.lines.slice(0);
col = search_index(lines[0].left, x_list);
if (col != 0) {
for (var c = 0; c < col; c ++) {
verticle_merges[[r, c].join('-')] = {row: r, col: c, width: 1, height: 2};
}
}
while (line = lines.shift()) {
left_col = search_index(line.left, x_list);
right_col = search_index(line.right, x_list);
if (left_col != col) {
for (var c = col; c < left_col; c ++) {
verticle_merges[[r, c].join('-')] = {row: r, col: c, width: 1, height: 2};
}
}
col = right_col;
}
if (col != verticles.length - 1 + top_out) {
for (var c = col; c < verticles.length - 1 + top_out; c ++) {
verticle_merges[[r, c].join('-')] = {row: r, col: c, width: 1, height: 2};
}
}
}
while (true) {
var merged = false;
for (var r_c in verticle_merges) {
var m = verticle_merges[r_c];
var final_id = [m.row + m.height - 1, m.col + m.width - 1].join('-');
if ('undefined' !== typeof(verticle_merges[final_id])) {
verticle_merges[r_c].height += verticle_merges[final_id].height - 1;
delete(verticle_merges[final_id]);
merged = true;
break;
}
}
if (!merged) {
break;
}
}
var horizon_merges = {};
for (var c = 0; c < verticles.length - 2; c ++) {
ver = verticles[c + 1];
lines = ver.lines.slice(0);
row = search_index(lines[0].bottom, y_list) + bottom_out;
if (row != 0) {
for (var r = 0; r < row; r ++) {
horizon_merges[[r, c].join('-')] = {row: r, col: c, width: 2, height: 1};
}
}
while (line = lines.shift()) {
top_row = search_index(line.top, y_list);
if (top_row == -1) {
top_row = y_list.length + bottom_out;
} else {
top_row += bottom_out;
}
bottom_row = search_index(line.bottom, y_list) + bottom_out;
if (bottom_row != row) {
for (var r = bottom_row; r < row; r ++) {
horizon_merges[[r, c].join('-')] = {row: r, col: c, width: 2, height: 1};
}
}
row = top_row;
}
if (row != horizons.length - 1 + bottom_out + top_out) {
for (var r = row; r < horizons.length - 1 + bottom_out + top_out; r ++) {
horizon_merges[[r, c].join('-')] = {row: r, col: c, width: 2, height: 1};
}
}
}
if (top_out) {
horizons.unshift({y: y_min, lines: []});
}
if (bottom_out) {
horizons.push({y:y_max, lines:[]});
}
while (true) {
var merged = false;
for (var r_c in horizon_merges) {
var m = horizon_merges[r_c];
var final_id = [m.row + m.height - 1, m.col + m.width - 1].join('-');
if ('undefined' !== typeof(horizon_merges[final_id])) {
horizon_merges[r_c].width += horizon_merges[final_id].width - 1;
delete(horizon_merges[final_id]);
merged = true;
break;
}
}
if (!merged) {
break;
}
}
merges = verticle_merges;
for (var id in horizon_merges) {
if ('undefined' !== typeof(merges[id])) {
merges[id].width = horizon_merges[id].width;
} else {
merges[id] = horizon_merges[id];
}
}
for (var id in merges) {
for (var c = 0; c < merges[id].width; c ++) {
for (var r = 0; r < merges[id].height; r ++) {
if (c == 0 && r == 0) {
continue;
}
delete(merges[[r + merges[id].row, c + merges[id].col].join('-')]);
}
}
}
merge_alias = {};
for (var id in merges) {
for (var c = 0; c < merges[id].width; c ++) {
for (var r = 0; r < merges[id].height; r ++) {
if (r == 0 && c == 0) {
continue;
}
merge_alias[[merges[id].row + r, merges[id].col + c].join('-')] = [merges[id].row, merges[id].col].join('-');
}
}
}
}).then(function(){
return page.getTextContent().then(function (content) {
tables = [];
table_pos = [];
for (var i = 0; i < horizons.length - 1; i ++) {
tables[i] = [];
table_pos[i] = [];
for (var j = 0; j < verticles.length - 1; j ++) {
tables[i][j] = '';
table_pos[i][j] = null;
}
}
while (item = content.items.shift()) {
x = item.transform[4];
y = item.transform[5];
var col = -1;
for (var i = 0; i < verticles.length - 1 ; i ++) {
if (x >= verticles[i].x && x < verticles[i + 1].x) {
col = i;
break;
}
}
if (col == -1) {
continue;
}
var row = -1;
for (var i = 0; i < horizons.length - 1 ; i ++) {
if (y >= horizons[i].y && y < horizons[i + 1].y) {
row = horizons.length - i - 2;
break;
}
}
if (row == -1) {
continue;
}
if ('undefined' !== typeof(merge_alias[row + '-' + col])) {
id = merge_alias[row + '-' + col];
row = id.split('-')[0];
col = id.split('-')[1];
}
if (null !== table_pos[row][col] && Math.abs(table_pos[row][col] - y) > 5) {
tables[row][col] += "\n";
}
table_pos[row][col] = y;
tables[row][col] += item.str;
}
if (tables.length) {
result.pageTables.push({
page: pageNum,
tables: tables,
merges: merges,
merge_alias: merge_alias,
width: verticles.length - 1,
height: horizons.length - 1,
});
}
result.currentPages ++;
if ('function' === typeof(pdf_table_extractor_progress)) {
pdf_table_extractor_progress(result);
}
});
});
});
};
for (var i = 1; i <= numPages; i++) {
lastPromise = lastPromise.then(loadPage.bind(null, i));
}
return lastPromise.then(function(){
return result;
});
};
+14074
View File
File diff suppressed because it is too large Load Diff
+47415
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,539 @@
<template>
<div class="p-4">
<div class="dropbox p-5 mb-4 bg-light text-secondary rounded text-center position-relative"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver"
@drop.prevent="onDrop"
:class="{ 'bg-secondary text-white': isDragging }">
<div id="drop">
<span>
<img class="logo mb-3" src="logo.png" alt="Your Logo" v-if="false"><br>
Drag and drop your PDF file here
</span>
</div>
<div v-if="filesProcessed" class="alert alert-success mt-3 mb-0" role="alert">
<strong>{{ fileName }}</strong> content extracted. <br/>
<div class="row m-b-10">
<div class="col">
<span class="text-complete pointer requestModal text-underline" data-type="previewExtractedTable">Preview</span>
</div>
</div>
</div>
<div class="d-flex justify-content-center mt-3">
<label for="pdf-file-input"
class="btn btn-sm btn-outline-complete rounded-0 pointer flex-fill mx-1 text-center mb-0">
Browse files
</label>
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 pointer flex-fill mx-1"
v-if="isPurchaseOrderWithTablesPDF"
@click="exportToCSV"
:disabled="isExporting">
{{ isExporting ? 'Exporting...' : 'Export Extracted Content (csv)' }}
</button>
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 pointer flex-fill mx-1"
v-if="isPurchaseOrderWithTablesPDF"
@click="importToSystem"
:disabled="isImporting">
{{ isImporting ? 'Uploading...' : 'Continue Upload' }}
</button>
</div>
<input type="file" id="pdf-file-input" @change="onFileChange" accept="application/pdf" style="display: none;" />
</div>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="previewExtractedTable" size="large">
<div class="row p-t-25 text-left">
<div class="col bg-white padding-40 b-rad-lg">
<div class="modal-header">
<h2>Preview Extracted Content</h2>
</div>
<div id="html-result" v-if="tables.length > 0">
<div v-for="(table, tIndex) in tables" :key="tIndex" class="table-wrapper">
<table border="1">
<tbody>
<tr v-for="(row, rIndex) in table" :key="rIndex">
<td v-for="(cell, cIndex) in row"
:key="cIndex"
:colspan="cell.colspan"
:rowspan="cell.rowspan"
v-if="!cell.hidden">
{{ cell.text }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</modal-component>
</div>
</template>
<script>
import { integer } from 'vuelidate/lib/validators';
export default {
name: 'PdfTableExtractorComponent',
props: {
section:{
type: String,
required: true
},
bookingId: {
type: Number,
required: true
}
},
data() {
return {
isDragging: false,
tables: [],
nonTableData: [],
allTablesForExportImport: [],
isExporting: false,
isImporting: false,
filesProcessed: false,
showPreviewModal: false,
fileName: '',
translationCache: {},
mergeTableRemoveFirstLine: false,
parameters: {
products: [],
productsMetadata: []
}
};
},
computed: {
isPurchaseOrderWithTablesPDF() {
return this.allTablesForExportImport.length > 0;
}
},
methods: {
onDragEnter(e) {
this.isDragging = true;
},
onDragOver(e) {
this.isDragging = true;
e.dataTransfer.dropEffect = 'copy';
},
onDrop(e) {
this.isDragging = false;
const files = e.dataTransfer.files;
if (files.length) {
this.processFile(files[0]);
}
},
onFileChange(e) {
const files = e.target.files;
if (files.length) {
this.processFile(files[0]);
}
},
async processFile(file) {
this.tables = [];
this.allTablesForExportImport = [];
this.filesProcessed = false;
this.fileName = file.name;
this.parameters.products = [];
this.parameters.productsMetadata = [];
try {
await this.loadDependencies();
} catch (error) {
console.error("Failed to load PDF dependencies:", error);
alert("Critical Error: Could not load PDF libraries.");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target.result;
this.parseContent(data);
};
reader.readAsArrayBuffer(file);
},
async loadDependencies() {
if (typeof pdfjsLib !== 'undefined' && typeof pdf_table_extractor !== 'undefined') {
return;
}
if (typeof pdfjsLib === 'undefined') {
await this.loadScript('/js/pdf.js');
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.js';
}
if (typeof pdf_table_extractor === 'undefined') {
await this.loadScript('/js/pdf-table-extractor.js');
}
},
loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
},
parseContent(content) {
if (typeof pdfjsLib === 'undefined' || typeof pdf_table_extractor === 'undefined') {
console.error('PDF.js or pdf-table-extractor not loaded.');
alert('Required PDF libraries are missing.');
return;
}
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.js';
}
const loadingTask = pdfjsLib.getDocument(content);
loadingTask.promise
.then(async (doc) => {
const result = await pdf_table_extractor(doc);
this.processExtractedTables(result);
await this.extractNonTableData4(doc, result);
this.filesProcessed = true;
})
.catch(error => {
console.error("Error extracting tables:", error);
alert("Error parsing PDF.");
});
},
processExtractedTables(result) {
let extractedTables = [];
let exportData = [];
const pages = [...result.pageTables];
pages.forEach(page_tables => {
const tables = page_tables.tables;
const merge_alias = page_tables.merge_alias;
const merges = page_tables.merges;
if (tables.length > 0) {
let currentTable = [];
for (let r = 0; r < tables.length; r++) {
if (this.mergeTableRemoveFirstLine && page_tables.page != 1 && r == 0) {
continue;
}
let rowCells = [];
let rowForExport = [];
for (let c = 0; c < tables[r].length; c++) {
let r_c = [r, c].join('-');
if (merge_alias[r_c]) {
rowCells.push({ hidden: true });
continue;
}
if (merges[r_c] && merges[r_c].width > 2) {
rowCells.push({ hidden: true });
continue;
}
let cell = {
text: tables[r][c],
hidden: false,
rowspan: 1,
colspan: 1
};
if (merges[r_c]) {
if (merges[r_c].width > 1) cell.colspan = merges[r_c].width;
if (merges[r_c].height > 1) cell.rowspan = merges[r_c].height;
}
rowCells.push(cell);
let value = tables[r][c];
let cleanValue = value.replace(/[\r\n]+/g, ' ').trim();
rowForExport.push(cleanValue);
}
currentTable.push(rowCells);
if (rowForExport.length > 1) {
const limitedRowForExport = rowForExport.slice(0, 7);
const headerRow = exportData[0];
const isSameAsHeader = headerRow && limitedRowForExport.every((val, idx) => val === headerRow[idx]);
if (!isSameAsHeader) {
exportData.push(limitedRowForExport);
}
}
}
extractedTables.push(currentTable);
}
});
this.tables = extractedTables;
this.allTablesForExportImport = exportData;
},
async extractNonTableData4(doc, result) {
this.nonTableData = [];
for (let i = 1; i <= doc.numPages; i++) {
const page = await doc.getPage(i);
const content = await page.getTextContent();
const pageStrItems = content.items;
const pageTableData = result.pageTables.find(pt => pt.page === i);
const tableBoxes = [];
if (pageTableData?.tables?.length) {
pageTableData.tables.forEach((table, tIdx) => {
let dataRowCount = 0;
table.forEach(row => {
const hasData = Array.isArray(row)
? row.some(cell => cell.trim().length > 0)
: String(row).trim().length > 0;
if (hasData) dataRowCount++;
});
if (dataRowCount > 1) {
let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
table.forEach((row, rIdx) => {
if (dataRowCount === 1 && rIdx !== 0) return;
if (Array.isArray(row)) {
row.forEach(cell => {
const lines = String(cell).split('\n').map(l => l.trim()).filter(Boolean);
lines.forEach(line => {
const matched = pageStrItems.find(item => item.str.includes(line));
if (matched) {
const x = matched.transform[4];
const y = matched.transform[5];
const w = matched.width || 0;
xMin = Math.min(xMin, x);
xMax = Math.max(xMax, x + w);
yMin = Math.min(yMin, y);
yMax = Math.max(yMax, y);
}
});
});
} else if (typeof row === 'string') {
const lines = row.split('\n').map(l => l.trim()).filter(Boolean);
lines.forEach(line => {
const matched = pageStrItems.find(item => item.str.includes(line));
if (matched) {
const x = matched.transform[4];
const y = matched.transform[5];
const w = matched.width || 0;
xMin = Math.min(xMin, x);
xMax = Math.max(xMax, x + w);
yMin = Math.min(yMin, y);
yMax = Math.max(yMax, y);
}
});
}
});
if (xMin < Infinity) {
tableBoxes.push({ xMin, xMax, yMin, yMax });
}
}
});
}
const filteredItems = pageStrItems.filter(item => {
const x = item.transform[4];
const y = item.transform[5];
const str = item.str.trim();
if (!str) return false;
const isInTable = tableBoxes.some(box =>
x >= box.xMin && x <= box.xMax &&
y >= box.yMin && y <= box.yMax
);
return !isInTable;
});
const lines = {};
const tolerance = 5;
filteredItems.forEach(item => {
const y = item.transform[5];
const x = item.transform[4];
let added = false;
for (const lineY in lines) {
if (Math.abs(parseFloat(lineY) - y) < tolerance) {
lines[lineY].push({ str: item.str, x });
added = true;
break;
}
}
if (!added) {
lines[y] = [{ str: item.str, x }];
}
});
const sortedLineKeys = Object.keys(lines)
.sort((a, b) => parseFloat(b) - parseFloat(a));
const readableContent = sortedLineKeys.map(key => {
const lineItems = lines[key].sort((a, b) => a.x - b.x);
return lineItems.map(i => i.str).join(' ');
});
if (readableContent.length) {
this.nonTableData.push({
page: i,
content: readableContent
});
}
}
},
exportToCSV() {
if (this.isExporting) return;
this.isExporting = true;
this.parameters.products = [];
this.parameters.productsMetadata = [];
for (let index = 0; index < this.allTablesForExportImport.length; index++) {
if (index === 0) continue;
const rowArray = this.allTablesForExportImport[index];
const partNumber = rowArray[1] || '';
let productName = rowArray[2] || '';
const specification = rowArray[3] || '';
const quantity = rowArray[4] || '';
const rawUnitPrice = rowArray[5] || '';
const unitPrice = rawUnitPrice.match(/\d+\.?\d*/)?.[0] || '';
productName = productName.replace(/^["'](.*)["']$/, '$1');
this.parameters.products.push({
stockCode: partNumber,
description: productName + ' | ' + specification,
quantity: quantity,
unit_price: unitPrice
});
this.parameters.productsMetadata = this.nonTableData;
}
this.submit(route('api.transaction.po.etl', this.bookingId), 'post', this.section + 'PDFExport', true, true);
},
importToSystem() {
if (this.isImporting) return;
this.isImporting = true;
if(this.parameters.products && this.parameters.products.length === 0){
for (let index = 0; index < this.allTablesForExportImport.length; index++) {
if (index === 0) continue;
const rowArray = this.allTablesForExportImport[index];
const partNumber = rowArray[1] || '';
let productName = rowArray[2] || '';
const specification = rowArray[3] || '';
const quantity = rowArray[4] || '';
const rawUnitPrice = rowArray[5] || '';
const unitPrice = rawUnitPrice.match(/\d+\.?\d*/)?.[0] || '';
productName = productName.replace(/^["'](.*)["']$/, '$1');
this.parameters.products.push({
stockCode: partNumber,
description: productName + ' | ' + specification,
quantity: quantity,
unit_price: unitPrice
});
this.parameters.productsMetadata = this.nonTableData;
}
}
this.submit(route('api.transaction.po.import', this.bookingId), 'post', this.section + 'PDFImport', true, true);
},
escapeCSV(value) {
if (value === null || value === undefined) return '';
const str = value.toString();
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
},
containsChinese(text) {
return /[\u4e00-\u9fff]/.test(text);
},
successHandler(response, section){
if(this.section + 'PDFImport' === section){
this.updateList()
this.isImporting = false;
}
else if(this.section + 'PDFExport' === section){
try {
const BOM = '\uFEFF';
let csvContent = "stock code,description,quantity,unit price\r\n";
response.payload.forEach(item => {
const formattedRow = [
item.stockCode,
this.escapeCSV(item.description),
item.quantity,
item.unit_price
].join(',');
csvContent += formattedRow + "\r\n";
});
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.setAttribute("download", "order_details.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.parameters.products = response.payload;
this.parameters.productsMetadata = [];
} catch (e) {
console.error("Export failed:", e);
alert("Export failed. Please try again.");
} finally {
this.isExporting = false;
}
}
},
errorHandler(response, statusCode, section){
this.isExporting = false;
this.isImporting = false;
}
}
}
</script>
<style scoped>
.dropbox {
border: 2px dashed #bbb;
border-radius: 5px;
padding: 25px;
text-align: center;
color: #bbb;
margin-bottom: 20px;
transition: background-color 0.2s;
}
.table-wrapper {
margin-bottom: 20px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
td, th {
border: 1px solid #ddd;
padding: 8px;
}
</style>
@@ -29,12 +29,17 @@
</div>
<div class="row m-t-20" v-if="!submitted">
<div class="col">
<div class="row m-b-15">
<div class="row" v-if="$store.getters.isAdmin">
<div class="col">
<span class="pointer text-complete fs-11 text-upper text-underline" @click="useUploadCsvPo = !useUploadCsvPo">{{ useUploadCsvPo ? '< Back to Manual Enter Purchase Order' : 'Click here to Upload PURCHASE ORDER CSV' }}</span>
<span class="pointer text-complete fs-11 text-upper text-underline" @click="useUploadPdfPo = !useUploadPdfPo; useUploadCsvPo = false;">{{ useUploadPdfPo ? '< Back to Manual Enter Purchase Order' : 'Click here to Upload PURCHASE ORDER PDF' }}</span>
</div>
</div>
<div class="row" v-if="!useUploadCsvPo">
<div class="row m-b-15">
<div class="col">
<span class="pointer text-complete fs-11 text-upper text-underline" @click="useUploadCsvPo = !useUploadCsvPo; useUploadPdfPo = false;">{{ useUploadCsvPo ? '< Back to Manual Enter Purchase Order' : 'Click here to Upload PURCHASE ORDER CSV' }}</span>
</div>
</div>
<div class="row" v-if="!useUploadCsvPo && !useUploadPdfPo">
<div class="col">
<div class="row">
<div class="col-sm-12 col-md-4 pr-md-1">
@@ -112,6 +117,11 @@
</div>
</div>
</div>
<div class="row" v-if="useUploadPdfPo">
<div class="col">
<pdf-table-extractor-component :booking-id="data.id" :section="section"></pdf-table-extractor-component>
</div>
</div>
</div>
</div>
</div>
@@ -325,6 +335,7 @@
interval:false,
submitted: false,
useUploadCsvPo: false,
useUploadPdfPo: false,
product: {
stockCode: '',
description: '',
@@ -0,0 +1,81 @@
<template>
<div class="row m-b-15 parentContainer">
<div class="col">
<div class="row align-items-center">
<div class="col-3">
<div class="row">
<div class="col-auto p-r-10">
<div class="font-heading all-caps fs-8 muted">Date</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="font-heading fs-10">{{item.created_at}}</div>
</div>
</div>
</div>
<div class="col-3">
<div class="row">
<div class="col-auto p-r-10">
<div class="font-heading all-caps fs-8 muted">Title</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="font-heading fs-10">{{item.key | truncate(50)}}</div>
</div>
</div>
</div>
<div class="col">
<div class="row">
<div class="col-auto p-r-10">
<div class="font-heading all-caps fs-8 muted">Prompt</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="font-heading fs-10">{{item.value | truncate(100)}}</div>
</div>
</div>
</div>
<div class="col-auto parentContainer">
<div class="row align-items-center">
<template v-if="$store.getters.isAdmin">
<div class="col-auto no-padding">
<button class="btn btn-xs btn-complete b-rad-none m-r-5 requestModal" data-type="editModal">
Edit
</button>
</div>
<div class="col-auto no-padding">
<button class="btn btn-xs btn-outline-danger b-rad-none m-r-5 requestModal" data-type="deleteChatGPTPrompt" v-if="item.key !== 'CHATGPT_PROMPT_TRANSLATE' && item.key !== 'CHATGPT_PROMPT_EXTRACT1'">
<i class="fa fa-times"></i>
</button>
</div>
</template>
</div>
<modal-form-component :data="data" size="large" section="allChatGPTPromptsSection">
<template slot="form" slot-scope="{section}">
<chat-gpt-form-component :data="data" :section="section"></chat-gpt-form-component>
</template>
</modal-form-component>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="deleteChatGPTPrompt">
<delete-chat-gpt-form-component :data="item" section="allChatGPTPromptsSection" class="text-center"></delete-chat-gpt-form-component>
</modal-component>
</div>
</div>
</div>
</div>
</template>
<script>
import componentHandler from '../../../general/mixins/componentHandler';
export default {
props: {
section: {
default: 'singleChatGPTPromptSection'
}
},
mixins: [componentHandler]
}
</script>
@@ -0,0 +1,83 @@
<template>
<div class="row">
<div class="col">
<loading-component style="height: 50px; top: 0;" key="1" color="success" v-show="$store.getters.isLoading(section)"></loading-component>
<div class="row" v-show="!$store.getters.isLoading(section)">
<div class="col">
<div class="row m-b-20">
<div class="col">
<h6 class="all-caps m-b-5 bold no-margin">{{this.data?"Update":"Create"}} ChatGPT Prompt</h6>
</div>
</div>
<div class="row m-b-5 animate__animated animate__fadeInUpBig animate__fast" v-if="error">
<div class="col">
<small class="bold fs-10 text-danger">{{error}}</small>
</div>
</div>
<div class="row m-b-10">
<div class="col">
<validation-wrapper-component :validator="$v.parameters.key">
<label>Title</label>
<input type="text" class="form-control" v-model="parameters.key" :disabled="this.data">
</validation-wrapper-component>
</div>
</div>
<div class="row">
<div class="col">
<validation-wrapper-component :validator="$v.parameters.value">
<label>Prompt</label>
<textarea class="form-control h-75" v-model="parameters.value" rows="20"></textarea>
</validation-wrapper-component>
</div>
</div>
<div class="row">
<div class="col p-r-5">
<div class="btn btn-sm btn-default bg-master-lightest btn-block b-rad-none" :data-dismiss="closable ? 'modal' : ''" @click="$emit('close')">Cancel</div>
</div>
<div class="col p-l-5">
<div class="btn btn-sm btn-success btn-block b-rad-none" @click="submitForm()">{{this.data?"Update":"Create"}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
import { required } from "vuelidate/lib/validators";
export default {
props : {
closable: {
type: Boolean,
default: true
},
},
data(){
return {
parameters: {
key: '',
value: '',
}
}
},
validations: {
parameters: {
key: {
required
},
value: {
required
},
}
},
computed:{},
created(){},
methods: {
submitForm(){
this.submit((this.data ? this.route('api.kvp.prompt.update', this.data.id) : this.route('api.kvp.prompt.create')), (this.data ? 'put' : 'post'), this.section, true, false)
}
},
mixins: [ModalFormHandler],
}
</script>
@@ -0,0 +1,32 @@
<template>
<div class="row">
<div class="col">
<loading-component style="height: 300px; top: 0;" key="1" color="success" v-show="isLoading" ></loading-component>
<div class="row justify-content-center" v-show="!isLoading">
<div class="col">
<div class="row m-b-20">
<div class="col">
<h3 class="all-caps">Are you Sure?</h3>
<div class="fs-11">Are you sure you want to delete ChatGPT prompt with title <b>{{item.key}}</b> ?</div>
</div>
</div>
<div class="row">
<div class="col p-r-5">
<div class="btn btn-sm btn-success btn-block b-rad-none" data-dismiss="modal">Cancel</div>
</div>
<div class="col p-l-5">
<div class="btn btn-sm btn-danger btn-block b-rad-none" @click="submit(route('api.kvp.prompt.delete', item.id), 'delete', section, true, true)">Delete</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import componentHandler from '../../../general/mixins/componentHandler';
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
export default {
mixins: [componentHandler, ModalFormHandler]
}
</script>
@@ -0,0 +1,28 @@
<template>
<div class="row">
<div class="col">
<div class="alert alert-info padding-15" role="alert" v-show="!isLoading">
<div class="row">
<div class="col">
<h5 class="pull-left">{{item.key}}</h5>
</div>
<div class="col-auto">
<i class="fa fa-times text-info-darker pointer" data-dismiss="alert"></i>
</div>
</div>
<div class="row">
<div class="col">
<p v-html="item.value"></p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import componentHandler from '../../../general/mixins/componentHandler';
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
export default {
mixins: [componentHandler, ModalFormHandler]
}
</script>
+61 -2
View File
@@ -240,8 +240,31 @@
</div>
</div>
</div>
<div class="row m-b-5">
<div class="col">
<div class="row">
<div class="col">
<div class="row">
<div class="col bg-master-light tabButton" tab-name="chatGPTPrompts">
<div class="row align-items-center">
<div class="col-auto p-t-10 p-b-10 b-r b-grey">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="35" height="35"
viewBox="0 0 172 172"
style=" fill:#000000;"><defs><linearGradient x1="99.4375" y1="43.33594" x2="99.4375" y2="60.14625" gradientUnits="userSpaceOnUse" id="color-1_43971_gr1"><stop offset="0" stop-color="#4ec9ff"></stop><stop offset="1" stop-color="#2bffe6"></stop></linearGradient><linearGradient x1="106.15625" y1="26.875" x2="106.15625" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-2_43971_gr2"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient><linearGradient x1="83.3125" y1="26.875" x2="83.3125" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-3_43971_gr3"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient><linearGradient x1="73.90625" y1="26.875" x2="73.90625" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-4_43971_gr4"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient><linearGradient x1="51.0625" y1="26.875" x2="51.0625" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-5_43971_gr5"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient><linearGradient x1="51.0625" y1="26.875" x2="51.0625" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-6_43971_gr6"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient><linearGradient x1="45.6875" y1="26.875" x2="45.6875" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-7_43971_gr7"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient><linearGradient x1="43" y1="26.875" x2="43" y2="146.16775" gradientUnits="userSpaceOnUse" id="color-8_43971_gr8"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient></defs><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,172v-172h172v172z" fill="none"></path><g><path d="M112.875,59.125h-26.875c-1.4835,0 -2.6875,-1.204 -2.6875,-2.6875v-10.75c0,-1.4835 1.204,-2.6875 2.6875,-2.6875h26.875c1.4835,0 2.6875,1.204 2.6875,2.6875v10.75c0,1.4835 -1.204,2.6875 -2.6875,2.6875z" fill="url(#color-1_43971_gr1)"></path><path d="M155.875,96.75c0,-7.97381 -5.82381,-14.59581 -13.4375,-15.88313v-18.82325c0,-2.08819 -1.16906,-3.94525 -3.05031,-4.85094c-1.88125,-0.90031 -4.06081,-0.65575 -5.69481,0.64769l-21.48925,17.18925c-0.08062,0.0645 -0.13975,0.14781 -0.21769,0.21769h-31.36044c-7.40944,0 -13.4375,6.02806 -13.4375,13.4375h-5.375c-2.96431,0 -5.375,2.41069 -5.375,5.375v5.375c0,2.96431 2.41069,5.375 5.375,5.375h5.375c0,7.40944 6.02806,13.4375 13.4375,13.4375v16.125c0,5.92863 4.82138,10.75 10.75,10.75c5.92863,0 10.75,-4.82137 10.75,-10.75v-16.125h9.86044c0.07794,0.06988 0.13706,0.15319 0.22038,0.22037l21.48656,17.18925c0.98094,0.78475 2.16075,1.18519 3.35131,1.18519c0.79281,0 1.59369,-0.17738 2.3435,-0.5375c1.88125,-0.903 3.05031,-2.76275 3.05031,-4.84825v-18.82325c7.61369,-1.28731 13.4375,-7.90931 13.4375,-15.88312zM61.8125,99.4375v-5.375h5.375v5.375zM96.75,134.375c0,2.96431 -2.41069,5.375 -5.375,5.375c-2.96431,0 -5.375,-2.41069 -5.375,-5.375v-16.125h10.75zM80.625,112.875c-4.44513,0 -8.0625,-3.61738 -8.0625,-8.0625v-16.125c0,-4.44513 3.61737,-8.0625 8.0625,-8.0625h29.5625v32.25h-8.0625zM137.05175,131.46175l-21.48925,-17.19462v-35.04231l10.75,-8.59194v26.11713h5.375v-30.41175l5.375,-4.29462l0.00538,69.42887c0,0 -0.00538,-0.00269 -0.01613,-0.01075zM142.4375,107.11837v-20.73675c4.6225,1.20131 8.0625,5.375 8.0625,10.36838c0,4.99337 -3.44,9.16706 -8.0625,10.36838z" fill="url(#color-2_43971_gr2)"></path><path d="M77.9375,91.375v5.375h5.375v-5.375h5.375v-5.375h-5.375c-2.96431,0 -5.375,2.41069 -5.375,5.375z" fill="url(#color-3_43971_gr3)"></path><path d="M21.5,107.5v-67.1875c0,-4.44512 3.61737,-8.0625 8.0625,-8.0625h88.6875c4.44512,0 8.0625,3.61738 8.0625,8.0625v10.75h5.375v-10.75c0,-7.40944 -6.02806,-13.4375 -13.4375,-13.4375h-88.6875c-7.40944,0 -13.4375,6.02806 -13.4375,13.4375v67.1875c0,7.40944 6.02806,13.4375 13.4375,13.4375h32.25v-5.375h-32.25c-4.44513,0 -8.0625,-3.61737 -8.0625,-8.0625z" fill="url(#color-4_43971_gr4)"></path><path d="M32.25,43h37.625v5.375h-37.625z" fill="url(#color-5_43971_gr5)"></path><path d="M32.25,53.75h37.625v5.375h-37.625z" fill="url(#color-6_43971_gr6)"></path><path d="M32.25,64.5h26.875v5.375h-26.875z" fill="url(#color-7_43971_gr7)"></path><path d="M32.25,75.25h21.5v5.375h-21.5z" fill="url(#color-8_43971_gr8)"></path></g></g></svg>
</div>
<div class="col">
<div class="fs-12 m-t-5 all-caps m-b-5">ChatGPT Prompts</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<div class="row m-b-5" v-if="$store.getters.isAdmin">
<!-- <div class="row m-b-5" v-if="$store.getters.isAdmin">
<div class="col">
<div class="row">
<div class="col">
@@ -311,7 +334,7 @@
</div>
</div>
</div>
</div>
</div> -->
</div>
</div>
</div>
@@ -699,6 +722,42 @@
</div>
</div>
</div>
<div class="row tabsContainer hide tabContent" tab-name="chatGPTPrompts">
<div class="col">
<loading-component style="height: 200px; top: 0;" key="1" color="success" v-show="$store.getters.isLoading('announcementsSection')"></loading-component>
<div class="row" v-show="!$store.getters.isLoading('announcementsSection')">
<div class="col">
<div class="row p-b-5 m-b-20 b-b b-grey align-items-center parentContainer">
<div class="col">
<div class="font-heading all-caps fs-10 hint-text">
ChatGPT Prompts
</div>
</div>
<div class="col-auto">
<button class="btn btn-xs btn-primary b-rad-none requestModal" data-type="createModal">
<i class="fa fa-plus m-r-5"></i>
Create ChatGPT Prompt
</button>
<modal-form-component section="allChatGPTPromptsSection">
<template slot="form" slot-scope="{section}">
<chat-gpt-form-component :section="section"></chat-gpt-form-component>
</template>
</modal-form-component>
</div>
</div>
<div class="row m-b-20">
<div class="col">
<list-component section="allChatGPTPromptsSection" :endpoint="route('api.kvp.prompts.list')" :options="{}">
<template slot="list" slot-scope="{data}">
<single-chat-gpt-prompt-component :data="data"></single-chat-gpt-prompt-component>
</template>
</list-component>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<div class="row tabsContainer hide tabContent" tab-name="milestonesSegment" v-if="$store.getters.isAdmin">
<div class="col">
+3 -1
View File
@@ -77,9 +77,11 @@ Route::group(['middleware' => 'api', 'prefix' => 'v1', 'as' => 'api.'], function
require __DIR__ . '/export.php';
require __DIR__ . '/setting.php';
require __DIR__ . '/affiliate.php';
require __DIR__ . '/key_value_pair.php';
// require __DIR__ . '/rate.php';
// require __DIR__ . '/receipt.php';
});
+14
View File
@@ -0,0 +1,14 @@
<?php
use App\Http\Controllers\KeyValuePairs\CreateChatGPTPromptKVPController;
use App\Http\Controllers\KeyValuePairs\ListChatGPTPromptsKVPController;
use App\Http\Controllers\KeyValuePairs\UpdateChatGPTPromptKVPController;
use App\Http\Controllers\KeyValuePairs\DeleteChatGPTPromptKVPController;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'kvp', 'as' => 'kvp.', 'namespace' => 'KeyValuePairs'], function () {
Route::get('/prompts/list', [ListChatGPTPromptsKVPController::class, 'list'])->name('prompts.list');
Route::put('/prompt/update/{id}', [UpdateChatGPTPromptKVPController::class, 'update'])->name('prompt.update');
Route::post('/prompt/create', [CreateChatGPTPromptKVPController::class, 'create'])->name('prompt.create');
Route::delete('/prompt/delete/{id}', [DeleteChatGPTPromptKVPController::class, 'delete'])->name('prompt.delete');
});
+2
View File
@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Transactions\ETLPurchaseOrderTransactionController;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'transactions', 'namespace' => 'Transactions', 'as' => 'transaction.'], function () {
@@ -22,6 +23,7 @@ Route::group(['prefix' => 'transactions', 'namespace' => 'Transactions', 'as' =>
Route::post('booking/{id}/details/update', 'CreatePurchaseOrderTransactionController@create')->name('po.create');
Route::post('booking/{id}/details/import', 'ImportPurchaseOrderTransactionController@import')->name('po.import');
Route::post('booking/{id}/details/etl', [ETLPurchaseOrderTransactionController::class, 'etl'])->name('po.etl');
Route::get('bulk/po/{issuer_id}/{start_date}/{end_date}', 'CreateBulkPurchaseOrderTransactionController@create')->name('po.bulk.create');