Admin Workflow Initial Commit

This commit is contained in:
Dillon Ngo
2024-12-02 23:06:44 +08:00
parent 0824a45b41
commit c1bd61dfc7
54 changed files with 3662 additions and 16 deletions
@@ -0,0 +1,21 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class IsPrevious implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('is_previous', $value);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class QuestionId implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('question_id', $value);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class QuestionNumber implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('question_number', $value);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class QuestionnaireSetId implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('questionnaire_set_id', $value);
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Classes\Modules\Documents\Processors;
use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject;
use App\Classes\Modules\Documents\Services\CreatesDocument;
use App\Classes\Modules\Documents\Services\CreatesFiles;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\DocumentType;
use App\Models\Document;
use App\Models\QAUserAnswerSelected;
class UploadDocumentProcessor
{
/** @var CreatesDocument */
private $createsDocument;
/** @var CreatesFiles */
private $createsFiles;
/**
* UploadDocumentProcessor constructor.
* @param CreatesDocument $createsDocument
* @param CreatesFiles $createsFiles
*/
public function __construct(CreatesDocument $createsDocument, CreatesFiles $createsFiles)
{
$this->createsDocument = $createsDocument;
$this->createsFiles = $createsFiles;
}
/**
* @param $filesUpload
* @param QAUserAnswerSelected qaUserAnswerSelected
* @param string $path
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute($filesUpload, QAUserAnswerSelected $qaUserAnswerSelected, string $path) {
$object = new DocumentObject(DocumentType::ADMIN_WORK_FLOW, $filesUpload, '', ApprovalStatus::PENDING_VERIFICATION, $path);
/** @var Document $document */
$document = $this->createsDocument->execute($qaUserAnswerSelected, $object);
$result = $this->createsFiles->execute($document, $object);
return $result;
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Classes\Modules\Questionnaires\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Questionnaires\Standards\Rules\CanFetchQuestion;
use App\Classes\Modules\Questionnaires\Processors\FetchFirstQuestionV1AdminWFProcessor;
use App\Http\Resources\QuestionResource;
use ErrorException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class FetchQuestionV1AdminWFLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => 'Retrieved Admin Workflow First Question',
'message' => 'You have successfully retrieved a Admin Workflow First Question'
];
}
/** @var CanFetchQuestion */
private $canFetchQuestion;
/** @var FetchFirstQuestionV1AdminWFProcessor */
private $fetchFirstQuestionQAProcessor;
/**
* FetchQuestionV1AdminWFLogic constructor.
* @param CanFetchQuestion $canFetchQuestion
* @param FetchFirstQuestionV1AdminWFProcessor $fetchFirstQuestionQAProcessor
*/
public function __construct(CanFetchQuestion $canFetchQuestion, FetchFirstQuestionV1AdminWFProcessor $fetchFirstQuestionQAProcessor)
{
$this->canFetchQuestion = $canFetchQuestion;
$this->fetchFirstQuestionQAProcessor = $fetchFirstQuestionQAProcessor;
}
/**
* @param Request $request
* @return JsonResponse
* @throws ErrorException
*/
public function logic(Request $request) : JsonResponse
{
$this->canFetchQuestion->passes();
$userId = Auth::user()->id;
$query = $this->fetchFirstQuestionQAProcessor->execute($request);
return $this->resourceResponse(new QuestionResource($query, $userId));
}
}
@@ -0,0 +1,164 @@
<?php
namespace App\Classes\Modules\Questionnaires\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Questionnaires\Standards\Rules\CanFetchQuestion;
use App\Classes\Modules\Questionnaires\Processors\SaveAnswerV1AdminWFProcessor;
use App\Classes\Modules\Questionnaires\Processors\FetchNextQuestionV1AdminWFProcessor;
use App\Classes\Modules\Questionnaires\Processors\FetchFirstQuestionV1AdminWFProcessor;
use App\Classes\Modules\Questionnaires\Services\ListsUserAnswerSelected;
use App\Classes\Modules\Accounts\Services\CreatesKeyValuePair;
use App\Classes\Modules\Accounts\DataTransferObjects\KeyValuePairObject;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use App\Http\Resources\QuestionResource;
use App\Models\Booking;
class UpdateNextQuestionV1AdminWFLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => 'Retrieved Admin Workflow Next Question',
'message' => 'You have successfully retrieved a Admin Workflow Next Question'
];
}
/** @var CanFetchQuestion */
private $canFetchQuestion;
/** @var SaveAnswerV1AdminWFProcessor */
private $saveAnswerProcessor;
/** @var FetchNextQuestionV1AdminWFProcessor */
private $fetchNextQuestionQAProcessor;
/** @var FetchFirstQuestionV1AdminWFProcessor */
private $fetchFirstQuestionQAProcessor;
/** @var ListsUserAnswerSelected */
private $listsUserAnswerSelected;
/** @var CreatesKeyValuePair */
private $createsKeyValuePair;
/**
* UpdateNextQuestionV1AdminWFLogic constructor.
* @param CanFetchQuestion $canFetchQuestion
* @param SaveAnswerV1AdminWFProcessor $saveAnswerProcessor
* @param FetchNextQuestionV1AdminWFProcessor $fetchNextQuestionQAProcessor
* @param FetchFirstQuestionV1AdminWFProcessor $fetchFirstQuestionQAProcessor
* @param ListsUserAnswerSelected $listsUserAnswerSelected
* @param CreatesKeyValuePair $createsKeyValuePair
*/
public function __construct(CanFetchQuestion $canFetchQuestion, SaveAnswerV1AdminWFProcessor $saveAnswerProcessor, FetchNextQuestionV1AdminWFProcessor $fetchNextQuestionQAProcessor, FetchFirstQuestionV1AdminWFProcessor $fetchFirstQuestionQAProcessor, ListsUserAnswerSelected $listsUserAnswerSelected, CreatesKeyValuePair $createsKeyValuePair)
{
$this->canFetchQuestion = $canFetchQuestion;
$this->saveAnswerProcessor = $saveAnswerProcessor;
$this->fetchNextQuestionQAProcessor = $fetchNextQuestionQAProcessor;
$this->fetchFirstQuestionQAProcessor = $fetchFirstQuestionQAProcessor;
$this->listsUserAnswerSelected = $listsUserAnswerSelected;
$this->createsKeyValuePair = $createsKeyValuePair;
}
/**
* @param Request $request
* @return JsonResponse
* @throws MalformedRequestException
* @throws ResourceNotFoundException
*/
public function logic(Request $request) : JsonResponse
{
$questionId = 0;
$userId = Auth::user()->id;
$currentQuestion = $request->question;
$questionId = $currentQuestion['id'];
$questionType = $currentQuestion['question_type'];
$currentQuestionNumber = $currentQuestion['question_number'];
$answerNextQuestionNumber = null;
$answerValue = null;
$timeUsedSeconds = 0;
$reference = null;
$questionMetadata = null;
$this->canFetchQuestion->passes();
Log::info('currentQuestion: '. json_encode($currentQuestion));
if ($request->has('answerObj')) {
$answer = $request->answerObj;
$answerNextQuestionNumber = $answer['next_question_number'];
$answerValue = $answer['value'];
Log::info('answerNextQuestionNumber: '. $answerNextQuestionNumber);
Log::info('answerValue: '. $answerValue);
}
if ($request->has('extra')) {
$extra = $request->extra;
$questionMetadata = $extra['questionMetadata'] ?? null;
$timeUsedSeconds = intval($extra['timeUsedSeconds']) ?? 0;
$reference = $extra['session_id'];
}
//Save answer given by user (both next and previous)
$filesUpload = $request->input('files');
$answerOptionId = 0;
if ($request->has('answerObj')) {
$answerOptionId = intval($request->answerObj['id']);
}
$answerInText = $request->answer;
if ($request->has('isPrevious')){
$answerInText = "go_back";
}
$answer = $this->saveAnswerProcessor->execute($userId, 0, $questionId, $questionType, $questionMetadata, $answerInText, $answerOptionId, $filesUpload, $timeUsedSeconds, $request->has('isPrevious'), $reference);
//Marked data that has already been processed so that it does not appear again
$isNoGoingBack = null;
if($currentQuestion && $currentQuestion['is_end'] === 1){
$booking = Booking::where('id',$questionMetadata['booking']['id'])->first();
$kvp = $answer->attributesKVP()->where('key', 'App\Models\Bank')->where('value', $questionMetadata['booking']['id'])->latest()->first();
$key = "admin_workflow_processed";
if (strpos($currentQuestion['question_number'], '1688') === 0) {
$key = '1688_'.$key;
}
if (strpos($currentQuestion['question_number'], 'fill_po') === 0) {
$key = 'fill_po_'.$key;
}
if (strpos($currentQuestion['question_number'], 'approve_po') === 0) {
$key = 'approve_po_'.$key;
}
if(!$kvp){
$keyValuePairObject = new KeyValuePairObject($key, true);
$this->createsKeyValuePair->execute($booking, $keyValuePairObject);
}
// $isNoGoingBack = 1;
}
$previousAnswer = null;
if ($request->has('extra')) {
$extra = $request->extra;
$reference = $extra['session_id'];
$previousAnswer = $this->listsUserAnswerSelected->execute(['user_id' => $userId, 'reference' => $reference, 'is_previous' => 0, 'order_by' => (object)['column' => 'id','DESC' => true]])[0];
}
//Get returned question (previous or next)
$returnQuestion = $this->fetchNextQuestionQAProcessor->execute($request, $previousAnswer);
//When a questionnaire ended, return back the first question
if(is_null($returnQuestion)){
$returnQuestion = $this->fetchFirstQuestionQAProcessor->execute($request);
}
return $this->resourceResponse(new QuestionResource($returnQuestion, $userId, $request->has('isPrevious'), $isNoGoingBack, $previousAnswer));
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Classes\Modules\Questionnaires\DataTransferObjects;
use App\Classes\General\Interfaces\DataTransferObject;
class QAUserSourceObject implements DataTransferObject
{
/** @var string */
private $system;
/** @var string */
private $marking;
/** @var string */
private $email;
/**
* QAUserSourceObject constructor.
* @param string $system
* @param string $marking
* @param string $email
*/
public function __construct(string $system, string $marking, string $email)
{
$this->system = $system;
$this->marking = $marking;
$this->email = $email;
}
/**
* @return string
*/
public function getSystem(): string
{
return $this->system;
}
/**
* @return string
*/
public function getMarking(): string
{
return $this->marking;
}
/**
* @return string
*/
public function getEmail(): string
{
return $this->email;
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Classes\Modules\Questionnaires\Processors;
use App\Classes\Modules\Questionnaires\Services\FetchesQuestion;
use App\Models\QAQuestionnaireSet;
use ErrorException;
use Illuminate\Http\Request;
class FetchFirstQuestionV1AdminWFProcessor
{
/** @var FetchesQuestion */
private $fetchesQuestion;
/**
* FetchFirstQuestionV1AdminWFProcessor constructor.
* @param FetchesQuestion $fetchesQuestion
*/
public function __construct(FetchesQuestion $fetchesQuestion)
{
$this->fetchesQuestion = $fetchesQuestion;
}
public function execute(Request $request){
try {
$setId = $request->route('set_id');
if($setId === '0'){
$set = QAQuestionnaireSet::where('group', 'workflow')->latest('id')->first();
$setId = $set->id;
}
$query = $this->fetchesQuestion->execute(['questionnaire_set_id' => $setId]);
return $query;
} catch (\Exception $exception){
throw new ErrorException($exception->getMessage(), $exception->getCode());
}
}
}
@@ -0,0 +1,87 @@
<?php
namespace App\Classes\Modules\Questionnaires\Processors;
use App\Classes\Modules\Questionnaires\Services\FetchesQuestion;
use Illuminate\Http\Request;
use App\Classes\ValueObjects\Constants\QAType;
class FetchNextQuestionV1AdminWFProcessor
{
/** @var FetchesQuestion */
private $fetchesQuestion;
/**
* FetchNextQuestionV1AdminWFProcessor constructor.
* @param FetchesQuestion $fetchesQuestion
*/
public function __construct(FetchesQuestion $fetchesQuestion)
{
$this->fetchesQuestion = $fetchesQuestion;
}
public function execute(Request $request, $previousAnswer){
$isEnd = 0;
$questionId = 0;
$nextQuestionNumber = 0;
$nextNestedQuestion = 0;
$nextMainQuestion= 0;
$questionnaireSetId = 0;
$currentQuestion = $request->question;
$questionType = QAType::DEFAULT;
$returnQuestion = null;
if ($currentQuestion && array_key_exists('id', $currentQuestion)) {
$questionId = $currentQuestion['id'];
$isEnd = $currentQuestion['is_end'];
$questionType = $currentQuestion['question_type'];
$nextNestedQuestion = $currentQuestion['next_nested_question'];
$nextMainQuestion = $currentQuestion['next_main_question'];
$questionnaireSetId = $currentQuestion['questionnaire_set_id'];
}
if ($request->has('answerObj') && $request->answerObj['next_question_number']) {
$nextQuestionNumber = $request->answerObj['next_question_number'];
}
if ($request->has('isPrevious')) {
if($previousAnswer)
{
$previousAnswer->delete();
}
else{
return null;
}
//Get previous question
$previousQuestion = $this->fetchesQuestion->execute(['questionnaire_set_id' => $questionnaireSetId, 'id' => $previousAnswer['question_id']]);
$returnQuestion = $previousQuestion;
}
else{
//Get next question
$nextQuestion = null;
$question_number = 0;
if($nextQuestionNumber !== 0){
$question_number = $nextQuestionNumber;
}
else if($nextNestedQuestion !== 0){
$question_number = $nextNestedQuestion;
}
else {
$question_number = $nextMainQuestion;
}
if($question_number !== 0){
$nextQuestion = $this->fetchesQuestion->execute(['questionnaire_set_id' => $questionnaireSetId, 'question_number' => $question_number]);
$returnQuestion = $nextQuestion;
}
else if (array_key_exists('id', $request->input('question'))) {
$nextQuestion = $this->fetchesQuestion->execute(['questionnaire_set_id' => $questionnaireSetId, 'id_next' => $questionId, 'next_main_question' => null]);
$returnQuestion = $nextQuestion;
}
}
return $returnQuestion;
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Classes\Modules\Questionnaires\Processors;
use App\Models\QAUserAnswerSelected;
use App\Classes\Modules\Questionnaires\Services\FetchesUserAnswerSelected;
use App\Classes\Modules\Documents\Processors\UploadDocumentProcessor;
use App\Classes\ValueObjects\Constants\QAType;
class SaveAnswerV1AdminWFProcessor
{
/** @var FetchesUserAnswerSelected */
private $fetchesUserAnswerSelected;
/** @var UploadDocumentProcessor */
private $uploadDocumentForProcessor;
/**
* SaveAnswerV1AdminWFProcessor constructor.
* @param FetchesUserAnswerSelected $fetchesUserAnswerSelected
* @param UploadDocumentProcessor $uploadDocumentForProcessor
*/
public function __construct(FetchesUserAnswerSelected $fetchesUserAnswerSelected, UploadDocumentProcessor $uploadDocumentForProcessor)
{
$this->fetchesUserAnswerSelected = $fetchesUserAnswerSelected;
$this->uploadDocumentForProcessor = $uploadDocumentForProcessor;
}
/**
* @return QAUserAnswerSelected|\Illuminate\Database\Eloquent\Model
*/
public function execute($userId, $sourceId, $questionId, $questionType, $questionMetadata, $answerInText, $answerOptionId, $filesUpload, $timeUsedSeconds, $isPrevious, $reference = null){
$answer = null;
$attachments = null;
if(is_null($answer))
{
$answer = new QAUserAnswerSelected;
}
$answer->user_id = $userId;
$answer->source_id = $sourceId;
$answer->question_id = $questionId;
$answer->question_metadata = json_encode($questionMetadata);
$answer->answer_option_id = $answerOptionId;
$answer->time_used_seconds = $timeUsedSeconds;
$answer->is_previous = $isPrevious;
if($reference){
$answer->reference = $reference;
}
$answer->save(); //cief todo: why 2 save() in this file, this is wrong
if($filesUpload){
$result = $this->uploadDocumentForProcessor->execute($filesUpload, $answer, 'questionnaires');
$attachments = array_map(function ($item) {
return [
'document_id' => $item->document_id,
'file_id' => $item->id,
];
}, $result);
}
if($questionType === QAType::REMARKS_WITH_DOCUMENT_UPLOAD){
$structuredAnswer = [
'text' => $answerInText,
'files' => $attachments,
];
$answer->answer = json_encode($structuredAnswer);
}
else if($questionType === QAType::DOCUMENT_UPLOAD){
$structuredAnswer = [
'files' => $attachments,
];
$answer->answer = json_encode($structuredAnswer);
}
else {
$answer->answer = $answerInText;
}
$answer->save();
return $answer;
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Classes\Modules\Questionnaires\Services;
use App\Classes\General\Eloquent\AbstractUpdateRecord;
use App\Classes\Modules\Questionnaires\DataTransferObjects\QAUserSourceObject;
use App\Models\QAUserSource;
class CreatesQAUserSource extends AbstractUpdateRecord
{
/**
* @param QAUserSourceObject $object
* @return \Illuminate\Database\Eloquent\Model
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute(QAUserSourceObject $object) {
$model = new QAUserSource();
$model->system = $object->getSystem();
$model->marking = $object->getMarking();
$model->email = $object->getEmail();
return $this->handler($model);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\Questionnaires\Services;
use App\Classes\General\Eloquent\AbstractFetchRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\QAUserSource;
class FetchesQAUserSource extends AbstractFetchRecord
{
/** @var QAUserSource */
private $repository;
/**
* FetchesQAUserSource constructor.
* @param QAUserSource $repository
*/
public function __construct(QAUserSource $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\Questionnaires\Services;
use App\Classes\General\Eloquent\AbstractFetchRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\QAQuestions;
class FetchesQuestion extends AbstractFetchRecord
{
/** @var QAQuestions */
private $repository;
/**
* FetchesQuestion constructor.
* @param QAQuestions $repository
*/
public function __construct(QAQuestions $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\Questionnaires\Services;
use App\Classes\General\Eloquent\AbstractFetchRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\QAUserAnswerSelected;
class FetchesUserAnswerSelected extends AbstractFetchRecord
{
/** @var QAUserAnswerSelected */
private $repository;
/**
* FetchesUserAnswerSelected constructor.
* @param QAUserAnswerSelected $repository
*/
public function __construct(QAUserAnswerSelected $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\Questionnaires\Services;
use App\Classes\General\Eloquent\AbstractListRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\QAQuestions;
class ListsQuestions extends AbstractListRecord
{
/** @var QAQuestions */
private $repository;
/**
* ListsQuestions constructor.
* @param QAQuestions $repository
*/
public function __construct(QAQuestions $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Classes\Modules\Questionnaires\Services;
use App\Classes\General\Eloquent\AbstractListRecord;
use Illuminate\Database\Eloquent\Builder;
use App\Models\QAUserAnswerSelected;
class ListsUserAnswerSelected extends AbstractListRecord
{
/** @var QAUserAnswerSelected */
private $repository;
/**
* ListsUserAnswerSelected constructor.
* @param QAUserAnswerSelected $repository
*/
public function __construct(QAUserAnswerSelected $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Classes\Modules\Questionnaires\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
class CanFetchQuestion extends AbstractRule
{
/**
* @return bool
*/
protected function authorized($object): bool
{
// TODO Set Authorization rules
return true;
}
/**
* @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,45 @@
<?php
namespace App\Classes\Modules\Questionnaires\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\ValueObjects\Constants\RoleTypes;
class CanListQuestions extends AbstractRule
{
/**
* @return bool
*/
protected function authorized($object): bool
{
$roleToCheck = Auth()->user()->type;
if (in_array($roleToCheck, RoleTypes::ADMIN_ROLES)) {
return true;
} else {
return false;
}
}
/**
* @param $object
* @return bool
*/
protected function validators($object): bool
{
return true;
}
/**
* @param $object
* @return bool
*/
protected function criteria($object): bool
{
return true;
}
}
@@ -2,6 +2,7 @@
namespace App\Classes\ValueObjects\Constants;
final class DocumentType {
public const PROFILE_PICTURE = 'PROFILE_PICTURE';
@@ -27,4 +28,5 @@ final class DocumentType {
public const BULK_PURCHASE_ORDER = 'BULK_PURCHASE_ORDER';
public const BILL_GROUP_PAYMENT_PROOF = 'BILL_GROUP_PAYMENT_PROOF';
public const ADMIN_WORK_FLOW = 'ADMIN_WORK_FLOW';
}
@@ -0,0 +1,25 @@
<?php
namespace App\Classes\ValueObjects\Constants;
final class QAType {
public const DEFAULT = 0;
public const MULTIPLE_CHOICES = 1;
// public const FREE_TEXT = 2;
public const DOCUMENT_UPLOAD = 3;
// public const ANSWER = 4;
// public const TICKET_CRM = 5;
// public const RATING_5_STARS = 6;
public const REMARKS_WITH_DOCUMENT_UPLOAD = 7;
public const SUBMIT_1688_3_TYPES_DOCUMENTS = 8;
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Questionnaires;
use App\Classes\Modules\Questionnaires\ControllersLogic\FetchQuestionV1AdminWFLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class FetchQuestionV1AdminWFController
{
/**
* @param Request $request
* @param FetchQuestionV1AdminWFLogic $logic
* @return JsonResponse
*/
public function fetch(Request $request, FetchQuestionV1AdminWFLogic $logic): JsonResponse {
return $logic->execute($request);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Questionnaires;
use App\Classes\Modules\Questionnaires\ControllersLogic\UpdateNextQuestionV1AdminWFLogic;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Models\QAQuestionnaireSet;
class UpdateNextQuestionV1AdminWFController
{
/**
* @param Request $request
* @param FetchQAQuestionListLogic $logic
* @return JsonResponse
*/
public function fetch(Request $request, UpdateNextQuestionV1AdminWFLogic $logic): JsonResponse {
// To deploy different logic by versioning
// $currentQuestion = $request->question;
// $questionnaireSetId = $currentQuestion['questionnaire_set_id'];
// $questionnaireSet = QAQuestionnaireSet::where('id', $questionnaireSetId)->first();
// $group = '';
// $version = 0;
// if($questionnaireSet){
// $group = $questionnaireSet->group;
// $version = $questionnaireSet->version;
// }
return $logic->execute($request);
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class AnswerOptionsResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'display_text' => $this->display_text,
'value' => $this->value,
'order' => $this->order,
'question_number' => $this->question_number,
'next_question_number' => $this->next_question_number,
];
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Models\QAQuestions;
use App\Models\QAAnswerOptions;
use App\Models\QAUserAnswerSelected;
class QuestionResource extends JsonResource
{
/** @var integer */
private $userId;
/** @var QAQuestions*/
private $question;
/** @var bool */
private $isPrevious;
/** @var integer */
private $isNoGoingBack;
/** @var integer */
private $previousAnswer;
public function __construct($question, $userId, $isPrevious = false, $isNoGoingBack = null, $previousAnswer = null) {
$this->question = $question;
$this->userId = $userId;
$this->isPrevious = $isPrevious;
$this->isNoGoingBack = $isNoGoingBack;
$this->previousAnswer = $previousAnswer;
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
if($this->question){
$q = (object)$this->question;
return [
'id' => $q->id,
'question_number' => $q->question_number,
'question_title' => $q->question_title,
'question_description' => $q->question_description,
'question_type' => $q->question_type,
'question_answers' => AnswerOptionsResource::collection(QAAnswerOptions::where('question_number', $q->question_number)->where('questionnaire_set_id', $q->questionnaire_set_id)->orderBy('order', 'ASC')->get()),
'questionnaire_set_id' => $q->questionnaire_set_id,
// 'questionnaire' => new QuestionnaireSetsResource($q->questionnaire),
// 'questionnaire_answers' => $q->is_end ? QuestionnaireAnswersResource::collection(QAUserAnswerSelected::where('user_id', $this->userId)->get()) : null,
'next_nested_question' => $q->next_nested_question,
'next_main_question' => $q->next_main_question,
'is_start' => $q->is_start,
'is_end' => $q->is_end,
'end_text' => $q->end_text,
'order' => $q->order,
'answer' => $this->previousAnswer,
'url' => $q->url,
'is_no_going_back' => $this->isNoGoingBack,
'is_previous' => $this->isPrevious,
];
}
else{
return [];
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Models\QAQuestions;
class QuestionnaireAnswersResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$question = QAQuestions::where('id', $this->question_id)->first();
return [
'question_title' => $question ? $question->question_title : null,
'answer' => $this->answer
];
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class QuestionnaireSetsResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
// 'name' => $this->name,
// 'description' => $this->description,
'group' => $this->group,
'version' => $this->version,
];
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Models\QAAnswerOptions;
use App\Models\QAQuestions;
use Carbon\Carbon;
class QuestionsAnswersResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$question = QAQuestions::where('id', $this->question_id)->first();
$source = new UserSourceResource($this->userSource);
$user = $this->source_id === 0 ? new UserResource($this->user) : null;
$answerOption = QAAnswerOptions::where('id', $this->answer_option_id)->first();
$user_marking = '';
if($user){
$companyModule = $user->companyModule()->first();
$user_marking = $companyModule ? $companyModule->inviters()->withPivot('invitee_reference')->first()->pivot->invitee_reference : "";
}
return [
'question_id' => $this->question_id,
'questionnaire' => new QuestionnaireSetsResource($this->question->questionnaire),
'question_title' => $question ? $question->question_title : null,
'answer' => $answerOption ? $answerOption->display_text : null,
'answer_value' => $answerOption ? $answerOption->value : null,
'source_system' => null,
'source_marking' => $user ? $user_marking : $source->marking,
'source_email' => $user ? $user->email : $source->email,
'created_at' => Carbon::parse($this->created_at)->format('d-m-Y'),
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserSourceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'system' => $this->system,
'email' => $this->email,
'marking' => $this->marking,
];
}
}
+10 -2
View File
@@ -2,10 +2,12 @@
namespace App\Models;
use App\Classes\General\Interfaces\Documentable;
use App\Classes\General\Interfaces\Transactionable;
use App\Classes\General\Traits\LogData;
use App\Classes\ValueObjects\Constants\RoleTypes;
use App\Classes\General\Interfaces\KeyValueInterface;
use App\Scopes\CustomerBookingsScope;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
@@ -25,7 +27,7 @@ use Staudenmeir\EloquentHasManyDeep\HasRelationships;
* @property int convertible_currency_id
* @property int conversion_currency_id
*/
class Booking extends AbstractModel implements Documentable, Transactionable
class Booking extends AbstractModel implements Documentable, Transactionable, KeyValueInterface
{
use HasRelationships;
use SoftDeletes;
@@ -121,5 +123,11 @@ class Booking extends AbstractModel implements Documentable, Transactionable
}
}
/**
* @return MorphMany
*/
public function attributesKVP(): MorphMany
{
return $this->morphMany(KeyValuePair::class, 'owner');
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Models;
class QAAnswerOptions extends AbstractModel
{
protected $table = 'qa_answer_options';
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Models;
class QAQuestionnaireSet extends AbstractModel
{
protected $table = 'qa_questionnaire_sets';
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class QAQuestions extends AbstractModel
{
protected $table = 'qa_questions';
/**
* @return BelongsTo
*/
public function questionnaire(): BelongsTo
{
return $this->BelongsTo(QAQuestionnaireSet::class, 'questionnaire_set_id', 'id');
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Classes\General\Interfaces\Documentable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Classes\General\Interfaces\KeyValueInterface;
/**
* Class Contact
* @package App\Models
*/
class QAUserAnswerSelected extends AbstractModel implements Documentable, KeyValueInterface
{
protected $table = 'qa_user_answer_selected';
use SoftDeletes;
/**
* @return MorphMany
*/
public function documents(): morphMany
{
return $this->morphMany(Document::class, 'owner');
}
/**
* @return BelongsTo
*/
public function userSource(): BelongsTo
{
return $this->BelongsTo(QAUserSource::class, 'source_id', 'id');
}
/**
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->BelongsTo(User::class, 'user_id', 'id');
}
/**
* @return BelongsTo
*/
public function question(): BelongsTo
{
return $this->BelongsTo(QAQuestions::class, 'question_id', 'id');
}
/**
* @return BelongsTo
*/
public function answer(): BelongsTo
{
return $this->BelongsTo(QAAnswerOptions::class, 'answer_option_id', 'id');
}
/**
* @return MorphMany
*/
public function attributesKVP(): MorphMany
{
return $this->morphMany(KeyValuePair::class, 'owner');
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace App\Models;
class QAUserSource extends AbstractModel
{
protected $table = 'qa_user_source';
}
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeValueColumnToTextInKeyValuePairsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('key_value_pairs', function (Blueprint $table) {
$table->text('value')->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('key_value_pairs', function (Blueprint $table) {
$table->string('value', 191)->change();
});
}
}
@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQAAnswerOptionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('qa_answer_options', function (Blueprint $table) {
$table->id();
$table->text('display_text');
$table->string('value');
$table->unsignedBigInteger('order')->default(0);
$table->string('question_number');
$table->string('next_question_number')->nullable();
$table->unsignedBigInteger('questionnaire_set_id');
$table->timestamps();
$table->softDeletes();
$table->foreign('questionnaire_set_id')->references('id')->on('qa_questionnaire_sets');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('qa_answer_options');
}
}
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQAQuestionnaireSetsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('qa_questionnaire_sets', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('description')->nullable();
$table->string('group')->nullable();
$table->unsignedBigInteger('order')->default(0);
$table->unsignedBigInteger('next_set')->nullable();
$table->unsignedBigInteger('version')->default(1);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('qa_questionnaire_sets');
}
}
@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQAQuestionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('qa_questions', function (Blueprint $table) {
$table->id();
$table->string('question_number');
$table->text('question_title');
$table->text('question_description')->nullable();
$table->unsignedBigInteger('questionnaire_set_id');
$table->string('next_nested_question')->nullable();
$table->string('next_main_question')->nullable();
$table->unsignedBigInteger('question_type');
$table->tinyInteger('is_start');
$table->tinyInteger('is_end');
$table->string('end_text')->nullable();
$table->unsignedBigInteger('order')->default(0);
$table->string('url')->nullable();
$table->timestamps();
$table->softDeletes();
$table->foreign('questionnaire_set_id')->references('id')->on('qa_questionnaire_sets');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('qa_questions');
}
}
@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQAUserAnswerSelectedTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('qa_user_answer_selected', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->default(0);
$table->unsignedBigInteger('source_id')->default(0);
$table->unsignedBigInteger('question_id');
$table->text('question_metadata')->nullable();
$table->unsignedBigInteger('answer_option_id');
$table->text('answer')->nullable();
$table->string('reference')->nullable();
$table->integer('time_used_seconds')->nullable();
$table->tinyInteger('is_previous')->default(0);
$table->timestamps();
$table->softDeletes();
$table->foreign('question_id')->references('id')->on('qa_questions');
// $table->foreign('answer_option_id')->references('id')->on('qa_answer_options');
// $table->foreign('user_id')->references('id')->on('users');
// $table->foreign('source_id')->references('id')->on('qa_user_source');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('qa_user_answer_selected');
}
}
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQaUserSourceTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('qa_user_source', function (Blueprint $table) {
$table->id();
$table->string('system');
$table->string('email');
$table->string('marking');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('qa_user_source');
}
}
+3
View File
@@ -48,6 +48,9 @@ class DatabaseSeeder extends Seeder
$this->call(DummyDataSeeder::class);
}
// Admin Work Flow
// $this->call(QAWorkFlow2Seeder::class);
// DB::commit();
}
}
+437
View File
@@ -0,0 +1,437 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\QAQuestionnaireSet;
use App\Models\QAQuestions;
use App\Models\QAAnswerOptions;
use App\Classes\ValueObjects\Constants\QAType;
class QAWorkFlow2Seeder extends Seeder
{
public function run()
{
$questionnaireSets = [
[
'name' => 'Admin Work Flow',
'description' => 'Admin Work Flow',
'group' => 'workflow',
'version' => 1,
'questions' => [
[
'question_number' => 'node_0',
'question_title' => 'Are you ready to work today?',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => true,
'is_end' => false,
'answer_options' => [
['display_text' => 'Yes', 'value' => 'start_work', 'next_question_number' => 'start_work'],
['display_text' => 'No', 'value' => 'no_work', 'next_question_number' => 'no_work'],
],
],
[
'question_number' => 'start_work',
'question_title' => 'What will you work on?',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => '1688', 'value' => '1688', 'next_question_number' => '1688'],
['display_text' => 'Approve PO', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
['display_text' => 'Fill PO', 'value' => 'fill_po', 'next_question_number' => 'fill_po'],
],
],
[
'question_number' => 'no_work',
'question_title' => 'Come back when you are ready to work',
'question_type' => QAType::DEFAULT,
'is_start' => false,
'is_end' => true,
],
[
'question_number' => '1688',
'question_title' => '1688',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => 'api.admin_work_flow.fetch_oldest_order', //'http://localhost:8082/api/v1/admin-work-flow/fetch-oldest-order', //this.route("api.admin_work_flow.fetch_oldest_order"),
'is_start' => true,
'is_end' => false,
'answer_options' => [
['display_text' => 'Login Issue', 'value' => '1688_login_issue', 'btn_color' => 'warning', 'next_question_number' => '1688_login_issue'],
['display_text' => 'Login Successful', 'value' => '1688_login_successful', 'next_question_number' => '1688_login_successful'],
],
],
[
'question_number' => '1688_login_issue',
'question_title' => '1688_login_issue',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Need TAC', 'value' => 'Need Tac', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'Wrong login details', 'value' => 'Wrong Login Details', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'Others', 'value' => '1688_login_issue_others', 'next_question_number' => '1688_login_issue_others'],
['display_text' => 'Order Cancelled', 'value' => '1688_refund_request', 'next_question_number' => '1688_refund_request'],
],
],
[
'question_number' => '1688_login_successful',
'question_title' => '1688_login_successful',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => 'api.admin_work_flow.fetch_model_attributes', //'http://localhost:8082/api/v1/admin-work-flow/{booking_id}/fetch-model-attributes',
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => "Can't verify order?", 'value' => '1688_login_successful_cannot_verify', 'next_question_number' => '1688_login_successful_cannot_verify'],
['display_text' => 'Order Verified', 'value' => '1688_order_verify', 'next_question_number' => '1688_order_verify'],
],
],
[
'question_number' => '1688_login_successful_cannot_verify',
'question_title' => '1688_login_successful_cannot_verify',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Amount not found', 'value' => 'Amount not found', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'Plus Member', 'value' => 'Plus Member', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'Others', 'value' => '1688_login_successful_cannot_verify_others', 'next_question_number' => '1688_login_successful_cannot_verify_others'],
['display_text' => 'Customer did not verify 1688 account', 'value' => 'Customer did not verify 1688 account', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'WorldFirst account linked another account', 'value' => 'WorldFirst account linked another account', 'next_question_number' => '1688_issue_submit'],
],
],
[
'question_number' => '1688_login_successful_cannot_verify_others',
'question_title' => 'What other issues did you encounter? Upload documents if needed',
'question_type' => QAType::REMARKS_WITH_DOCUMENT_UPLOAD,
'next_nested_question' => '1688_issue_submit',
'is_start' => false,
'is_end' => false,
],
[
'question_number' => '1688_order_verify',
'question_title' => 'Order Amount',
'question_description' => 'Please key in the order amount',
'question_' => 'Order Amount',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => 'api.admin_work_flow.fetch_model_attributes',
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Yes', 'value' => '1688_order_verification', 'next_question_number' => '1688_order_verification'],
],
],
[
'question_number' => '1688_order_verification',
'question_title' => 'Are You Sure this is the correct amount?',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Yes', 'value' => '1688_proceed_order', 'next_question_number' => '1688_proceed_order'],
['display_text' => 'No', 'value' => 'insufficient_order', 'next_question_number' => 'insufficient_order'],
],
],
[
'question_number' => '1688_proceed_order',
'question_title' => 'Proceed the order on 1688?',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Yes', 'value' => '1688_submit', 'next_question_number' => '1688_submit'],
['display_text' => 'Got Issue', 'value' => '1688_proceed_order_issue', 'next_question_number' => '1688_proceed_order_issue'],
],
],
[
'question_number' => '1688_proceed_order_issue',
'question_title' => '1688_proceed_order_issue',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Wrong Pin Number', 'value' => 'Wrong Pin Number', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'Not Enough Stock', 'value' => 'Not Enough Stock', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'Others', 'value' => 'Others', 'next_question_number' => '1688_proceed_order_issue_others'],
['display_text' => 'No CrossBoarder', 'value' => 'No CrossBoarder', 'next_question_number' => '1688_issue_submit'],
['display_text' => 'AngPau', 'value' => 'AngPau', 'next_question_number' => '1688_issue_submit'],
],
],
[
'question_number' => '1688_proceed_order_issue_others',
'question_title' => 'What other issues did you encounter? Upload documents if needed',
'question_type' => QAType::REMARKS_WITH_DOCUMENT_UPLOAD,
'next_nested_question' => '1688_issue_submit',
'is_start' => false,
'is_end' => false,
],
[
'question_number' => '1688_login_issue_others',
'question_title' => 'What other issues did you encounter? Upload documents if needed',
'question_type' => QAType::REMARKS_WITH_DOCUMENT_UPLOAD,
'next_nested_question' => '1688_issue_submit',
'is_start' => false,
'is_end' => false,
],
[
'question_number' => '1688_submit',
'question_title' => '1688_submit',
'question_type' => QAType::SUBMIT_1688_3_TYPES_DOCUMENTS,
'next_nested_question' => '1688_issue_submit',
'is_start' => false,
'is_end' => false,
],
[
'question_number' => '1688_issue_submit',
'question_title' => 'Issue has been submitted',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next Order', 'value' => '1688', 'next_question_number' => '1688'],
],
],
[
'question_number' => '1688_refund_request',
'question_title' => 'Refund Request sent',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next Order', 'value' => '1688', 'next_question_number' => '1688'],
],
],
[
'question_number' => 'approve_po',
'question_title' => 'approve_po',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => "api.admin_work_flow.fetch_pending_approve_po",
'is_start' => true,
'is_end' => false,
'answer_options' => [
['display_text' => 'Approve', 'value' => 'approve_po_approved', 'next_question_number' => 'approve_po_approved'],
['display_text' => 'Edit PO', 'value' => 'approve_po_edit', 'next_question_number' => 'approve_po_edit'],
['display_text' => 'Reject', 'value' => 'approve_po_reject', 'next_question_number' => 'approve_po_reject'],
],
],
[
'question_number' => 'fill_po',
'question_title' => 'fill_po',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => "api.admin_work_flow.fetch_pending_fill_po",
'is_start' => true,
'is_end' => false,
'answer_options' => [
['display_text' => 'Edit PO', 'value' => 'fill_po_edit', 'next_question_number' => 'fill_po_edit'],
],
],
[
'question_number' => 'approve_po_filled',
'question_title' => 'PO Filled',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next PO (Fill)', 'value' => 'fill_po', 'next_question_number' => 'fill_po'],
['display_text' => 'Next PO (Approve)', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
],
],
[
'question_number' => 'approve_po_approved',
'question_title' => 'PO Approved',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next PO (Fill)', 'value' => 'fill_po', 'next_question_number' => 'fill_po'],
['display_text' => 'Next PO (Approve)', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
],
],
[
'question_number' => 'approve_po_edit',
'question_title' => 'approve_po_edit',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => "api.booking.show",
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Issue?', 'value' => 'approve_po_edit_po_issue', 'next_question_number' => 'approve_po_edit_po_issue'],
['display_text' => 'Done', 'value' => 'approve_po_filled', 'next_question_number' => 'approve_po_filled'],
],
],
[
'question_number' => 'approve_po_reject',
'question_title' => 'PO Rejected',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Sensitive Goods', 'value' => 'Sensitive Goods'],
['display_text' => 'Others', 'value' => 'PO Others', 'next_question_number' => 'reject_po_others'],
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next Order', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
],
],
[
'question_number' => 'reject_po_others',
'question_title' => 'What other issues did you encounter? Upload documents if needed',
'question_type' => QAType::REMARKS_WITH_DOCUMENT_UPLOAD,
'next_nested_question' => 'reject_po_others_complete',
'is_start' => false,
'is_end' => false,
],
[
'question_number' => 'reject_po_others_complete',
'question_title' => 'Issue has been submitted',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next PO (Fill)', 'value' => 'fill_po', 'next_question_number' => 'fill_po'],
['display_text' => 'Next PO (Approve)', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
],
],
[
'question_number' => 'approve_po_edit_po_issue',
'question_title' => 'approve_po_edit_po_issue',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Sensitive Goods', 'value' => 'Sensitive Goods', 'next_question_number' => 'fill_po_issue_submit'],
['display_text' => 'Others', 'value' => 'fill_po_issue_others', 'next_question_number' => 'fill_po_issue_others'],
],
],
[
'question_number' => 'fill_po_issue_others',
'question_title' => 'What other issues did you encounter? Upload documents if needed',
'question_type' => QAType::REMARKS_WITH_DOCUMENT_UPLOAD,
'next_nested_question' => 'fill_po_issue_submit',
'is_start' => false,
'is_end' => false,
],
[
'question_number' => 'fill_po_issue_submit',
'question_title' => 'Issue has been submitted',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next PO (Fill)', 'value' => 'fill_po', 'next_question_number' => 'fill_po'],
['display_text' => 'Next PO (Approve)', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
],
],
[
'question_number' => 'stop_working',
'question_title' => 'Thank you. Reload page to restart.',
'question_type' => QAType::DEFAULT,
'is_start' => false,
'is_end' => true,
],
[
'question_number' => 'insufficient_order',
'question_title' => 'Are You Sure this is the correct amount?',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Underpaid Order', 'value' => 'underpaird_order_1', 'next_question_number' => ''],
['display_text' => 'Underpaid Order', 'value' => 'underpaird_order_2', 'next_question_number' => ''],
['display_text' => 'Overpaid Order', 'value' => 'underpaird_order_2', 'next_question_number' => ''],
],
],
[
'question_number' => 'fill_po_edit',
'question_title' => 'fill_po_edit',
'question_type' => QAType::MULTIPLE_CHOICES,
'url' => "api.booking.show",
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Issue?', 'value' => 'fill_po_edit_issue', 'next_question_number' => 'fill_po_edit_issue'],
['display_text' => 'Done', 'value' => 'fill_po_edit_filled', 'next_question_number' => 'fill_po_edit_filled'],
],
],
[
'question_number' => 'fill_po_edit_issue',
'question_title' => 'fill_po_edit_issue',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => false,
'answer_options' => [
['display_text' => 'Sensitive Goods', 'value' => 'Sensitive Goods', 'next_question_number' => 'fill_po_issue_submit'],
['display_text' => 'Others', 'value' => 'fill_po_issue_others', 'next_question_number' => 'fill_po_issue_others'],
],
],
[
'question_number' => 'fill_po_edit_filled',
'question_title' => 'PO Filled',
'question_type' => QAType::MULTIPLE_CHOICES,
'is_start' => false,
'is_end' => true,
'answer_options' => [
['display_text' => 'Stop Working', 'value' => 'stop_working', 'next_question_number' => 'stop_working'],
['display_text' => 'Next PO (Fill)', 'value' => 'fill_po', 'next_question_number' => 'fill_po'],
['display_text' => 'Next PO (Approve)', 'value' => 'approve_po', 'next_question_number' => 'approve_po'],
],
],
]
],
];
foreach ($questionnaireSets as $set) {
$questionnaireSet = QAQuestionnaireSet::create([
'name' => $set['name'],
'description' => $set['description'],
'group' => $set['group'],
]);
foreach ($set['questions'] as $key => $questionData) {
$question = new QAQuestions;
$question->question_number = $questionData['question_number'];
$question->question_title = $questionData['question_title'];
if (isset($questionData['question_description'])) {
$question->question_description = $questionData['question_description'];
}
$question->question_type = $questionData['question_type'];
$question->questionnaire_set_id = $questionnaireSet->id;
if (isset($questionData['next_nested_question'])) {
$question->next_nested_question = $questionData['next_nested_question'];
}
if (isset($questionData['next_main_question'])) {
$question->next_main_question = $questionData['next_main_question'];
}
$question->is_start = $questionData['is_start'];
$question->is_end = $questionData['is_end'];
if (isset($questionData['end_text'])) {
$question->end_text = $questionData['end_text'];
}
if (isset($questionData['url'])) {
$question->url = $questionData['url'];
}
$question->order = $key + 1;
$question->save();
if (isset($questionData['answer_options'])) {
foreach ($questionData['answer_options'] as $optionData) {
$answerOption = new QAAnswerOptions;
$answerOption->display_text = $optionData['display_text'];
$answerOption->value = $optionData['value'];
$answerOption->question_number = $questionData['question_number'];
$answerOption->next_question_number = $optionData['next_question_number'] ?? null;
$answerOption->questionnaire_set_id = $questionnaireSet->id;
$answerOption->save();
}
}
}
}
}
}
@@ -283,7 +283,9 @@
this.parameters = {
products: this.products
};
this.$emit('update-parameters', {
parameters: this.parameters,
});
this.submit(route('api.transaction.po.create', this.data.id), 'post', this.section, true, true);
},
uploadProducts() {
@@ -291,7 +293,9 @@
this.parameters = {
files: this.files
};
this.$emit('update-parameters', {
parameters: this.parameters,
});
this.submit(route('api.transaction.po.import', this.data.id), 'post', this.section, true, true);
},
successHandler(){
@@ -0,0 +1,86 @@
<template>
<div class="row">
<div class="col">
<div class="row no-margin">
<div class="col p-b-15 p-l-0 p-r-0">
<div class="row">
<div class="col">
<div class="row" v-for="(product, index) in products" :key="product.id">
<div class="col p-b-10 p-t-10 " :class="[{'b-grey' : index !== Object.keys(products).length - 1}, {'b-b' : index !== Object.keys(products).length - 1}]">
<purchase-order-item-form-component :data="product" :index="index" :currency="data.fixed_currency.short_code" :editable="false" :section="section"></purchase-order-item-form-component>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12" v-if="data.company.address">
<div class="row justify-content-center align-items-center text-center">
<h6 class="font-heading all-caps bold">Total: </h6>
<h6><span v-if="!submitted" class="bold m-r-5" :class="[{'text-danger' : (Math.round((poTotal + Number.EPSILON) * 1000) / 1000).toFixed(3) !== (Math.round((data.amount + Number.EPSILON) * 1000) / 1000).toFixed(3)}, {'text-success' : (Math.round((this.poTotal + Number.EPSILON) * 1000) / 1000).toFixed(3) === (Math.round((this.data.amount + Number.EPSILON) * 1000) / 1000).toFixed(3)}]">{{(Math.round(( poTotal + Number.EPSILON) * 1000) / 1000).toFixed(3)}}/</span><span class="text-primary bold m-l-5">{{(Math.round((data.amount + Number.EPSILON) * 1000) / 1000).toFixed(3)}} {{data.fixed_currency.short_code}}</span></h6>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import formHandler from '../../../general/mixins/formHandler';
import { required, requiredIf } from "vuelidate/lib/validators";
export default {
props:{
companySegmentIds: {
type: Array,
required: true
}
},
data(){
return {
interval:false,
submitted: false,
useUploadCsvPo: false,
product: {
stockCode: '',
description: '',
quantity: 1,
unit_price: 0,
},
products: [],
files: [],
uploadFiles: false,
}
},
validations: {
files: {
required: requiredIf(function () { return this.uploadFiles })
}
},
created() {
this.products = this.data.purchase_order ? this.data.purchase_order.details : [];
this.submitted = this.data.purchase_order ? this.data.purchase_order.status === 1 || this.data.purchase_order.status === 2: false;
},
computed: {
productTotal(){
return this.product.quantity * parseFloat((this.product.unit_price).toString().replaceAll(',', ''));
},
poTotal(){
return this.products.reduce(function(last, product) {
return last + product.total;
}, 0);
}
},
watch: {
'data': function () {
if (this.data && this.data.purchase_order && this.data.purchase_order.details) {
this.products = this.data.purchase_order.details;
this.submitted = this.data.purchase_order ? this.data.purchase_order.status === 1 || this.data.purchase_order.status === 2: false;
} else {
this.products = [];
}
}
},
mixins: [formHandler]
}
</script>
@@ -35,7 +35,7 @@
<div class="row align-items-center">
<div class="col-auto">
<p class="m-b-0 small muted">Unit Price</p>
<p class="m-b-0 bold">{{product.unit_price}}</p>
<p class="m-b-0 bold">{{product.unit_price.toFixed(3)}}</p>
</div>
<div class="col-auto text-center">
<p class="m-b-0 small muted">Quantity</p>
@@ -144,7 +144,7 @@
},
created() {
this.product = this.data;
this.product.unit_price = (Math.round((this.product.unit_price+ Number.EPSILON) * 1000) / 1000).toFixed(3)
this.product.unit_price = (Math.round((this.product.unit_price+ Number.EPSILON) * 1000) / 1000);
},
computed: {
productTotal(){
@@ -0,0 +1,640 @@
<template>
<div class="row justify-content-center align-items-center text-center" style="min-height: 80vh;">
<div class="col">
<h1 class="m-b-50" :class="{ 'text-success': timer }">{{ formattedTime }}</h1>
<loading-component style="height: 50px; top: 0;" key="1" color="success" v-show="isLoading"></loading-component>
<div class="row" v-if="question" v-show="!isLoading">
<div class="col">
<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>
<!-- MAIN QUESTION AND INFO -->
<div class="row m-b-20">
<div class="col">
<h3>{{questionTitle}}</h3>
<h4>{{question.question_description}}</h4>
<div v-if="question.question_number === '1688'">
<h1>Login Information</h1>
<div v-if="externalApiResponse.data.booking"
class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<table>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">ORDER Marking: </span></h3>
</td>
<td>
<h3><a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">{{ externalApiResponse.data.booking.marking
}}</a></h3>
</td>
</tr>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">1688 LOGIN ID/EMAIL/PHONE:
</span></h3>
</td>
<td>
<h3>{{ externalApiResponse.data.account_no }}</h3>
</td>
</tr>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">1688 LOGIN PASSWORD:
</span></h3>
</td>
<td>
<h3>{{ externalApiResponse.data.holder_name }}</h3>
</td>
</tr>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">ALIPAY 6-DIGIT PAYMENT PIN:
</span></h3>
</td>
<td>
<h3>{{ externalApiResponse.data.pin }}</h3>
</td>
</tr>
</table>
</div>
</div>
</div>
<div v-if="question.question_number === '1688_login_successful'">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a v-if="externalApiResponse.data.booking" :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">
{{ externalApiResponse.data.booking.marking }}
</a>
</h3>
<div v-if="externalApiResponse.data.booking_attributes && externalApiResponse.data.booking_attributes.length > 0" class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<h4>
<span class="bold d-inline-block m-r-15">Additional Info: </span>
</h4>
<div v-for="(attribute, index) in externalApiResponse.data.booking_attributes"
:key="index">
<h2>
{{ index + 1 + '. #' + attribute.value }}
</h2>
</div>
</div>
</div>
<div v-else class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<h4>
<span class="bold d-inline-block m-r-15">Additional Info: -</span>
</h4>
</div>
</div>
</div>
<div v-if="question.question_number === '1688_order_verify'">
<h3 v-if="externalApiResponse.data.booking">
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">
{{ externalApiResponse.data.booking.marking }}
</a>
</h3>
<div v-if="externalApiResponse.data.booking" class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Reference(s): </span>
</h3>
<div v-for="(attribute, index) in externalApiResponse.data.booking_attributes"
:key="index">
<h2>
{{ index + 1 + '. #' + attribute.value }}
</h2>
</div>
<div class="col-auto text-center">
<h4>
Total CNY: <!-- {{ attribute.cost }} -->
</h4>
</div>
</div>
</div>
</div>
<div v-if="question.question_number === 'approve_po'">
<div v-if="externalApiResponse.data.booking" class="row">
<div class="col">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">{{ externalApiResponse.data.booking.marking }}</a>
</h3>
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<purchase-order-form-read-only-component :data="externalApiResponse.data.booking" :companySegmentIds="companySegmentIds" :section="section"></purchase-order-form-read-only-component>
</div>
</div>
</div>
</div>
</div>
<div v-if="question.question_number === 'fill_po'">
<div v-if="externalApiResponse.data.booking" class="row">
<div class="col">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">{{ externalApiResponse.data.booking.marking }}</a>
</h3>
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<purchase-order-form-read-only-component :data="externalApiResponse.data.booking" :companySegmentIds="companySegmentIds" :section="section"></purchase-order-form-read-only-component>
</div>
</div>
</div>
</div>
</div>
<div v-if="question.question_number === 'approve_po_edit' || question.question_number === 'fill_po_edit'">
<div v-if="externalApiResponse.data.booking" class="row">
<div class="col">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">{{ externalApiResponse.data.booking.marking }}</a>
</h3>
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<purchase-order-form-component :data="externalApiResponse.data.booking" :companySegmentIds="companySegmentIds" :section="section + 'GetExternalApiResponse'"
@update-parameters="handleParametersUpdate"></purchase-order-form-component>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- QUESTION TYPE -->
<div class="row m-b-10" v-if="question.question_type === 8">
<div class="col">
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<h2>
Upload the English PO
</h2>
<!-- <file-upload-component :data="externalApiResponse.data" section="section"></file-upload-component> -->
<file-input-component :validator="$v.files" v-model="files">
<template slot="label">
<div class="font-heading fs-11 all-caps">Photo or File</div>
</template>
</file-input-component>
</div>
<div class="col">
<h2>
Upload the China PO
</h2>
<file-input-component :validator="$v.files" v-model="files">
<template slot="label">
<div class="font-heading fs-11 all-caps">Photo or File</div>
</template>
<template slot="tips">
<div class="row" v-show="false">
<div class="col">
<div class="row m-b-20">
<div class="col">
<div class="font-heading fs-11 text-warning m-b-10">{{ documentType === 0 ? 'IC': 'SSM registration'}} number shown in the photo should tally with {{ documentType === 0 ? 'IC': 'SSM registration'}} number provided above.</div>
<div class="font-heading fs-11 muted">Tips on how your photo should look</div>
</div>
</div>
</div>
</div>
</template>
</file-input-component>
</div>
<div class="col">
<h2>
Upload Bank Slip
</h2>
<file-input-component :validator="$v.files" v-model="files">
<template slot="label">
<div class="font-heading fs-11 all-caps">Photo or File</div>
</template>
<template slot="tips">
<div class="row" v-show="false">
<div class="col">
<div class="row m-b-20">
<div class="col">
<div class="font-heading fs-11 text-warning m-b-10">{{ documentType === 0 ? 'IC': 'SSM registration'}} number shown in the photo should tally with {{ documentType === 0 ? 'IC': 'SSM registration'}} number provided above.</div>
<div class="font-heading fs-11 muted">Tips on how your photo should look</div>
</div>
</div>
</div>
</div>
</template>
</file-input-component>
</div>
</div>
</div>
</div>
<div class="row m-b-10" v-else-if="question.question_type === 7">
<div class="col">
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<div class="m-t-25">
<!-- <remark-comment-form-component :data="externalApiResponse.data" :id="externalApiResponse.data.booking_id" :section="section" module_type="Booking"></remark-comment-form-component> -->
<validation-wrapper-component :validator="$v.answer">
<label>Answer</label>
<input type="text" class="form-control" v-model="answer" @input="formTouched = true">
</validation-wrapper-component>
</div>
<div class="m-t-25">
<!-- <file-upload-component v-model="files" :value="value" v-on:input="$emit('input', $event)"></file-upload-component> -->
<file-input-component :validator="$v.files" v-model="files">
<template slot="label">
<div class="font-heading fs-11 all-caps">Photo or File</div>
</template>
<template slot="tips">
<div class="row" v-show="false">
<div class="col">
<div class="row m-b-20">
<div class="col">
<div class="font-heading fs-11 text-warning m-b-10">{{ documentType === 0 ? 'IC': 'SSM registration'}} number shown in the photo should tally with {{ documentType === 0 ? 'IC': 'SSM registration'}} number provided above.</div>
<div class="font-heading fs-11 muted">Tips on how your photo should look</div>
</div>
</div>
</div>
</div>
</template>
</file-input-component>
</div>
</div>
</div>
</div>
</div>
<div class="row m-b-10" v-else-if="question.question_type === 4" >
<div class="col" v-for="ans in question.question_answers">
<div v-html="ans.display_text"></div>
</div>
</div>
<div class="row m-b-15" v-else-if="question.question_type === 3">
<div class="col">
<file-input-component :validator="$v.files" v-model="files">
<template slot="label">
<div class="font-heading fs-11 all-caps">Photo or File</div>
</template>
<template slot="tips">
<div class="row" v-show="false">
<div class="col">
<div class="row m-b-20">
<div class="col">
<div class="font-heading fs-11 text-warning m-b-10">{{ documentType === 0 ? 'IC': 'SSM registration'}} number shown in the photo should tally with {{ documentType === 0 ? 'IC': 'SSM registration'}} number provided above.</div>
<div class="font-heading fs-11 muted">Tips on how your photo should look</div>
</div>
</div>
</div>
</div>
</template>
</file-input-component>
</div>
</div>
<div class="row m-b-10" v-else-if="question.question_type === 2">
<div class="col" >
<validation-wrapper-component :validator="$v.answer">
<label>Answer</label>
<input type="text" class="form-control" v-model="answer" @input="formTouched = true">
</validation-wrapper-component>
</div>
</div>
<div class="row m-b-10" v-else-if="question.question_type === 1" >
<div class="col question.answers">
<div class="w-100 d-block text-center">
<validation-wrapper-component :validator="$v.answer" class="col">
<button v-for="(ans, index) in question.question_answers" :key="index"
:answerValue="ans.value" :answerId=ans.id
@click="addClass($event, 'question.answers')"
style="margin: 15px; padding: 15px 40px; min-width: 210px;">
{{ ans.display_text }}
</button>
<!-- <div class="col" v-for="ans in question.question_answers">
<div class="col question.answers">
<div class="btn btn-xs btn-block" :answerValue="ans.value" :answerId=ans.id @click="addClass($event, 'question.answers')">{{ ans.display_text }}</div>
</div>
</div> -->
</validation-wrapper-component>
</div>
</div>
</div>
<!-- QUESTION NAVIGATION -->
<div class="row" v-if="question.question_type !== 0">
<div class="col p-r-5">
<button class="btn btn-sm btn-default bg-master-lightest btn-block b-rad-none" :disabled="hidePrevious" @click="formTouched = true; submitForm('previous')">Previous</button>
</div>
<div class="col p-l-5">
<button class="btn btn-sm btn-primary btn-block b-rad-none" @click="formTouched = true; submitForm('next')">Next</button>
</div>
</div>
<div class="row" v-else>
<div class="col p-l-5">
<button class="btn btn-sm btn-primary btn-block b-rad-none" @click="reloadPage();">Reload Page</button>
</div>
</div>
</div>
</div>
</div>
<!-- Debug box -->
<div class="debug-box" v-if="question">
<div id="debug-meta">
ID: {{question.question_number}} <br>
</div>
</div>
</div>
</template>
<script>
import { required } from "vuelidate/lib/validators";
export default {
data(){
return {
section: 'adminWorkFlowForm',
showAnswerInput: false,
showUploadInput: false,
formTouched: false,
question: null,
answer: '',
answerId: 0,
direction: 'next',
documentType: 1,
files: [],
error: null,
// From AdminWorkFlowSectionComponent
sessionId: null,
externalApiResponse: { data: null },
externalApiUrl: "",
timer: null,
elapsedTime: 0,
stepTime: 0,
interval: 1000,
isFetching: false,
}
},
validations() {
return {
answer: {
required: this.formTouched && this.showAnswerInput ? required : false,
},
files: {
required: this.formTouched && this.showUploadInput ? required : false,
}
};
},
mounted() {
if (this.sessionId == null) {
this.sessionId = this.generateSessionId();
}
},
computed: {
pendingQueue () {
return this.$store.getters.isInCompleteQueue(this.section);
},
pendingQueueForSecondApiCall () {
return this.$store.getters.isInCompleteQueue(this.section+ 'GetExternalApiResponse');
},
formattedTime() {
const hours = String(Math.floor(this.elapsedTime / 3600)).padStart(2, '0');
const minutes = String(Math.floor((this.elapsedTime % 3600) / 60)).padStart(2, '0');
const seconds = String(this.elapsedTime % 60).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
},
companySegmentIds() {
return this.externalApiResponse.data?.booking?.company?.segments?.map(obj => parseInt(obj.id)) ?? [];
},
hidePrevious(){
// return (this.question && this.question.is_start) || (this.question && this.question.is_no_going_back && this.question.is_no_going_back === 1)
return (this.question && this.question.question_number === 'node_0') || (this.question && this.question.is_no_going_back && this.question.is_no_going_back === 1)
},
isLoading(){
return this.$store.getters.isLoading(this.section) || this.$store.getters.isLoading(this.section + 'GetExternalApiResponse') || this.isFetching;
},
questionTitle(){
return !this.question.question_title.includes('_') ? this.question.question_title : '';
}
},
watch: {
pendingQueue(inComplete, oldValue){
if(inComplete){
this.fetchFirstQuestion();
}
},
pendingQueueForSecondApiCall(inComplete, oldValue){
if(inComplete){
this.fetchAdditionalQuestionInfo();
}
}
},
created(){
this.$store.dispatch('updateListQueue', {'name': this.section});
this.$store.dispatch('updateListQueue', {'name': this.section + 'GetExternalApiResponse'});
},
methods: {
fetchFirstQuestion(){
this.submit(route('api.questionnaires.first.question', 0), 'get', this.section, false, false);
},
fetchAdditionalQuestionInfo(){
// console.log('fetchAdditionalQuestionInfo: ', JSON.stringify(this.question));
if(this.externalApiUrl){
this.isFetching = true;
this.externalApiResponse.data = {};
this.submit(this.externalApiUrl, 'get', this.section + 'GetExternalApiResponse', false, false);
}
},
successHandler(response, section){
if(section === 'adminWorkFlowFormGetExternalApiResponse'){
// console.log('adminWorkFlowFormGetExternalApiResponse: ', JSON.stringify(section), ' response: ', JSON.stringify(response));
this.$store.dispatch('completeList', {'name': this.section + 'GetExternalApiResponse', 'data': []});
if(response.data)
{
this.externalApiResponse.data = response.data;
}
else if(response.payload.data)
{
this.externalApiResponse.data.booking = response.payload.data;
}
this.isFetching = false;
}
else{
this.$store.dispatch('completeList', {'name': this.section, 'data': []});
this.question = response.payload.data;
this.reset();
this.removeClass('question.answers');
// if(this.question.answer != null && this.direction == "previous"){
// console.log("previous: " + this.question.answer.answer);
// this.answer = this.question.answer.answer;
// }
//if there is only 1 answer, select answer by default
if(this.question.question_answers.length === 1){
this.answer = this.question.question_answers[0].value;
this.answerId = this.question.question_answers[0].id;
}
if(this.question.is_previous && this.question.answer){
this.externalApiResponse.data = JSON.parse(this.question.answer.question_metadata);
}
else {
if(this.question && this.question.url){
let url = this.question.url;
if(!this.isURL(this.question.url)){
if(this.question.url === 'api.admin_work_flow.fetch_model_attributes'){
url = this.route(this.question.url, this.externalApiResponse.data.booking.id);
}
else if(this.question.url === 'api.booking.show'){
url = this.route(this.question.url, this.externalApiResponse.data.booking.marking);
}
else{
url = this.route(this.question.url);
}
}
this.externalApiUrl = url;
}
if(this.question.is_start){
this.elapsedTime = 0;
}
this.fetchAdditionalQuestionInfo();
}
}
},
errorHandler(error){
this.error = 'We are sorry, something went wrong. Please inform tech team.';
this.isFetching = false;
},
submitForm(direction) {
this.parameters = {}
this.parameters.question = this.question;
this.parameters.answer = this.answer;
this.parameters.answerId = this.answerId;
this.parameters.answerObj = this.question.question_answers.find(item => item.id === Number(this.answerId));
this.parameters.files = this.files;
this.parameters.extra = {};
this.error = null;
this.direction = direction;
if(this.question.question_type === 3){
this.showAnswerInput = false;
this.showUploadInput = true;
}
else if(this.question.question_type === 1 || (this.question.question_type === 2)){
this.showAnswerInput = true;
this.showUploadInput = false;
}
else if(this.question.question_type === 7){
this.showAnswerInput = true;
this.showUploadInput = true;
}
this.parameters.extra.timeUsedSeconds = this.stepTime;
this.parameters.extra.questionMetadata = this.externalApiResponse.data;
this.parameters.extra.user_id = this.$store.getters.getUserId;
this.parameters.extra.session_id = this.sessionId;
if(direction === 'next'){
if (this.parameters.answer === 'start_work') this.startWork();
if (this.parameters.answer === 'stop_working') this.stopTimer();
this.submit(this.route('api.questionnaires.next.question'), 'post', this.section, false, false);
}
else if(direction === 'previous'){
this.formTouched = false;
this.parameters.isPrevious = true;
this.submit(this.route('api.questionnaires.next.question'), 'post', this.section, false, false);
}
},
reset() {
this.parameters = null;
this.answer = '';
this.answerId = 0;
this.files = [];
this.formTouched = false;
this.showAnswerInput = false;
this.showUploadInput = false;
this.error = null;
this.stepTime = 0;
this.externalApiUrl = "";
},
addClass(event, cls) {
const div = event.target;
this.answer = div.getAttribute('answerValue');
this.answerId = div.getAttribute('answerId');
this.removeClass(cls);
div.classList.add('btn-primary');
},
removeClass(cls) {
document.getElementsByClassName(cls).forEach(el => {
const btnPrimaryEl = el.getElementsByClassName('btn-primary')[0];
if (btnPrimaryEl) {
btnPrimaryEl.classList.remove('btn-primary');
}
});
},
startTimer() {
if (!this.timer) {
this.timer = setInterval(() => {
this.elapsedTime++;
this.stepTime++;
}, this.interval);
} else {
console.log("time has already started");
}
},
stopTimer() { clearInterval(this.timer); this.timer = null; },
startWork() { this.startTimer(); },
endWork() { this.stopTimer(); console.log("work is ended"); },
generateSessionId() {
return 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
isURL(string) {
try {
new URL(string);
return true;
} catch (err) {
return false;
}
},
reloadPage() {
if (typeof window !== "undefined") {
window.location.reload();
}
},
handleParametersUpdate({ parameters }) {
console.log('Parameters:', JSON.stringify(parameters));
},
},
}
</script>
<style scoped>
.debug-box {
position: fixed;
bottom: 0px;
left: 0px;
width: 200px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 10px;
border-radius: 5px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
</style>
@@ -0,0 +1,670 @@
<template>
<div class="row justify-content-center align-items-center text-center" style="min-height: 80vh;">
<div class="col">
<h1 class="m-b-50" :class="{ 'text-success': timer }">{{ formattedTime }}</h1>
<div v-if="isLoading" class="row">
<div class="col">
<loading-component></loading-component>
</div>
</div>
<div v-else class="row">
<div class="col">
<div v-if="currentQuestion && Object.keys(currentQuestion).length && !apiFailed">
<h2 v-if="currentQuestion.text">{{ currentQuestion.text }}</h2>
<!-- <p>currentQuestionId - {{ currentQuestionId }}</p> -->
<!-- <p>currentOrder - {{ currentOrder? currentOrder.id : 'null' }}</p> -->
<div v-if="currentQuestionId === '1688'">
<h1>Login Information</h1>
<div v-if="externalApiResponse.data"
class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<table>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">ORDER Marking: </span></h3>
</td>
<td>
<h3><a :href="route('booking.details', externalApiResponse.data.booking_marking)"
target="_blank">{{ externalApiResponse.data.booking_marking
}}</a></h3>
</td>
</tr>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">1688 LOGIN ID/EMAIL/PHONE:
</span></h3>
</td>
<td>
<h3>{{ externalApiResponse.data.account_no }}</h3>
</td>
</tr>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">1688 LOGIN PASSWORD:
</span></h3>
</td>
<td>
<h3>{{ externalApiResponse.data.holder_name }}</h3>
</td>
</tr>
<tr>
<td>
<h3><span class="bold d-inline-block m-r-15">ALIPAY 6-DIGIT PAYMENT PIN:
</span></h3>
</td>
<td>
<h3>{{ externalApiResponse.data.pin }}</h3>
</td>
</tr>
</table>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === 'login_successful'">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">
{{ externalApiResponse.data.booking.marking }}
</a>
</h3>
<div v-if="externalApiResponse.data" class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Reference(s): </span>
</h3>
<div v-for="(attribute, index) in externalApiResponse.data.booking_attributes"
:key="index">
<h2>
{{ index + 1 + '. #' + attribute.value }}
</h2>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === 'order_verify'">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.booking.marking)"
target="_blank">
{{ externalApiResponse.data.booking.marking }}
</a>
</h3>
<div v-if="externalApiResponse.data" class="row bg-master-light m-t-15 m-b-15 rounded padding-30 justify-content-center">
<div class="col-auto text-left">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Reference(s): </span>
</h3>
<div v-for="(attribute, index) in externalApiResponse.data.booking_attributes"
:key="index">
<h2>
{{ index + 1 + '. #' + attribute.value }}
</h2>
</div>
<div class="col-auto text-center">
<h4>
Total CNY: <!-- {{ attribute.cost }} -->
</h4>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === '1688_submit'">
<div v-if="externalApiResponse.data" class="row">
<div class="col">
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<h2>
Upload the English PO
</h2>
<file-upload-component :data="externalApiResponse.data" section="section"></file-upload-component>
</div>
<div class="col">
<h2>
Upload the China PO
</h2>
<file-upload-component :data="externalApiResponse.data" section="section"></file-upload-component>
</div>
<div class="col">
<h2>
Upload Bank Slip
</h2>
<file-upload-component :data="externalApiResponse.data" section="section"></file-upload-component>
</div>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === 'approve_po'">
<div v-if="externalApiResponse.data" class="row">
<div class="col">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.marking)"
target="_blank">{{ externalApiResponse.data.marking }}</a>
</h3>
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<purchase-order-form-component :data="externalApiResponse.data"
:section="section"></purchase-order-form-component>
</div>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === 'fill_po'">
<div v-if="externalApiResponse.data" class="row">
<div class="col">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.marking)"
target="_blank">{{ externalApiResponse.data.marking }}</a>
</h3>
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<purchase-order-form-component :data="externalApiResponse.data"
section="section"></purchase-order-form-component>
</div>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === 'edit_po'">
<div v-if="externalApiResponse.data" class="row">
<div class="col">
<h3>
<span class="bold d-inline-block m-r-15">ORDER Marking: </span>
<a :href="route('booking.details', externalApiResponse.data.marking)"
target="_blank">{{ externalApiResponse.data.marking }}</a>
</h3>
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<purchase-order-form-component :data="externalApiResponse.data"
section="section"></purchase-order-form-component>
</div>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div v-if="currentQuestionId === '1688_issue_others' || currentQuestionId === 'po_others'">
<div v-if="externalApiResponse.data" class="row">
<div class="col">
<div class="row b-a b-primary padding-30 bg-white m-t-25">
<div class="col">
<h2>
What issues did you encounter?
</h2>
<div class="m-t-25">
<remark-comment-form-component @remark-submitted="handleRemarkSubmitted" :data="externalApiResponse.data" :id="externalApiResponse.data.booking_id" :section="section" module_type="Booking"></remark-comment-form-component>
</div>
<div class="m-t-25">
<file-upload-component v-model="files" :value="value" v-on:input="$emit('input', $event)"></file-upload-component>
</div>
</div>
</div>
</div>
</div>
<div v-else class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">Api Failed. Please contact Tech Support</h1>
</div>
</div>
</div>
<div class="row" v-if="currentQuestion.answers">
<div class="col">
<div class="w-100 d-block text-center">
<button @click="goBack()"
v-if="questionIds.length && currentQuestion.show_back_btn != false"
class="btn btn-lg btn-default b-a b-grey d-inline-block rounded fs-20"
style="margin: 15px; padding: 15px 40px; min-width: 210px;">
Go Back
</button>
<button v-for="(answer, index) in currentQuestion.answers" :key="index"
@click="selectAnswer(answer.next)" :data-attr-next-step="answer.next"
class="btn btn-lg btn-primary d-inline-block rounded fs-20"
:class="getbuttonClass(answer.btn_color)"
style="margin: 15px; padding: 15px 40px; min-width: 210px;">
{{ answer.text }}
</button>
</div>
</div>
</div>
</div>
<div v-else>
<div class="row bg-danger-light m-t-15 m-b-15 rounded padding-30">
<div class="col">
<h1 class="text-white">
"{{ currentQuestionId }}" {{ apiFailed ? 'api Failed' : "is empty" }}
. Please contact Tech Support</h1>
</div>
</div>
<button class="btn btn-lg btn-primary d-inline-block rounded fs-20"
style="margin: 15px; padding: 15px 40px; min-width: 210px;"
@click="selectAnswer('refresh_page')">Refresh Page</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
section: 'initial_section',
questions: {
"node_0": {
"text": "Are you ready to work today?",
"answers": [
{ "text": "Yes", "next": "start_work" },
{ "text": "No", "next": "no_work" }
]
},
"start_work": {
"text": "What will you work on?",
"answers": [
{ "text": "1688", "next": "1688" },
{ "text": "Approve PO", "next": "approve_po" },
{ "text": "Fill PO", "next": "fill_po" }
]
},
"no_work": {
"text": "Come back when you are ready to work",
"answers": [
{ "text": "Refresh Page", "next": "refresh_page" }
]
},
"1688": {
"load_api": this.route("api.admin_work_flow.fetch_oldest_order"),
"answers": [
{ "text": "Login Issue", "next": "login_issue", 'btn_color': "warning" },
{ "text": "Login Successful", "next": "login_successful" }
],
},
"login_issue": {
"text": "Login Issues",
"answers": [
{ "text": "Need TAC", "next": "Need Tac" },
{ "text": "Wrong login details", "next": "Wrong Login Details" },
{ "text": "Others", "next": "1688_issue_others" },
{ "text": "Order Cancelled", "next": "refund_request" }
]
},
"login_successful": {
"load_api": 'fetch_model_attributes',
"answers": [
{ "text": "Can't verify order?", "next": "1688_issue_order" },
{ "text": "Order Verified", "next": "order_verify" },
]
},
"1688_issue_order": {
"answers": [
{ "text": "Amount not found", "next": "Amount not found" },
{ "text": "Plus Member", "next": "Plus Member" },
{ "text": "Others", "next": "Others" },
{ "text": "Customer did not verify 1688 account", "next": "Customer did not verify 1688 account" },
{ "text": "WorldFirst account linked another account", "next": "WorldFirst account linked another account" },
]
},
"order_verify": {
// fetch order amount
"load_api": 'fetch_model_attributes',
"answers": [
{ "text": "Yes", "next": "order_verification" }
]
},
"order_verification": {
"text": "Are You Sure this is the correct amount?",
"answers": [
{ "text": "No", "next": "insufficient_order" },
{ "text": "Yes", "next": "proceed_order" }
]
},
"proceed_order": {
"text": "Please proceed the order on 1688",
"answers": [
{ "text": "Got Issue", "next": "1688_order_issue" },
{ "text": "Done", "next": "1688_submit" }
]
},
"1688_order_issue": {
"answers": [
{ "text": "Wrong Pin Number", "next": "Wrong Pin Number" },
{ "text": "Not Enough Stock", "next": "Not Enough Stock" },
{ "text": "Others", "next": "Others" },
{ "text": "No CrossBoarder", "next": "No CrossBoarder" },
{ "text": "AngPau", "next": "AngPau" }
]
},
"1688_issue_others": {
"text": "Others",
"answers": [
{ "text": "Submit Issue", "next": "1688_issue_submit"}
]
},
"1688_submit": {
"answers": [
{ "text": "Stop Working", "next": "stop_working"},
{ "text": "Proceed to next order", "next": "1688"}
]
},
"1688_issue_submit": {
"text": "Issue has been submitted",
"answers": [
{ "text": "Stop Working", "next": "stop_working" },
{ "text": "Next Order", "next": "1688" }
]
},
"refund_request": {
"text": "Refund Request sent",
"answers": [
{ "text": "Stop Working", "next": "stop_working" },
{ "text": "Next PO", "next": "1688" }
]
},
"approve_po": {
// "text": "Order Number",
"load_api": this.route("api.admin_work_flow.fetch_pending_approve_po"),
"answers": [
{ "text": "Approve", "next": "po_approved" },
{ "text": "Edit PO", "next": "edit_po" },
{ "text": "Reject", "next": "reject_po" }
]
},
"fill_po": {
// "text": "Order Number",
"load_api": this.route("api.admin_work_flow.fetch_pending_fill_po"),
"answers": [
{ "text": "Edit PO", "next": "edit_po" },
]
},
"po_filled": {
"text": "PO Filled",
"answers": [
{ "text": "Stop Working", "next": "stop_working" },
{ "text": "Next PO", "next": "approve_po" }
]
},
"po_approved": {
"text": "PO Approved",
"answers": [
{ "text": "Stop Working", "next": "stop_working" },
{ "text": "Next PO", "next": "approve_po" }
]
},
"edit_po": {
"answers": [
{ "text": "Issue?", "next": "issue_po"},
{ "text": "Done", "next": "po_filled" },
]
},
"po_others": {
"text": "Others",
"answers": [
{ "text": "Submit Issue", "next": "issue_submit"}
]
},
"reject_po": {
"text": "PO Rejected",
"answers": [
{ "text": "Issue", "next": "issue_po" },
]
},
"issue_po": {
"answers": [
{ "text": "Sensitive Goods", "next": "Sensitive Goods" },
{ "text": "Others", "next": "PO Others" },
// { "text": "Stop Working", "next": "stop_working" },
// { "text": "Next PO", "next": "approve_po" }
]
},
"issue_submit": {
"text": "Issue has been submitted",
"answers": [
{ "text": "Stop Working", "next": "stop_working" },
{ "text": "Next Order", "next": "approve_po" }
]
},
"stop_working": {
"text": "Thank you for your work!",
"answers": [
{ "text": "Refresh Page", "next": "refresh_page" }
]
}
},
currentQuestionId: null,
questionIds: [],
isLoading: false,
externalApiResponse: { data: null },
timer: null,
elapsedTime: 0,
stepTime: 0,
interval: 1000,
sessionId: null,
currentOrder: null,
apiFailed: false,
};
},
computed: {
currentQuestion() { return this.questions[this.currentQuestionId] || {}; },
formattedTime() {
const hours = String(Math.floor(this.elapsedTime / 3600)).padStart(2, '0');
const minutes = String(Math.floor((this.elapsedTime % 3600) / 60)).padStart(2, '0');
const seconds = String(this.elapsedTime % 60).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
},
mounted() {
this.currentQuestionId = 'node_0';
if (this.sessionId == null) {
this.sessionId = this.generateSessionId();
}
},
methods: {
startTimer() {
if (!this.timer) {
this.timer = setInterval(() => {
this.elapsedTime++;
this.stepTime++;
}, this.interval);
} else {
console.log("time has already started");
}
},
stopTimer() { clearInterval(this.timer); this.timer = null; },
startWork() { this.startTimer(); },
endWork() { this.stopTimer(); console.log("work is ended"); },
handleRemarkSubmitted(remarkContent) {
this.callApi(this.route('api.admin_work_flow.create_issue_remark'), 'post', 'section', {
'booking_id': this.currentOrder.booking_id,
'remark': remarkContent,
'user_id': this.$store.getters.getUserId,
}, false, false);
alert('Remark has been submitted');
},
selectAnswer(nextQuestionId, goBack = false) {
if (nextQuestionId === 'refresh_page') return window.location.reload();
if (this.currentQuestionId === 'issue_po') {
if (nextQuestionId === 'po_others') {
this.callApi(this.route('api.admin_work_flow.create_po_issue_remark'), 'post', 'section', {
'booking_id': this.currentOrder.id,
'remark': 'PO Others',
'user_id': this.$store.getters.getUserId,
}, false, false);
} else {
this.callApi(this.route('api.admin_work_flow.create_po_issue_remark'), 'post', 'section', {
'booking_id': this.currentOrder.id,
'remark': nextQuestionId,
'user_id': this.$store.getters.getUserId,
}, false, false);
nextQuestionId = 'issue_submit';
}
}
if (['login_issue', '1688_issue_order', '1688_order_issue'].includes(this.currentQuestionId)) {
if (nextQuestionId === 'refund_request') {
this.callApi(this.route('api.admin_work_flow.create_issue_remark'), 'post', 'section', {
'booking_id': this.currentOrder.booking_id,
'remark': nextQuestionId,
'user_id': this.$store.getters.getUserId,
}, false, false);
} else if(nextQuestionId === '1688_issue_others') {
// this.callApi(this.route('api.admin_work_flow.create_issue_remark'), 'post', 'section', {
// 'booking_id': this.currentOrder.booking_id,
// 'remark': nextQuestionId,
// 'user_id': this.$store.getters.getUserId,
// }, false, false);
} else {
this.callApi(this.route('api.admin_work_flow.create_issue_remark'), 'post', 'section', {
'booking_id': this.currentOrder.booking_id,
'remark': nextQuestionId,
'user_id': this.$store.getters.getUserId,
}, false, false);
nextQuestionId = '1688_issue_submit';
}
}
if (!Object.keys(this.questions[nextQuestionId]).length) this.endWork();
if (nextQuestionId === 'start_work') this.startWork();
if (nextQuestionId === 'stop_working') this.stopTimer();
// exclude some node to click back
console.log('selectAnswer goBack: ', goBack);
if (!['node_0'].includes(this.currentQuestionId) && !goBack) {
console.log('exclude some node 1: ', goBack);
this.questionIds.push(this.currentQuestionId);
this.callLogApi(this.currentQuestionId, nextQuestionId);
}
if (!['node_0'].includes(this.currentQuestionId) && goBack) {
console.log('exclude some node 2: ', goBack);
this.callLogApi(this.currentQuestionId, 'goBack');
}
if (this.questions[nextQuestionId]?.load_api) this.callApi(this.questions[nextQuestionId].load_api, 'get', 'section');
this.currentQuestionId = nextQuestionId;
},
callLogApi(currentNode, nextNode) {
console.log('callLogApi currentNode: ', currentNode);
console.log('callLogApi nextNode: ', nextNode);
// call api to recorrd timestamp
var logBody = {
'current_node': currentNode,
'next_node': nextNode,
'seconds': this.stepTime,
'user_id': this.$store.getters.getUserId,
'session_id': this.sessionId,
};
this.callApi(this.route('api.admin_work_flow.add_workflow_timestamp'), 'post', 'section', logBody, false, false);
this.stepTime = 0;
},
goBack() {
var prevId = this.questionIds.pop();
this.selectAnswer(prevId, true);
},
generateSessionId() {
return 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
callApi(url, method, section, parameters = null, showLoadingAnimation = true, storeResponseBody = true) {
console.log('callApi url: ', url);
if (url == 'fetch_model_attributes') {
url = this.route("api.admin_work_flow.fetch_model_attributes", this.currentOrder.booking_id)
} else if (url == 'fetch_pending_approve_po') {
url = this.route("api.admin_work_flow.fetch_pending_approve_po", this.currentOrder)
}
if (showLoadingAnimation) this.isLoading = true;
this.$store.dispatch('crudRequest', { endpoint: url, method, parameters: parameters })
.then(response => response.json().then(data => ({ data, ok: response.ok, status: response.status })))
.then(apiResponse => {
if (storeResponseBody) {
this.externalApiResponse = apiResponse.data;
}
if (apiResponse.ok) {
this.successHandler(apiResponse.data);
} else {
// Improved error handling
this.handleError(apiResponse.data, apiResponse.status);
}
if (showLoadingAnimation) {
this.isLoading = false;
}
if ([this.route("api.admin_work_flow.fetch_oldest_order"), this.route("api.admin_work_flow.fetch_pending_approve_po"), this.route("api.admin_work_flow.fetch_pending_fill_po")].includes(url)) {
this.currentOrder = apiResponse.data.data
}
})
.catch(error => {
// Handle network or other unexpected errors
console.error('Unexpected error:', error);
this.errorHandler({ message: 'An unexpected error occurred. Please try again later.' }, 500);
this.apiFailed = true;
if (showLoadingAnimation) {
this.isLoading = false;
}
});
},
getbuttonClass(className) {
return 'btn-' + className;
},
}
};
</script>
+1 -1
View File
@@ -41,7 +41,7 @@ export default {
}
successNotification ? this.$store.dispatch('createNotification', { title: response.title, message: response.message, type: 'success' }) : null;
this.successHandler(response)
this.successHandler(response, section);
});
@@ -0,0 +1,5 @@
@extends('layouts.base_portal')
@section('inner_content')
<admin-work-flow-form-component></admin-work-flow-form-component>
<!-- <admin-work-flow-section-component></admin-work-flow-section-component> -->
@endsection
+228
View File
@@ -0,0 +1,228 @@
<?php
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Classes\ValueObjects\Constants\BookingAttributeNames;
use App\Http\Resources\BookingResource;
use App\Models\Booking;
use App\Models\KeyValuePair;
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'admin-work-flow', 'as' => 'admin_work_flow.', 'namespace' => 'AdminWorkFlow'], function () {
// Generalized JSON response function
function jsonResponse($data = null, $message = null, $status = 200)
{
$response = ['message' => $message];
if ($data !== null) {
$response['data'] = $data;
}
return response()->json($response, $status);
}
Route::get('/fetch-oldest-order', function () {
$excludedBookingIds = KeyValuePair::where('owner_type', 'App\Models\Booking')
->where('key', '1688_admin_workflow_processed')
->pluck('owner_id')
->filter(function ($value) {
return is_numeric($value);
})
->toArray();
$booking = Booking::where('service_id', 4)
->where('status', ApprovalStatus::APPROVED)
->whereNotIn('id', $excludedBookingIds)
->first();
return $booking ? jsonResponse([
'passwords' => $booking->bank->holder_name ?? null,
'account_no' => $booking->bank->account_no ?? null,
'pin' => $booking->bank->bank_branch ?? null,
'holder_name' => $booking->bank->holder_name ?? null,
'booking' => $booking
]) : jsonResponse(null, 'No booking found', 404);
})->name('fetch_oldest_order');
Route::get('/fetch-pending-approved-po', function () {
$excludedBookingIds = KeyValuePair::where('owner_type', 'App\Models\Booking')
->where('key', 'approve_po_admin_workflow_processed')
->pluck('owner_id')
->filter(function ($value) {
return is_numeric($value);
})
->toArray();
$booking = Booking::with('transactions')
->where('service_id', 4)
->where('status', ApprovalStatus::APPROVED)
->whereNotIn('id', $excludedBookingIds)
->whereHas('transactions', fn ($query) => $query->where('type', TransactionType::PURCHASE_ORDER)->where('status', '<', ApprovalStatus::APPROVED))
->first();
$result = new BookingResource($booking);
return jsonResponse([
'booking' => $result,
]);
// return $booking ? jsonResponse(new BookingResource($booking)) : jsonResponse(null, 'No booking found', 404);
})->name('fetch_pending_approve_po');
Route::get('/fetch-pending-fill-po', function () {
$excludedBookingIds = KeyValuePair::where('owner_type', 'App\Models\Booking')
->where('key', 'fill_po_admin_workflow_processed')
->pluck('owner_id')
->filter(function ($value) {
return is_numeric($value);
})
->toArray();
$booking = Booking::with('transactions')
->where('service_id', 4)
->where('status', ApprovalStatus::APPROVED)
->whereNotIn('id', $excludedBookingIds)
->whereDoesntHave('transactions', fn ($query) => $query->where('type', TransactionType::PURCHASE_ORDER))
->first();
$result = new BookingResource($booking);
return jsonResponse([
'booking' => $result,
]);
// return $booking ? jsonResponse(new BookingResource($booking)) : jsonResponse(null, 'No booking found', 404);
})->name('fetch_pending_fill_po');
Route::get('{booking_id}/fetch-model-attributes', function ($bookingId) {
$booking = Booking::find($bookingId);
if (!$booking) {
return jsonResponse(null, 'No approved booking found', 404);
}
$attributes = $booking->modelAttributes()
->where('name', BookingAttributeNames::ORDER_REFERENCE_NO)
->get(['id', 'value'])
->map(fn ($attr) => $attr->only(['id', 'value']));
return jsonResponse([
'booking' => $booking,
'booking_attributes' => $attributes,
]);
})->name('fetch_model_attributes');
// Route::post('/add-workflow-timestamp', function (Request $request) {
// $validator = Validator::make($request->all(), [
// 'current_node' => 'required|string',
// 'next_node' => 'required|string',
// 'seconds' => 'required|integer',
// 'user_id' => 'required|integer|exists:users,id',
// 'session_id' => 'required|string',
// ]);
// if ($validator->fails()) {
// throw new RequestValidationException($validator->messages()->first());
// }
// $workflowTimestamp = WorkflowTimestamp::create($validator->validated());
// return jsonResponse($workflowTimestamp, 'Workflow timestamp created successfully', 201);
// })->name('add_workflow_timestamp');
// Route::post('/create-1688-login-issue-remark', function (Request $request) {
// $validator = Validator::make($request->all(), [
// 'booking_id' => 'required',
// 'remark' => 'required',
// 'user_id' => 'required',
// ]);
// if ($validator->fails()) {
// throw new RequestValidationException($validator->messages()->first());
// }
// $booking = Booking::find($request->booking_id);
// if (!$booking) {
// return jsonResponse(null, 'Booking not found', 404);
// }
// $remark = new Remark([
// 'commenter_id' => $request->user_id,
// 'content' => $request->remark,
// 'owner_type' => get_class($booking),
// 'owner_id' => $booking->id,
// ]);
// $remark->save();
// return jsonResponse($remark, 'Remark created successfully', 201);
// })->name('create_1688_login_issue_remark');
// Route::post('/create_issue_remark', function (Request $request) {
// $validator = Validator::make($request->all(), [
// 'booking_id' => 'required',
// 'remark' => 'required',
// 'user_id' => 'required',
// ]);
// if ($validator->fails()) {
// throw new RequestValidationException($validator->messages()->first());
// }
// $booking = Booking::find($request->booking_id);
// if (!$booking) {
// return jsonResponse(null, 'Booking not found', 404);
// }
// $remark = new Remark([
// 'commenter_id' => $request->user_id,
// 'content' => $request->remark,
// 'owner_type' => get_class($booking),
// 'owner_id' => $booking->id,
// ]);
// $remark->save();
// return jsonResponse($remark, 'Remark created successfully', 201);
// })->name('create_issue_remark');
// Route::post('/create_po_issue_remark', function (Request $request) {
// $validator = Validator::make($request->all(), [
// 'booking_id' => 'required',
// 'remark' => 'required',
// 'user_id' => 'required',
// ]);
// if ($validator->fails()) {
// return response()->json(['message' => $validator->messages()->first()], 400); // Return validation error
// }
// $booking = Booking::find($request->booking_id);
// if (!$booking) {
// return response()->json(['message' => 'Booking not found'], 404);
// }
// $remark = new Remark([
// 'commenter_id' => $request->user_id,
// 'content' => $request->remark,
// 'owner_type' => get_class($booking),
// 'owner_id' => $booking->id,
// ]);
// $remark->save();
// return response()->json(['data' => $remark, 'message' => 'Remark created successfully'], 201);
// })->name('create_po_issue_remark');
});
+4
View File
@@ -71,6 +71,10 @@ Route::group(['middleware' => 'api', 'prefix' => 'v1', 'as' => 'api.'], function
require __DIR__ . '/job.php';
require __DIR__ . '/questionnaires.php';
require __DIR__ . '/admin_work_flow.php';
// require __DIR__ . '/rate.php';
// require __DIR__ . '/receipt.php';
+9
View File
@@ -0,0 +1,9 @@
<?php
use Illuminate\Support\Facades\Route;
Route::group(['prefix' => 'questionnaires', 'as' => 'questionnaires.', 'namespace' => 'Questionnaires'], function () {
Route::get('/{set_id}', 'FetchQuestionV1AdminWFController@fetch')->name('first.question');
Route::post('/question', 'UpdateNextQuestionV1AdminWFController@fetch')->name('next.question');
Route::get('/list', 'ListQuestionsAnswersQAController@list')->name('list.questions.answers');
});
+13 -9
View File
@@ -1183,42 +1183,42 @@ Route::get('/show-white-form-transactions-in-date-range/{from_date}/{to_date}',
const fileInput = form.querySelector("input[type=\'file\']");
const files = fileInput.files;
const userToken = localStorage.getItem("user-token");
if (files.length === 0) {
alert("Please select at least one file to upload.");
return;
}
const fileReaders = [];
let totalFiles = files.length;
// Create a promise for each file to read it as Base64
Array.from(files).forEach((file) => {
const reader = new FileReader();
// Create a promise that resolves when the file is read
const fileReadPromise = new Promise((resolve, reject) => {
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = (error) => {
reject(error);
};
reader.readAsDataURL(file); // Read the file as Data URL (Base64)
});
fileReaders.push(fileReadPromise);
});
// Wait for all files to be read
Promise.all(fileReaders)
.then((fileData) => {
const payload = {
files: fileData, // Array of objects with name, type, and Base64 content
};
// Send the payload as JSON
return fetch(`/api/v1/transactions/${billId}/bill/verification`, {
method: "POST",
@@ -1339,3 +1339,7 @@ Route::get('/site.webmanifest', function () {
$contents = File::get($filePath);
return response($contents, 200)->header('Content-Type', 'application/manifest+json');
});
Route::get('/admin-work-flow', function () {
return view('pages.dashboards.admin_work_flow');
})->name('admin-work-flow');