mirror of
https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git
synced 2026-08-19 04:23:55 +00:00
Merge branch 'dillon/122-1688-po-automation-project-pusher' into vapor/production
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -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 [];
|
||||
}
|
||||
}
|
||||
+41
@@ -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 [];
|
||||
}
|
||||
}
|
||||
+40
@@ -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 [];
|
||||
}
|
||||
}
|
||||
+40
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -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);
|
||||
}
|
||||
}
|
||||
+233
-31
@@ -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 don’t 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
@@ -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'),
|
||||
];
|
||||
+32
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
};
|
||||
Vendored
+14074
File diff suppressed because it is too large
Load Diff
Vendored
+47415
File diff suppressed because it is too large
Load Diff
Vendored
+13
@@ -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: '',
|
||||
|
||||
+172
@@ -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>
|
||||
+3
-2
@@ -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>
|
||||
@@ -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
@@ -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"/>
|
||||
|
||||
@@ -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';
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user