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

This commit is contained in:
Dillon Ngo
2026-01-19 00:04:27 +08:00
85 changed files with 66228 additions and 47 deletions
+2
View File
@@ -80,3 +80,5 @@ SENDING_EMAIL_WELCOME_VOUCHER_ENABLED=true
E_INVOICE_START_DATE="2025-07-01 00:00:00"
MAINTENANCE_MESSAGE_TITLE="" # default empty, e.g. 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=""
Vendored
+27
View File
@@ -23,10 +23,30 @@ pipeline {
steps {
script{
currentBuild.description = 'Step 1 of 6 Completed'
def pusherKeyCredId
def pusherCluster
switch(GIT_BRANCH) {
case "vapor/production":
pusherKeyCredId = 'pusher-prod-key'
pusherCluster = 'ap1'
git(
url: 'https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git',
credentialsId: 'gitlab-jenkins-localhost',
branch: GIT_BRANCH
)
break
case "vapor/staging":
pusherKeyCredId = 'pusher-staging-key'
pusherCluster = 'ap1'
git(
url: 'https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git',
credentialsId: 'gitlab-jenkins-localhost',
branch: GIT_BRANCH
)
break
case "vapor/development":
pusherKeyCredId = 'pusher-dev-key'
pusherCluster = 'ap1'
git(
url: 'https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git',
credentialsId: 'gitlab-jenkins-localhost',
@@ -41,6 +61,13 @@ pipeline {
)
break
}
withCredentials([string(credentialsId: pusherKeyCredId, variable: 'PUSHER_KEY')]) {
env.MIX_PUSHER_APP_KEY = PUSHER_KEY
env.MIX_PUSHER_APP_CLUSTER = pusherCluster
println "Pusher cluster : ${env.MIX_PUSHER_APP_CLUSTER}"
println "Pusher key set : ${env.MIX_PUSHER_APP_KEY}"
}
currentBuild.description = 'Step 2 of 6 Completed'
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use App\Classes\ValueObjects\Constants\KVPKey;
use Illuminate\Database\Eloquent\Builder;
class Prompt implements Filter
{
public static function apply(Builder $builder, $value)
{
return $builder->where('key', 'LIKE', KVPKey::CHATGPT_PROMPT_PREFIX . '%')
->orWhere('key', 'LIKE', KVPKey::CLAUDE_PROMPT_PREFIX . '%');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Classes\Jobs\Commands\V2;
use App\Events\ETLPurchaseOrderTransactionCompleteEvent;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ETLPurchaseOrderTransactionCompleteV2CommandJob implements ShouldQueue
{
use Dispatchable, Queueable, SerializesModels;
public $update;
public $bookingId;
public function __construct(bool $update, int $bookingId)
{
$this->update = $update;
$this->bookingId = $bookingId;
}
public function handle()
{
event(new ETLPurchaseOrderTransactionCompleteEvent($this->update, $this->bookingId));
}
}
@@ -0,0 +1,111 @@
<?php
namespace App\Classes\Jobs\Commands\V2;
use App\Classes\Modules\Transactions\Processors\ETLPDFPurchaseOrderClaudeProcessor;
use App\Events\ETLPurchaseOrderTransactionCompleteEvent;
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\Cache;
use Illuminate\Support\Facades\Log;
class ETLPurchaseOrderTransactionV2CommandJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var array */
private $files;
/** @var int|null */
private $bookingId;
/** @var string */
private $groupId;
/** @var int */
private $batchIndex;
/** @var int */
private $totalBatches;
/** @var bool */
private $isLast;
/**
* constructor.
*
* @param array $files
* @param int|null $bookingId
* @param string $groupId
* @param int $batchIndex
* @param int $totalBatches
* @param bool $isLast
*/
public function __construct(
array $files,
?int $bookingId,
string $groupId,
int $batchIndex,
int $totalBatches,
bool $isLast = false
) {
$this->files = $files;
$this->bookingId = $bookingId;
$this->groupId = $groupId;
$this->batchIndex = $batchIndex;
$this->totalBatches = $totalBatches;
$this->isLast = $isLast;
}
public function handle()
{
Log::info(Carbon::now() . ': Start job - ETL Purchase Order Transaction.');
$start = new Carbon();
Log::info(Carbon::now() . ': Processing - ETL Purchase Order Transaction.', [
'group_id' => $this->groupId,
'batch_index' => $this->batchIndex,
'total_batches' => $this->totalBatches,
'is_last' => $this->isLast,
'file_count' => count($this->files),
]);
$cacheKey = 'etl_finished_jobs_' . $this->groupId;
$lockKey = $cacheKey . '_lock';
$finished = 0;
Cache::lock($lockKey, 300)->block(180, function () use ($cacheKey, &$finished) {
(App()->make(ETLPDFPurchaseOrderClaudeProcessor::class))->execute($this->files, $this->bookingId, $this->batchIndex !== 1);
// Get current counter (default 0)
$current = Cache::get($cacheKey, 0);
$finished = (int)$current + 1;
// Save updated counter
Cache::put($cacheKey, $finished, 3600);
});
Log::info('ETL job finished for group ' . $this->groupId, [
'batch_index' => $this->batchIndex,
'total_batches' => $this->totalBatches,
'finished_counter' => $finished,
'timestamp' => now(),
]);
if ($finished >= $this->totalBatches) {
Log::info('All ETL jobs finished for group ' . $this->groupId . '. Last job finished at ' . now());
event(new ETLPurchaseOrderTransactionCompleteEvent(true, $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,62 @@
<?php
namespace App\Classes\Modules\Anthropic\Services;
use Illuminate\Support\Facades\Http;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ConnectionErrorException;
use Illuminate\Support\Facades\Log;
class CreatesClaudeResponse
{
/**
* @param string $userPrompt
* @param string $model
* @return null|object
* @throws MalformedRequestException
*/
public function execute(string $userPrompt, string $model = "") {
try {
// Default to the latest recommended Claude model
$modelId = $model ?? config('anthropic.default_model', 'claude-sonnet-4-5-20250929');
$data = [
'model' => $modelId,
'messages' => [
[
'role' => 'user',
'content' => $userPrompt
]
]
];
$response = Http::withHeaders([
'x-api-key' => config('anthropic.api_key'),
'anthropic-version' => config('anthropic.api_version', '2023-06-01'),
])->post(config('anthropic.base_url') . '/v1/messages', $data);
if ($response->successful()) {
return (object) $response->json();
} else {
Log::info('CreatesClaudeResponse error: ' . $response->body());
return null;
}
}
catch (\Illuminate\Http\Client\ConnectionException $exception) {
Log::info('ConnectionException' . json_encode($exception));
throw new ConnectionErrorException(
'Failed to connect to Claude API',
$exception->getMessage(),
$userPrompt,
$exception->getTraceAsString()
);
}
catch (\Exception $exception) {
Log::info('Exception' . json_encode($exception));
throw new MalformedRequestException(
'Unable to get correct response from Claude API: ' . $exception->getMessage()
);
}
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Classes\Modules\Anthropic\Services;
use Illuminate\Support\Facades\Http;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ConnectionErrorException;
class CreatesClaudeResponseWithFiles
{
public function execute(string $userPrompt, array $fileIds)
{
try {
$content = [];
// User prompt
$content[] = [
'type' => 'text',
'text' => $userPrompt,
];
// Attach files
foreach ($fileIds as $fileId) {
$content[] = [
'type' => 'document',
'source' => [
'type' => 'file',
'file_id' => $fileId,
],
];
}
$response = Http::withHeaders([
'x-api-key' => config('anthropic.api_key'),
'anthropic-version' => config('anthropic.api_version', '2023-06-01'),
'anthropic-beta' => 'files-api-2025-04-14',
'Content-Type' => 'application/json',
])->post(config('anthropic.base_url') . '/v1/messages', [
'model' => config('anthropic.default_model', 'claude-sonnet-4-5-20250929'),
'max_tokens' => 16384,
'messages' => [
[
'role' => 'user',
'content' => $content,
],
],
]);
return $response->json();
}
catch (\Illuminate\Http\Client\ConnectionException $exception) {
$error = 'Failed to connect to Claude API';
throw new ConnectionErrorException(
$error,
$exception->getMessage(),
$userPrompt,
$exception->getTraceAsString()
);
}
catch (\Exception $exception) {
throw new MalformedRequestException(
'Unable to get correct response from Claude API: ' . $exception->getMessage()
);
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Classes\Modules\Anthropic\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ConnectionErrorException;
class UploadsClaudeFiles
{
public function execute(array $paths): array
{
try {
$ids = [];
foreach ($paths as $path) {
$response = Http::withHeaders([
'x-api-key' => config('anthropic.api_key'),
'anthropic-version' => '2023-06-01',
'anthropic-beta' => 'files-api-2025-04-14',
])->attach(
'file',
fopen($path, 'r'),
basename($path)
)->post(config('anthropic.base_url') . '/v1/files');
$data = $response->json();
if ($response->successful() && isset($data['id'])) {
$ids[] = $data['id'];
} else {
Log::error('Claude file upload failed', [
'path' => $path,
'response' => $data,
]);
}
}
return $ids;
}
catch (\Illuminate\Http\Client\ConnectionException $exception) {
$error = 'Failed to connect to Claude API';
throw new ConnectionErrorException(
$error,
$exception->getMessage(),
$exception->getTraceAsString()
);
}
catch (\Exception $exception) {
throw new MalformedRequestException(
'Unable to get correct response from Claude API: ' . $exception->getMessage()
);
}
}
}
@@ -57,7 +57,7 @@ class DeletePurchaseOrderPdfLogic extends AbstractControllerLogic
{
$booking = $this->fetchesBooking->execute(['id' => $request->route('id')]);
$document = $booking->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->first();
$document = $booking->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->latest()->first();
$this->canDeleteDocument->passes();
@@ -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,68 @@
<?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\CreateClaudePromptDTO;
use App\Classes\Modules\KeyValuePairs\Standards\Rules\CanCreateClaudePrompt;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Http\Resources\ClaudePromptResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateClaudePromptKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Created Claude Prompt',
'message' => 'You have successfully created a new Claude Prompt'
];
}
/** @var CanCreateClaudePrompt */
private $canCreateClaudePrompt;
/** @var CreatesKeyValuePair */
private $createsKeyValuePair;
/**
* CreateClaudePromptKVPLogic constructor.
* @param CanCreateClaudePrompt $canCreateClaudePrompt
* @param CreatesKeyValuePair $createsKeyValuePair
*/
public function __construct(
CanCreateClaudePrompt $canCreateClaudePrompt,
CreatesKeyValuePair $createsKeyValuePair
) {
$this->canCreateClaudePrompt = $canCreateClaudePrompt;
$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 CreateClaudePromptDTO($request->all());
$this->canCreateClaudePrompt->passes($dto);
$key = KVPKey::CLAUDE_PROMPT_PREFIX . strtoupper(str_replace(' ', '_', $dto->getTitle()));
$keyValuePairObject = new KeyValuePairObject($key, $dto->getPrompt());
$kvp = $this->createsKeyValuePair->execute(null, $keyValuePairObject);
return $this->resourceResponse(new ClaudePromptResource($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,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\CanDeleteClaudePrompt;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DeleteClaudePromptKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Delete Claude Prompt',
'message' => 'You have successfully deleted the Claude Prompt'
];
}
/** @var CanDeleteClaudePrompt */
private $canDeleteClaudePrompt;
/** @var FetchesKeyValuePair */
private $fetchesKeyValuePair;
/** @var DeletesKeyValuePair */
private $deletesKeyValuePair;
/**
* DeleteClaudePromptKVPLogic constructor.
* @param CanDeleteClaudePrompt $canDeleteClaudePrompt
* @param FetchesKeyValuePair $fetchesKeyValuePair
* @param DeletesKeyValuePair $deletesKeyValuePair
*/
public function __construct(
CanDeleteClaudePrompt $canDeleteClaudePrompt,
FetchesKeyValuePair $fetchesKeyValuePair,
DeletesKeyValuePair $deletesKeyValuePair
) {
$this->canDeleteClaudePrompt = $canDeleteClaudePrompt;
$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->canDeleteClaudePrompt->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\CanListAiPrompts;
use App\Http\Resources\ChatGPTPromptResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListAiPromptsKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Retrieve Prompts',
'message' => 'You have successfully retrieved a list of Prompts'
];
}
/** @var CanListAiPrompts */
private $canListAiPrompts;
/** @var ListsKeyValuePairs */
private $listsKeyValuePairs;
/**
* ListAiPromptsKVPLogic constructor.
* @param CanListAiPrompts $canListAiPrompts
* @param ListsKeyValuePairs $listsKeyValuePairs
*/
public function __construct(CanListAiPrompts $canListAiPrompts, ListsKeyValuePairs $listsKeyValuePairs)
{
$this->canListAiPrompts = $canListAiPrompts;
$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->canListAiPrompts->passes();
$prompts = $this->listsKeyValuePairs->execute(array_merge($this->listsKeyValuePairs->deserializeFilters($request->input('filters')), ['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,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\UpdateClaudePromptDTO;
use App\Classes\Modules\KeyValuePairs\Standards\Rules\CanUpdateClaudePrompt;
use App\Classes\Modules\Accounts\Services\FetchesKeyValuePair;
use App\Classes\Modules\Accounts\Services\UpdatesKeyValuePair;
use App\Http\Resources\ClaudePromptResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateClaudePromptKVPLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => 'Update Claude Prompt',
'message' => 'You have successfully updated the Claude prompt'
];
}
/** @var CanUpdateClaudePrompt */
private $canUpdateClaudePrompt;
/** @var UpdatesKeyValuePair */
private $updatesKeyValuePair;
/** @var FetchesKeyValuePair */
private $fetchesKeyValuePair;
/**
* UpdateClaudePromptKVPLogic constructor.
* @param CanUpdateClaudePrompt $canUpdateClaudePrompt
* @param UpdatesKeyValuePair $updatesKeyValuePair
* @param FetchesKeyValuePair $fetchesKeyValuePair
*/
public function __construct(CanUpdateClaudePrompt $canUpdateClaudePrompt, UpdatesKeyValuePair $updatesKeyValuePair, FetchesKeyValuePair $fetchesKeyValuePair)
{
$this->canUpdateClaudePrompt = $canUpdateClaudePrompt;
$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 UpdateClaudePromptDTO($request->all());
$this->canUpdateClaudePrompt->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 ClaudePromptResource($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,36 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class CreateClaudePromptDTO 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,44 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class UpdateClaudePromptDTO 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,56 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\KeyValuePairs\Standards\Validators\CreateClaudePromptValidation;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\CreateClaudePromptDTO;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanCreateClaudePrompt extends AbstractRule
{
/** @var CreateClaudePromptValidation */
private $validation;
/**
* CanCreateClaudePrompt constructor.
* @param CreateClaudePromptValidation $validation
*/
public function __construct(CreateClaudePromptValidation $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 CreateClaudePromptDTO $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,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 CanDeleteClaudePrompt 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 CanListAiPrompts 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,58 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\UpdateClaudePromptDTO;
use App\Classes\Modules\KeyValuePairs\Standards\Validators\UpdateClaudePromptValidation;
use App\Classes\ValueObjects\Constants\RoleTypes;
use Illuminate\Support\Facades\Auth;
class CanUpdateClaudePrompt extends AbstractRule
{
/** @var UpdateClaudePromptValidation */
private $validation;
/**
* CanUpdateClaudePrompt constructor.
* @param UpdateClaudePromptValidation $validation
*/
public function __construct(UpdateClaudePromptValidation $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 UpdateClaudePromptDTO $object
* @return bool
* @throws \App\Classes\Exceptions\RequestValidationException
*/
protected function validators($object): bool
{
return $this->validation->validate($object);
}
/**
* @param UpdateClaudePromptDTO $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,41 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Validators;
use App\Classes\General\Abstracts\AbstractValidation;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\CreateClaudePromptDTO;
class CreateClaudePromptValidation extends AbstractValidation
{
/**
* @param CreateClaudePromptDTO $object
* @return array
*/
protected function data($object): array
{
return [
'title' => $object->getTitle(),
'prompt' => $object->getPrompt(),
];
}
/**
* @return array
*/
protected function rules(): array
{
return [
'title' => 'required',
'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,40 @@
<?php
namespace App\Classes\Modules\KeyValuePairs\Standards\Validators;
use App\Classes\General\Abstracts\AbstractValidation;
use App\Classes\Modules\KeyValuePairs\DataTransferObjects\UpdateClaudePromptDTO;
class UpdateClaudePromptValidation extends AbstractValidation
{
/**
* @param UpdateClaudePromptDTO $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,52 @@
<?php
namespace App\Classes\Modules\OpenAI\Services;
use Illuminate\Support\Facades\Http;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ConnectionErrorException;
class CreatesChatGPTResponseWithFiles
{
public function execute(string $userPrompt, array $fileIds)
{
try {
$fileContents = [];
foreach ($fileIds as $fileId) {
$fileContents[] = [
'type' => 'input_file',
'file_id' => $fileId,
];
}
$inputs = array_merge([
[
'type' => 'input_text',
'text' => $userPrompt,
]
], $fileContents);
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('openai.api_key'),
'Content-Type' => 'application/json',
])->post(config('openai.base_url') . '/v1/responses', [
'model' => 'gpt-4.1-mini',
'input' => [
[
'role' => 'user',
'content' => $inputs,
],
],
]);
return $response->json();
}
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,49 @@
<?php
namespace App\Classes\Modules\OpenAI\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Classes\Exceptions\MalformedRequestException;
use App\Classes\Exceptions\ConnectionErrorException;
class UploadsOpenAIFiles
{
public function execute(array $paths): array
{
try {
$ids = [];
foreach ($paths as $path) {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('openai.api_key'),
])->attach(
'file',
fopen($path, 'r'),
basename($path)
)->post(config('openai.base_url') . '/v1/files', [
'purpose' => 'user_data',
]);
$data = $response->json();
if ($response->successful() && isset($data['id'])) {
$ids[] = $data['id'];
} else {
Log::error('File upload failed', [
'path' => $path,
'response' => $data
]);
}
}
return $ids;
}
catch (\Illuminate\Http\Client\ConnectionException $exception) {
$error = 'Failed to connect to ChatGPT API';
throw new ConnectionErrorException($error, $exception->getMessage(), $exception->getTraceAsString());
}
catch (\Exception $exception) {
throw new MalformedRequestException('Unable to get correct response from ChatGPT API: ' . $exception->getMessage());
}
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Classes\Modules\Transactions\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Transactions\Processors\ETLPDFPurchaseOrderClaudeProcessor;
use App\Classes\Modules\Transactions\Standards\Rules\CanETLPurchaseOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
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 ETLPDFPurchaseOrderClaudeProcessor */
private $eTLPDFPurchaseOrderClaudeProcessor;
/**
* ETLPurchaseOrderTransactionLogic constructor.
* @param CanETLPurchaseOrder $canETLPurchaseOrder
* @param ETLPDFPurchaseOrderClaudeProcessor $eTLPDFPurchaseOrderClaudeProcessor
*/
public function __construct(CanETLPurchaseOrder $canETLPurchaseOrder, ETLPDFPurchaseOrderClaudeProcessor $eTLPDFPurchaseOrderClaudeProcessor)
{
$this->canETLPurchaseOrder = $canETLPurchaseOrder;
$this->eTLPDFPurchaseOrderClaudeProcessor = $eTLPDFPurchaseOrderClaudeProcessor;
}
/**
* @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->etlPDFPurchaseOrderTransactionProcessor->execute($products, $productsMetadata);
// } catch (\Exception $exception) {
// Log::info('Exception: ' .json_encode($exception));
// throw new InternalServerErrorException('Something went wrong!');
// }
$files = $request->input('files');
$result = $this->eTLPDFPurchaseOrderClaudeProcessor->execute($files);
return $this->response($result);
}
}
@@ -1,22 +1,31 @@
<?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\Services\CreatesDocument;
use App\Classes\Modules\Documents\Services\DeletesDocument;
use App\Classes\Modules\Documents\Services\CreatesFiles;
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\ETLPDFPurchaseOrderClaudeProcessor;
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;
use App\Classes\ValueObjects\Constants\DocumentType;
use App\Http\Resources\TransactionResource;
use App\Models\Booking;
use App\Models\Document;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
@@ -25,13 +34,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 +53,42 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
/** @var CreatePurchaseOrderTransactionProcessor */
private $createPurchaseOrderTransactionProcessor;
/** @var ETLPDFPurchaseOrderClaudeProcessor */
private $eTLPDFPurchaseOrderClaudeProcessor;
/** @var CanETLPurchaseOrder */
private $canETLPurchaseOrder;
/** @var CreatesDocument */
private $createsDocument;
/** @var DeletesDocument */
private $deletesDocument;
/** @var CreatesFiles */
private $createsFile;
/**
* CreatePurchaseOrderTransactionLogic constructor.
* ImportPurchaseOrderTransactionLogic constructor.
* @param FetchesBooking $fetchesBooking
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
* @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor
* @param ETLPDFPurchaseOrderClaudeProcessor $eTLPDFPurchaseOrderClaudeProcessor
* @param CanETLPurchaseOrder $canETLPurchaseOrder
* @param CreatesDocument $createsDocument
* @param DeletesDocument $deletesDocument
* @param CreatesFiles $createsFile
*/
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor)
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, ETLPDFPurchaseOrderClaudeProcessor $eTLPDFPurchaseOrderClaudeProcessor, CanETLPurchaseOrder $canETLPurchaseOrder, CreatesDocument $createsDocument, DeletesDocument $deletesDocument, CreatesFiles $createsFile)
{
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
$this->eTLPDFPurchaseOrderClaudeProcessor = $eTLPDFPurchaseOrderClaudeProcessor;
$this->canETLPurchaseOrder = $canETLPurchaseOrder;
$this->createsDocument = $createsDocument;
$this->deletesDocument = $deletesDocument;
$this->createsFile = $createsFile;
}
/**
@@ -60,37 +97,113 @@ 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');
$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();
}
// dd($products);
$products = [];
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $request->route('id') ?? $id]);
if ($request->has('products') && is_array($request->input('products'))) {
$this->canETLPurchaseOrder->passes();
$products = $request->input('products');
if(empty($products)){
$files = $request->input('files', []);
$maxSize = 150 * 1024; // 150 KB
$totalSize = 0;
$base64FilesEN = []; // files with "EN" in the name
$base64FilesOther = []; // files without "EN"
foreach ($files as $index => $file) {
if (!isset($file['base64'])) {
Log::info("Missing base64 for file index {$index}");
continue;
}
$fileBase64 = $file['base64'];
$fileName = $file['name'] ?? "file_{$index}";
if (stripos($fileName, 'EN') !== false) {
$base64FilesEN[] = $fileBase64;
if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) {
$cleanBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64);
}
else{
$cleanBase64 = $fileBase64;
}
$fileContent = base64_decode($cleanBase64, true);
if ($fileContent === false) {
Log::info("Invalid Base64 for file index {$index}");
}
$totalSize += strlen($fileContent);
} else {
$base64FilesOther[] = $fileBase64;
}
}
//Keep a copy of all PDF files uploaded include pdf file with 'EN' as part of its filename
$allBase64Files = array_merge($base64FilesEN, $base64FilesOther);
$deleteDocument = $booking->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->latest()->first();
if($deleteDocument){
$this->deletesDocument->execute($deleteDocument);
}
$object = new DocumentObject(DocumentType::ECOMMERCE_PURCHASE_ORDER, $allBase64Files, '', ApprovalStatus::APPROVED, '1688_purchase_orders');
/** @var Document $document */
$document = $this->createsDocument->execute($booking, $object);
$this->createsFile->execute($document, $object);
//ETL only applicable for pdf file with 'EN' as part of its filename
if ($totalSize > 0) {
if ($totalSize > $maxSize) {
// ETLPurchaseOrderTransactionV2CommandJob::dispatch($base64FilesEN, $request->route('id'));
$this->runAsJob($base64FilesEN, $request->route('id'));
$this->notificationMessage = "Please dont refresh the page. The page will automatically update when the process is done.";
// $this->notificationMessage = "Please refresh page in a few minutes";
return $this->response([]);
}
else{
$products = $this->eTLPDFPurchaseOrderClaudeProcessor->execute($base64FilesEN);
}
}
else{
return $this->response([]);
}
}
}
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();
}
}
$billNumber = $this->generatesTransactionBillNumber->execute('PO-');
$total = collect($products)->sum(function($product){
@@ -108,4 +221,93 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
return $this->resourceResponse(new TransactionResource($transaction));
}
private function runAsJob($files, $bookingId){
$maxFilesPerBatch = 4;
$maxBytesPerBatch = 350 * 1024; // 350 KB
$currentBatch = [];
$currentBatchSize = 0;
$fileBatches = [];
foreach ($files as $index => $fileBase64) {
if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) {
$cleanBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64);
}
else{
$cleanBase64 = $fileBase64;
}
$fileContent = base64_decode($cleanBase64, true);
if ($fileContent === false) {
Log::info("Invalid Base64 for file index {$index}");
continue; // skip invalid files
}
$fileSize = strlen($fileContent); // size in bytes
// If adding this file would exceed limits, start a new batch
$wouldExceedFileCount = count($currentBatch) >= $maxFilesPerBatch;
$wouldExceedTotalSize = ($currentBatchSize + $fileSize) > $maxBytesPerBatch;
if ($wouldExceedFileCount || $wouldExceedTotalSize) {
// save current batch
$fileBatches[] = $currentBatch;
// reset
$currentBatch = [];
$currentBatchSize = 0;
}
// Add file to batch
$currentBatch[] = $fileBase64;
$currentBatchSize += $fileSize;
}
// push final remaining batch
if (!empty($currentBatch)) {
$fileBatches[] = $currentBatch;
}
$totalBatches = count($fileBatches);
$groupId = (string) Str::uuid();
foreach ($fileBatches as $index => $batch) {
$isLast = ($index === $totalBatches - 1);
// Calculate total size of this batch
$batchSize = 0;
foreach ($batch as $fileBase64) {
if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) {
$cleanBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64);
}
else{
$cleanBase64 = $fileBase64;
}
$fileContent = base64_decode($cleanBase64, true);
if ($fileContent !== false) {
$batchSize += strlen($fileContent);
}
}
// Log batch info before dispatching job
Log::info("Dispatching Job for Batch #" . ($index + 1), [
'group_id' => $groupId,
'batch_index' => $index + 1,
'total_batches' => $totalBatches,
'is_last' => $isLast,
'files_in_batch' => count($batch),
'batch_size_bytes' => $batchSize,
]);
ETLPurchaseOrderTransactionV2CommandJob::dispatch(
$batch,
$bookingId,
$groupId,
$index + 1,
$totalBatches,
$isLast
)->delay(now()->addSeconds($index * 10));
}
}
}
@@ -53,10 +53,11 @@ class CreatePurchaseOrderTransactionProcessor
/**
* @param Booking $booking
* @param TransactionObject $object
* @param bool $append
* @return Transaction|\Illuminate\Database\Eloquent\Model
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute(Booking $booking, TransactionObject $object){
public function execute(Booking $booking, TransactionObject $object, bool $append = false){
/** @var Transaction $transaction */
$transaction = $booking->transactions()->where('type', TransactionType::PURCHASE_ORDER)->first();
@@ -64,7 +65,9 @@ class CreatePurchaseOrderTransactionProcessor
$this->updatesTransactionStatus->execute($transaction, (float) number_format($object->getAmount(), 2, '.', '') === (float) number_format((float)$booking->fix_amount, 2, '.', '') ? ApprovalStatus::PENDING_VERIFICATION : ApprovalStatus::PENDING_SUBMISSION);
$this->deletesTransactionDetails->execute($transaction);
if(!$append){
$this->deletesTransactionDetails->execute($transaction);
}
foreach ($object->getDetails() as $product){
$this->createsTransactionDetail->execute($transaction, $product);
@@ -73,4 +76,4 @@ class CreatePurchaseOrderTransactionProcessor
return $transaction;
}
}
}
@@ -0,0 +1,193 @@
<?php
namespace App\Classes\Modules\Transactions\Processors;
use App\Classes\Modules\OpenAI\Services\UploadsOpenAIFiles;
use App\Classes\Modules\OpenAI\Services\CreatesChatGPTResponseWithFiles;
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\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;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class ETLPDFPurchaseOrderChatGPTProcessor
{
/** @var FetchesBooking */
private $fetchesBooking;
/** @var GeneratesTransactionBillNumber */
private $generatesTransactionBillNumber;
/** @var CreatePurchaseOrderTransactionProcessor */
private $createPurchaseOrderTransactionProcessor;
/** @var UploadsOpenAIFiles */
private $uploadsOpenAIFiles;
/** @var CreatesChatGPTResponseWithFiles */
private $createsChatGPTResponseWithFiles;
/**
* ETLPDFPurchaseOrderChatGPTProcessor constructor.
* @param FetchesBooking $fetchesBooking
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
* @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor
* @param UploadsOpenAIFiles $uploadsOpenAIFiles
* @param CreatesChatGPTResponseWithFiles $createsChatGPTResponseWithFiles
*/
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, UploadsOpenAIFiles $uploadsOpenAIFiles,
CreatesChatGPTResponseWithFiles $createsChatGPTResponseWithFiles)
{
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
$this->uploadsOpenAIFiles = $uploadsOpenAIFiles;
$this->createsChatGPTResponseWithFiles = $createsChatGPTResponseWithFiles;
}
/**
* @param UploadedFile[] $files
* @param int $bookingId
* @return array|null
*/
public function execute(array $files, ?int $bookingId = null): ?array
{
$tempPaths = [];
try {
foreach ($files as $index => $fileBase64) {
// Strip prefix if present
if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) {
$fileBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64);
}
// Decode Base64
$fileContent = base64_decode($fileBase64);
if ($fileContent === false) {
Log::warning("Invalid Base64 for file index $index");
continue;
}
// Ensure temp directory exists
$tempDir = storage_path('app/documents');
if (!file_exists($tempDir)) {
mkdir($tempDir, 0755, true);
}
// Generate unique temp file path
$fileName = uniqid("file_{$index}_") . '.pdf';
$filePath = $tempDir . '/' . $fileName;
// Save file to disk
file_put_contents($filePath, $fileContent);
// Track temp path
$tempPaths[] = $filePath;
}
Log::info('tempPaths: ' . json_encode($tempPaths));
// Upload to OpenAI
$fileIds = $this->uploadsOpenAIFiles->execute($tempPaths);
$kvp = KeyValuePair::where(
'key',
KVPKey::CHATGPT_PROMPT_PREFIX . 'EXTRACT_1'
)->first();
if (! $kvp) {
throw new ResourceNotFoundException('AI Setup incomplete!');
}
$userPrompt = $kvp->value;
$response = $this->createsChatGPTResponseWithFiles->execute($userPrompt, $fileIds);
// Extract the text content from the first output message
$textContent = $response['output'][0]['content'][0]['text'] ?? '[]';
// Remove ```json fences if present
if (preg_match('/```json\s*(.*?)\s*```/s', $textContent, $matches)) {
$jsonString = $matches[1];
} else {
$jsonString = $textContent;
}
// Decode JSON
$data = json_decode($jsonString, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::error('Failed to decode OpenAI JSON response', [
'error' => json_last_error_msg(),
'textContent' => $textContent
]);
$data = [];
}
// Optional: convert keys to snake_case
$data = array_map(function($item) {
$newItem = [];
foreach ($item as $key => $value) {
$newKey = strtolower(str_replace(' ', '_', $key));
$newItem[$newKey] = $value;
}
return $newItem;
}, $data);
if($bookingId){
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $bookingId]);
$billNumber = $this->generatesTransactionBillNumber->execute('PO-');
$total = collect($data)->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, $data);
$transaction = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object);
}
return $data;
} catch (\Exception $e) {
Log::error('Error processing PDF files with ChatGPT', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
} finally {
// Clean up
foreach ($tempPaths as $path) {
if (file_exists($path)) {
unlink($path);
}
}
}
}
function convertKeysToSnakeCase(array $items): array {
return array_map(function($item) {
$newItem = [];
foreach ($item as $key => $value) {
$newKey = strtolower(str_replace(' ', '_', $key));
$newItem[$newKey] = $value;
}
return $newItem;
}, $items);
}
}
@@ -0,0 +1,186 @@
<?php
namespace App\Classes\Modules\Transactions\Processors;
use App\Classes\Modules\Anthropic\Services\UploadsClaudeFiles;
use App\Classes\Modules\Anthropic\Services\CreatesClaudeResponseWithFiles;
use App\Classes\Exceptions\ResourceNotFoundException;
use App\Classes\Jobs\Commands\V2\ETLPurchaseOrderTransactionCompleteV2CommandJob;
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\ValueObjects\Constants\KVPKey;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Events\ETLPurchaseOrderTransactionCompleteEvent;
use App\Models\KeyValuePair;
use App\Models\Booking;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\UploadedFile;
class ETLPDFPurchaseOrderClaudeProcessor
{
private FetchesBooking $fetchesBooking;
private GeneratesTransactionBillNumber $generatesTransactionBillNumber;
private CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor;
private UploadsClaudeFiles $uploadsClaudeFiles;
private CreatesClaudeResponseWithFiles $createsClaudeResponseWithFiles;
public function __construct(
FetchesBooking $fetchesBooking,
GeneratesTransactionBillNumber $generatesTransactionBillNumber,
CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor,
UploadsClaudeFiles $uploadsClaudeFiles,
CreatesClaudeResponseWithFiles $createsClaudeResponseWithFiles
) {
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
$this->uploadsClaudeFiles = $uploadsClaudeFiles;
$this->createsClaudeResponseWithFiles = $createsClaudeResponseWithFiles;
}
/**
* @param UploadedFile[] $files
* @param int|null $bookingId
* @param bool $append
* @return array|null
*/
public function execute(array $files, ?int $bookingId = null, bool $append = false): ?array
{
$tempPaths = [];
try {
foreach ($files as $index => $fileBase64) {
if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) {
$fileBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64);
}
$fileContent = base64_decode($fileBase64);
if ($fileContent === false) {
Log::warning("Invalid Base64 for file index {$index}");
continue;
}
$tempDir = storage_path('app/documents');
if (!file_exists($tempDir)) {
mkdir($tempDir, 0755, true);
}
$filePath = $tempDir . '/' . uniqid("file_{$index}_") . '.pdf';
file_put_contents($filePath, $fileContent);
$tempPaths[] = $filePath;
}
Log::info('Claude tempPaths', $tempPaths);
// Upload PDFs to Claude
$fileIds = $this->uploadsClaudeFiles->execute($tempPaths);
Log::info('fileIds: '. json_encode($fileIds));
$kvp = KeyValuePair::where(
'key',
KVPKey::CLAUDE_PROMPT_PREFIX . 'EXTRACT_1'
)->first();
if (!$kvp) {
throw new ResourceNotFoundException('AI Setup incomplete!');
}
$userPrompt = $kvp->value;
$response = $this->createsClaudeResponseWithFiles->execute($userPrompt, $fileIds);
Log::info('response: '. json_encode($response));
$textContent = $response['content'][0]['text'] ?? '[]';
// Strip ```json fences
if (preg_match('/```json\s*(.*?)\s*```/s', $textContent, $matches)) {
$jsonString = $matches[1];
} else {
$jsonString = $textContent;
}
$data = json_decode($jsonString, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::info('Failed to decode Claude JSON response', [
'error' => json_last_error_msg(),
'textContent' => $textContent,
]);
$data = [];
}
// Normalize keys to snake_case
$data = $this->convertKeysToSnakeCase($data);
Log::info('data: '. json_encode($data));
if ($bookingId) {
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $bookingId]);
$billNumber = $this->generatesTransactionBillNumber->execute('PO-');
$total = collect($data)->sum(function ($product) {
return $product['quantity']
* floatval(str_replace(',', '', $product['unit_price']));
});
Log::info('total: '. (string) $total);
$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,
$data
);
$this->createPurchaseOrderTransactionProcessor->execute($booking, $object, $append);
}
return $data;
} catch (\Exception $e) {
Log::error('Error processing PDF files with Claude', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
} finally {
foreach ($tempPaths as $path) {
if (file_exists($path)) {
unlink($path);
}
}
}
}
private function convertKeysToSnakeCase(array $items): array
{
return array_map(function ($item) {
$newItem = [];
foreach ($item as $key => $value) {
$newKey = strtolower(str_replace(' ', '_', $key));
$newItem[$newKey] = $value;
}
return $newItem;
}, $items);
}
}
@@ -0,0 +1,195 @@
<?php
namespace App\Classes\Modules\Transactions\Processors;
use App\Classes\Exceptions\ResourceNotFoundException;
use App\Classes\Modules\Anthropic\Services\CreatesClaudeResponse;
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;
//This solution has partial dependency on pdf table extractor on front end (output not ideal)
class ETLPurchaseOrderTransactionProcessor
{
/** @var CreatesChatGPTResponse */
private $createsChatGPTResponse;
/** @var FetchesBooking */
private $fetchesBooking;
/** @var GeneratesTransactionBillNumber */
private $generatesTransactionBillNumber;
/** @var CreatePurchaseOrderTransactionProcessor */
private $createPurchaseOrderTransactionProcessor;
/** @var CreatesClaudeResponse */
private $createsClaudeResponse;
/**
* ETLPurchaseOrderTransactionProcessor constructor.
* @param FetchesBooking $fetchesBooking
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
* @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor
* @param CreatesChatGPTResponse $createsChatGPTResponse
* @param CreatesClaudeResponse $createsClaudeResponse
*/
public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, CreatesChatGPTResponse $createsChatGPTResponse, CreatesClaudeResponse $createsClaudeResponse)
{
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
$this->createsChatGPTResponse = $createsChatGPTResponse;
$this->createsClaudeResponse = $createsClaudeResponse;
}
/**
* @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::CLAUDE_PROMPT_PREFIX . "TRANSLATE_1")->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);
$result = $this->createsClaudeResponse->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 AI service 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::CLAUDE_PROMPT_PREFIX . "EXTRACT_1")->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);
$result = $this->createsClaudeResponse->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;
}
}
@@ -28,6 +28,9 @@ class KVPKey
public const AUTOCOUNT_DOCNO_SALES_DEPOSIT_BY_WALLET_REFUND = 'AUTOCOUNT_DOCNO_SALES_DEPOSIT_BY_WALLET_RF';
public const CHATGPT_PROMPT_PREFIX = 'CHATGPT_PROMPT_';
public const CLAUDE_PROMPT_PREFIX = 'CLAUDE_PROMPT_';
public const BOOKING_AMOUNT_UPDATE = 'BOOKING_AMOUNT_UPDATE';
}
@@ -0,0 +1,44 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ETLPurchaseOrderTransactionCompleteEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* True/false to indicate update status
*/
public $update;
/**
* The booking ID this event is for
*/
public $bookingId;
/**
* @param bool $update
* @param int $bookingId
*/
public function __construct($update, $bookingId)
{
$this->update = $update; // true or false
$this->bookingId = $bookingId; // unique booking identifier
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new Channel('admin.pdf.1688.processing.' . $this->bookingId);
}
}
@@ -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\CreateClaudePromptKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateClaudePromptKVPController
{
/**
* @param Request $request
* @param CreateClaudePromptKVPLogic $logic
* @return JsonResponse
*/
public function create(Request $request, CreateClaudePromptKVPLogic $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\DeleteClaudePromptKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DeleteClaudePromptKVPController
{
/**
* @param Request $request
* @param DeleteClaudePromptKVPLogic $logic
* @return JsonResponse
*/
public function delete(Request $request, DeleteClaudePromptKVPLogic $logic): JsonResponse
{
return $logic->execute($request);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\KeyValuePairs;
use App\Classes\Modules\KeyValuePairs\ControllersLogic\ListAiPromptsKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListAiPromptsKVPController
{
/**
* @param Request $request
* @param ListAiPromptsKVPLogic $logic
* @return JsonResponse
*/
public function list(Request $request, ListAiPromptsKVPLogic $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\KeyValuePairs;
use App\Classes\Modules\KeyValuePairs\ControllersLogic\UpdateClaudePromptKVPLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UpdateClaudePromptKVPController
{
/**
* @param Request $request
* @param UpdateClaudePromptKVPLogic $logic
* @return JsonResponse
*/
public function update(Request $request, UpdateClaudePromptKVPLogic $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'),
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
class ClaudePromptResource 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'),
];
}
}
+1
View File
@@ -28,6 +28,7 @@
"maatwebsite/excel": "^3.1",
"maxbanton/cwh": "^2.0",
"mpdf/mpdf": "^8.1",
"pusher/pusher-php-server": "^7.2",
"rinvex/countries": "^6.1",
"rspective/voucherify": " v2.0.*",
"smalot/pdfparser": "^2.2",
+8
View File
@@ -0,0 +1,8 @@
<?php
return [
'base_url' => env('ANTHROPIC_BASE_URL', 'https://api.anthropic.com'),
'api_key' => env('ANTHROPIC_API_KEY', ''),
'api_version' => env('ANTHROPIC_API_VERSION', '2023-06-01'),
// 'is_enabled' => env('OPENAI_IS_ENABLED', 'true'),
];
+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();
});
}
}
+2
View File
@@ -6,6 +6,8 @@ server {
access_log /var/log/nginx/access.log;
root /var/www/html/public;
client_max_body_size 10M;
server_name localhost;
location / {
Vendored
+11
View File
@@ -95,6 +95,15 @@ gulp.task('sourceJs', () => {
.pipe(gulp.dest(pkg.paths.build.js));
});
// gulp.task('sourceJsKeepNames', () => {
// fancyLog('Compiling source JS Keep Original Names dependencies');
// return gulp.src(pkg.globs.sourceJsKeepNames)
// .pipe(plugins.plumber({ errorHandler: onError }))
// .pipe(plugins.print())
// .pipe(plugins.uglifyEs())
// .pipe(gulp.dest(pkg.paths.build.js));
// });
gulp.task('sourceFonts', () => {
fancyLog('Compiling source font files');
return gulp.src(pkg.globs.sourceFonts)
@@ -109,6 +118,7 @@ gulp.task('clean', () => {
gulp.task('vendor', gulp.series('vendorJs', 'vendorCss', 'vendorFonts', 'vendorFlags', 'vendorImages'));
gulp.task('source', gulp.series('sourceCss', 'sourceImages', 'sourceJs', 'sourceFonts'));
// gulp.task('source', gulp.series('sourceCss', 'sourceImages', 'sourceJs', 'sourceJsKeepNames', 'sourceFonts'));
gulp.task('build', gulp.series('clean', 'vendor', 'source'));
gulp.task('watch', (done) => {
@@ -116,5 +126,6 @@ gulp.task('watch', (done) => {
gulp.watch(pkg.globs.sourceImages, gulp.series('sourceImages'));
gulp.watch(pkg.globs.sourceJs, gulp.series('sourceJs'));
gulp.watch(pkg.globs.sourceJs, gulp.series('sourceFonts'));
// gulp.watch(pkg.globs.sourceJsKeepNames, gulp.series('sourceJsKeepNames'));
done();
});
+7
View File
@@ -22,10 +22,12 @@
"jquery": "^3.7.0",
"jquery.scrollbar": "^0.2.11",
"jwt-decode": "^3.1.2",
"laravel-echo": "^1.19.0",
"laravel-vapor": "^0.6.0",
"noty": "^3.2.0-beta",
"perfect-scrollbar": "^1.5.0",
"popper.js": "^1.12",
"pusher-js": "^8.4.0",
"sass-loader": "10.1.0",
"select2": "^4.0.6-rc.1",
"v-money": "^0.8.1",
@@ -124,6 +126,11 @@
"./resources/assets/js/vanta.birds.min.js",
"./resources/assets/js/scripts.js"
],
"sourceJsKeepNames": [
"./resources/assets/js/pdf.js",
"./resources/assets/js/pdf-table-extractor.js",
"./resources/assets/js/pdf.worker.js"
],
"sourceFonts": [
"./resources/assets/fonts/**/*"
]
+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
+13
View File
@@ -32,6 +32,19 @@ import Vapor from 'laravel-vapor';
// Vapor.withBaseAssetUrl(import.meta.env.VITE_VAPOR_ASSET_URL);
window.Vapor = Vapor;
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
forceTLS: true
});
/** Application Injections */
Vue.use(vuelidate);
Vue.use(VueTheMask);
@@ -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(window.pdfJsUrl);
pdfjsLib.GlobalWorkerOptions.workerSrc = window.pdfWorkerJsUrl;
}
if (typeof pdf_table_extractor === 'undefined') {
await this.loadScript(window.pdfTableExtractorJsUrl);
}
},
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,12 @@
</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> -->
<upload-purchase-order-pdf-v2-form-component :booking-id="data.id" :section="section + 'PDFUpload1'"></upload-purchase-order-pdf-v2-form-component>
</div>
</div>
</div>
</div>
</div>
@@ -325,6 +336,7 @@
interval:false,
submitted: false,
useUploadCsvPo: false,
useUploadPdfPo: false,
product: {
stockCode: '',
description: '',
@@ -0,0 +1,172 @@
<template>
<div class="row" @keyup.enter="submitForm">
<div class="col">
<loading-component style="height: 200px; 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">
<div class="col">
<file-input-component :key="fileInputKey" :validator="$v.parameters.files" v-model="parameters.files" :withMeta="true">
<template slot="label">
<div class="font-heading fs-11 text-primary all-caps">Import Purchase Order PDF</div>
</template>
</file-input-component>
</div>
</div>
<div class="row">
<div class="col">
<button type="button"
class="btn btn-sm btn-block p-t-10 p-b-10 p-r-35 p-l-35 btn-success b-rad-none"
@click="importToSystem"
:disabled="isExporting || isImporting">
{{ isImporting ? 'Uploading...' : 'Upload' }}
</button>
</div>
</div>
<!-- <div class="row m-t-20 justify-content-center" v-if="parameters.files.length > 0">
<div class="col-auto">
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 mx-1"
@click="exportToCSV"
:disabled="isExporting || isImporting">
{{ isExporting ? 'Exporting...' : 'Export to CSV' }}
</button>
</div>
<div class="col-auto">
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 mx-1"
@click="importToSystem"
:disabled="isExporting || isImporting">
{{ isImporting ? 'Uploading...' : 'Continue Upload' }}
</button>
</div>
</div> -->
</div>
</div>
</div>
</div>
</template>
<script>
import { required } from "vuelidate/lib/validators";
import formHandler from '../../../general/mixins/formHandler';
export default {
props: {
section: {
type: String,
required: true
},
bookingId: {
type: Number,
required: true
}
},
data(){
return {
isExporting: false,
isImporting: false,
parameters: {
files: [],
products: []
},
fileInputKey: 0,
}
},
validations: {
parameters: {
files: {
required
}
}
},
methods: {
exportToCSV(){
this.parameters.products = [];
if (this.isExporting) return;
this.isExporting = true;
this.submit(this.route('api.transaction.po.etl', this.bookingId), 'post', this.section + 'PDFExport', true, false)
},
importToSystem() {
if (this.isImporting) return;
this.isImporting = true;
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;
},
resetFileInput() {
this.parameters.files = [];
this.$v.parameters.files.$reset();
this.fileInputKey++;
},
onComplete(data) {
this.$store.dispatch('reloadList', {'name': 'bookingDetailSection'});
this.fileInputKey += 1;
const bookingChannel = `admin.pdf.1688.processing.${data.bookingId}`;
window.Echo.leave(bookingChannel);
},
successHandler(response, section){
if(this.section + 'PDFImport' === section){
if (Array.isArray(response.payload) && response.payload.length === 0) {
const bookingChannel = `admin.pdf.1688.processing.${this.bookingId}`;
window.Echo.channel(bookingChannel)
.listen('ETLPurchaseOrderTransactionCompleteEvent', this.onComplete);
this.isImporting = false;
this.resetFileInput();
}
else{
this.isImporting = false;
this.fileInputKey += 1;
}
this.$store.dispatch('reloadList', {'name': 'bookingDetailSection'});
}
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;
} 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;
},
},
mixins: [formHandler]
}
</script>
@@ -351,7 +351,7 @@
</a>
</div>
</div>
<div class="row m-t-15" v-if="$store.getters.isAdmin && oneSixEightEightServiceIds.includes(booking.service.id) && booking.documents.ecommerce_purchase_order">
<div class="row m-t-15 m-b-25" v-if="$store.getters.isAdmin && oneSixEightEightServiceIds.includes(booking.service.id) && booking.documents.ecommerce_purchase_order">
<div class="col-sm col-md-auto">
<div class="btn btn-sm btn block all-caps b-rad-none btn-danger pointer requestModal" data-type="deletePo">Delete PO</div>
</div>
@@ -363,7 +363,8 @@
<div class="col">
<div class="row" v-if="!booking.documents.ecommerce_purchase_order">
<div class="col">
<upload-purchase-order-pdf-form-component :data="booking" :section="section"></upload-purchase-order-pdf-form-component>
<!-- <upload-purchase-order-pdf-form-component :data="booking" :section="section"></upload-purchase-order-pdf-form-component> -->
<upload-purchase-order-pdf-v2-form-component :booking-id="booking.id" :section="section + 'PDFUpload2'"></upload-purchase-order-pdf-v2-form-component>
</div>
</div>
</div>
@@ -26,7 +26,7 @@
<slot name="tips"></slot>
</div>
</div>
<file-upload-component v-model="files" :value="value" v-on:input="$emit('input', $event)"></file-upload-component>
<file-upload-component v-model="files" :value="value" :withMeta="true" v-on:input="$emit('input', $event)"></file-upload-component>
</div>
</div>
</div>
@@ -40,6 +40,10 @@
type: Array,
required: false
},
withMeta: {
type: Boolean,
default: false
},
validator: {
required: true
}
@@ -50,4 +54,4 @@
}
}
}
</script>
</script>
@@ -65,6 +65,10 @@
value: {
type: Array,
required: false
},
withMeta: {
type: Boolean,
default: false
}
},
data(){
@@ -142,11 +146,22 @@
},
methods: {
fileToBase64(file) {
const vm = this;
return new Promise(resolve => {
var reader = new FileReader();
reader.onload = function(event) {
resolve(event.target.result);
// If meta requested, return object
if (vm.withMeta) {
resolve({
name: file.name,
type: file.type,
size: file.size,
base64: event.target.result
});
} else {
resolve(event.target.result);
}
};
reader.readAsDataURL(file);
@@ -0,0 +1,106 @@
<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" v-if="item.key.startsWith('CHATGPT_PROMPT_')">
<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_EXTRACT_1'">
<i class="fa fa-times"></i>
</button>
</div>
</template>
</div>
<modal-form-component :data="data" size="large" section="allAIPromptsSection">
<template slot="form" slot-scope="{section}">
<ai-prompt-form-component :data="data" :section="section"></ai-prompt-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="allAIPromptsSection" class="text-center"></delete-chat-gpt-form-component>
</modal-component>
</div>
<div class="col-auto parentContainer" v-if="item.key.startsWith('CLAUDE_PROMPT_')">
<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="deleteClaudePrompt" v-if="item.key !== 'CLAUDE_PROMPT_EXTRACT_1'">
<i class="fa fa-times"></i>
</button>
</div>
</template>
</div>
<modal-form-component :data="data" size="large" section="allAIPromptsSection">
<template slot="form" slot-scope="{section}">
<ai-prompt-form-component :data="data" :section="section"></ai-prompt-form-component>
</template>
</modal-form-component>
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="deleteClaudePrompt">
<delete-claude-form-component :data="item" section="allAIPromptsSection" class="text-center"></delete-claude-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,143 @@
<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">
{{ data ? "Update" : "Create" }}
<span v-if="!data">
{{ selectedModel === "chatgpt" ? "ChatGPT" : "Claude" }}
</span>
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" v-if="!data">
<div class="col">
<label>AI Model</label>
<div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
id="chatgptRadio"
value="chatgpt"
v-model="selectedModel"
/>
<label class="form-check-label" for="chatgptRadio">
ChatGPT
</label>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
id="claudeRadio"
value="claude"
v-model="selectedModel"
/>
<label class="form-check-label" for="claudeRadio">
Claude
</label>
</div>
</div>
</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: '',
},
selectedModel: "claude", // default
}
},
validations: {
parameters: {
key: {
required
},
value: {
required
},
}
},
computed:{},
created(){},
methods: {
submitForm() {
let routeUrl;
let method;
if(this.data) {
if (this.data.key.startsWith("CHATGPT_PROMPT_")) {
routeUrl = this.route("api.kvp.chatgpt.prompt.update", this.data.id);
method = "put";
} else if (this.data.key.startsWith("CLAUDE_PROMPT_")) {
routeUrl = this.route("api.kvp.claude.prompt.update", this.data.id);
method = "put";
}
}
else{
if (this.selectedModel === "chatgpt") {
routeUrl = this.route("api.kvp.chatgpt.prompt.create");
method = "post";
} else if (this.selectedModel === "claude") {
routeUrl = this.route("api.kvp.claude.prompt.create");
method = "post";
}
}
this.submit(routeUrl, method, 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.chatgpt.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,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 Claude 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.claude.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">
@@ -286,7 +309,7 @@
</div>
</div>
</div>
</div>
</div> -->
</div>
</div>
</div>
@@ -656,6 +679,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">
AI 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 AI Prompt
</button>
<modal-form-component section="allAIPromptsSection" >
<template slot="form" slot-scope="{section}">
<ai-prompt-form-component :section="section"></ai-prompt-form-component>
</template>
</modal-form-component>
</div>
</div>
<div class="row m-b-20">
<div class="col">
<list-component section="allAIPromptsSection" :endpoint="route('api.kvp.prompts.list')" :options="{}">
<template slot="list" slot-scope="{data}">
<single-prompt-component :data="data"></single-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
View File
@@ -7,6 +7,9 @@
<!-- End Google Tag Manager -->
<script>
window.LARAVEL_VAPOR_ENABLED = @json(env('LARAVEL_VAPOR_ENABLED', false));
// window.pdfJsUrl = "{{ asset('js/pdf.js') }}";
// window.pdfWorkerJsUrl = "{{ asset('js/pdf.worker.js') }}";
// window.pdfTableExtractorJsUrl = "{{ asset('js/pdf-table-extractor.js') }}";
</script>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<meta charset="utf-8"/>
+2
View File
@@ -76,6 +76,8 @@ Route::group(['middleware' => 'api', 'prefix' => 'v1', 'as' => 'api.'], function
require __DIR__ . '/export.php';
require __DIR__ . '/key_value_pair.php';
// require __DIR__ . '/rate.php';
// require __DIR__ . '/receipt.php';
});
+22
View File
@@ -0,0 +1,22 @@
<?php
use App\Http\Controllers\KeyValuePairs\CreateChatGPTPromptKVPController;
use App\Http\Controllers\KeyValuePairs\ListAiPromptsKVPController;
use App\Http\Controllers\KeyValuePairs\UpdateChatGPTPromptKVPController;
use App\Http\Controllers\KeyValuePairs\DeleteChatGPTPromptKVPController;
use App\Http\Controllers\KeyValuePairs\CreateClaudePromptKVPController;
use App\Http\Controllers\KeyValuePairs\UpdateClaudePromptKVPController;
use App\Http\Controllers\KeyValuePairs\DeleteClaudePromptKVPController;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'kvp', 'as' => 'kvp.', 'namespace' => 'KeyValuePairs'], function () {
Route::get('/prompts/list', [ListAiPromptsKVPController::class, 'list'])->name('prompts.list');
Route::put('/chatgpt/prompt/update/{id}', [UpdateChatGPTPromptKVPController::class, 'update'])->name('chatgpt.prompt.update');
Route::post('/chatgpt/prompt/create', [CreateChatGPTPromptKVPController::class, 'create'])->name('chatgpt.prompt.create');
Route::delete('/chatgpt/prompt/delete/{id}', [DeleteChatGPTPromptKVPController::class, 'delete'])->name('chatgpt.prompt.delete');
Route::put('/claude/prompt/update/{id}', [UpdateClaudePromptKVPController::class, 'update'])->name('claude.prompt.update');
Route::post('/claude/prompt/create', [CreateClaudePromptKVPController::class, 'create'])->name('claude.prompt.create');
Route::delete('/claude/prompt/delete/{id}', [DeleteClaudePromptKVPController::class, 'delete'])->name('claude.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');
+3
View File
@@ -15,6 +15,7 @@ environments:
storage: exchange-2.0-production
runtime: 'docker'
timeout: 180
cli-timeout: 300
build:
- 'composer update'
- 'npm install'
@@ -33,6 +34,7 @@ environments:
storage: exchange-2.0-production-duplicate #cief todo: exchange-2.0-staging
runtime: 'docker'
timeout: 180
cli-timeout: 300
build:
- 'composer update'
- 'npm install'
@@ -51,6 +53,7 @@ environments:
storage: exchange-2.0-production-duplicate #cief todo: exchange-2.0-develoopment
runtime: 'docker'
timeout: 180
cli-timeout: 300
build:
- 'composer update'
- 'npm install'