diff --git a/ProcessMaker/Console/Commands/CasesSync.php b/ProcessMaker/Console/Commands/CasesSync.php new file mode 100644 index 0000000000..e23d9ce0d4 --- /dev/null +++ b/ProcessMaker/Console/Commands/CasesSync.php @@ -0,0 +1,47 @@ +option('request_ids'); + $requestIds = $requestIds ? explode(',', $requestIds) : []; + + if (count($requestIds) > 0) { + $data = CaseSyncRepository::syncCases($requestIds); + + foreach ($data['successes'] as $value) { + $this->info('Case started synced ' . $value); + } + + foreach ($data['errors'] as $value) { + $this->error('Error syncing case started ' . $value); + } + } else { + $this->error('Please specify a list of request IDs.'); + } + } +} diff --git a/ProcessMaker/Contracts/CaseApiRepositoryInterface.php b/ProcessMaker/Contracts/CaseApiRepositoryInterface.php new file mode 100644 index 0000000000..b71ffdd0d2 --- /dev/null +++ b/ProcessMaker/Contracts/CaseApiRepositoryInterface.php @@ -0,0 +1,62 @@ +json([ + 'message' => $this->getMessage(), + ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY); + } +} diff --git a/ProcessMaker/Filters/BaseFilter.php b/ProcessMaker/Filters/BaseFilter.php new file mode 100644 index 0000000000..966ba9b76b --- /dev/null +++ b/ProcessMaker/Filters/BaseFilter.php @@ -0,0 +1,315 @@ +', + '<', + '>=', + '<=', + 'between', + 'in', + 'contains', + 'regex', + 'starts_with', + ]; + + public function __construct($definition) + { + $this->subjectType = $definition['subject']['type']; + $this->subjectValue = Arr::get($definition, 'subject.value'); + $this->operator = $definition['operator']; + $this->value = $definition['value']; + $this->or = Arr::get($definition, 'or', []); + + $this->detectRawValue(); + } + + public static function filter(Builder $query, string|array $filterDefinitions): void + { + if (is_string($filterDefinitions)) { + $filterDefinitions = json_decode($filterDefinitions, true); + } + + if (!$filterDefinitions) { + return; + } + + $query->where(function ($query) use ($filterDefinitions) { + foreach ($filterDefinitions as $filter) { + (new static($filter))->addToQuery($query); + } + }); + } + + public function addToQuery(Builder $query): void + { + if (!empty($this->or)) { + $query->where(fn ($query) => $this->apply($query)); + } else { + $this->apply($query); + } + } + + private function apply($query): void + { + if ($valueAliasMethod = $this->valueAliasMethod()) { + $this->valueAliasAdapter($valueAliasMethod, $query); + } elseif ($this->subjectType === self::TYPE_PROCESS) { + $this->filterByProcessId($query); + } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { + $this->filterByRelationship($query); + } elseif ($this->isJsonData() && $query->getModel() instanceof ProcessRequestToken) { + $this->filterByRequestData($query); + } else { + $this->applyQueryBuilderMethod($query); + } + + if (!empty($this->or)) { + $query->orWhere(function ($orQuery) { + foreach ($this->or as $or) { + (new static($or))->addToQuery($orQuery); + } + }); + } + } + + private function applyQueryBuilderMethod($query) + { + $method = $this->method(); + + if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { + $query->$method( + $this->subject(), + $this->value(), + ); + } elseif ($this->isJsonData()) { + $this->manuallyAddJsonWhere($query); + } else { + $query->$method( + $this->subject(), + $this->operator(), + $this->value(), + ); + } + } + + /** + * We must do this manually because Laravel bindings cast + * floats/doubles to strings and that wont work to compare + * json values + * + * @param [type] $query + * @return void + */ + private function manuallyAddJsonWhere($query): void + { + $parts = explode('.', $this->subjectValue); + + array_shift($parts); + + $selector = implode('"."', $parts); + $operator = $this->operator(); + $value = $this->value(); + + if (!is_numeric($value)) { + $value = DB::connection()->getPdo()->quote($value); + } + + if ($operator === 'like') { + // For JSON data is required to do a CAST in order to make insensitive the comparison + $query->whereRaw( + "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" + ); + } else { + $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); + } + } + + private function operator() + { + if (!in_array($this->operator, $this->operatorWhitelist)) { + abort(422, "Invalid operator: {$this->operator}"); + } + + if ($this->operator === 'contains' || $this->operator === 'starts_with') { + return 'like'; + } + + if ($this->operator === 'regex') { + $this->operator = 'REGEXP'; + } + + return $this->operator; + } + + private function method() + { + switch($this->operator) { + case 'in': + $method = 'whereIn'; + if ($this->isJsonData()) { + $method = 'whereJsonContains'; + } + break; + case 'between': + $method = 'whereBetween'; + break; + default: + $method = 'where'; + } + + return $method; + } + + private function isJsonData() + { + return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); + } + + private function subject() + { + if ($this->isJsonData()) { + return str_replace('.', '->', $this->subjectValue); + } + + if ($this->subjectType === self::TYPE_PARTICIPANTS) { + return 'user_id'; + } + + if ($this->subjectType === self::TYPE_PROCESS) { + return 'process_id'; + } + + if ($this->subjectType === self::TYPE_RELATIONSHIP) { + return $this->relationshipSubjectTypeParts()[1]; + } + + return $this->subjectValue; + } + + private function relationshipSubjectTypeParts(): array + { + return explode('.', $this->subjectValue); + } + + public function value() + { + if ($this->operator === 'contains') { + return '%' . $this->value . '%'; + } + + if ($this->operator === 'starts_with') { + return $this->value . '%'; + } + + if ($this->filteringWithRawValue()) { + return $this->getRawValue(); + } + + return $this->value; + } + + abstract protected function valueAliasMethod(); + + private function valueAliasAdapter(string $method, Builder $query): void + { + $operator = $this->operator(); + + if ($operator === 'in') { + $operator = '='; + } + + $values = (array) $this->value(); + $expression = (object) ['operator' => $operator]; + $model = $query->getModel(); + + if ($method === 'valueAliasParticipant') { + $values = $this->convertUserIdsToUsernames($values); + } + + foreach ($values as $i => $value) { + if ($i === 0) { + $query->where($model->$method($value, $expression)); + } else { + $query->orWhere($model->$method($value, $expression)); + } + } + } + + private function convertUserIdsToUsernames($values) + { + return array_map(function ($value) { + $username = User::find($value)?->username; + + return isset($username) ? $username : $value; + }, $values); + } + + private function filterByProcessId(Builder $query): void + { + if ($query->getModel() instanceof ProcessRequestToken) { + $query->whereIn('process_request_id', function ($query) { + $query->select('id') + ->from('process_requests') + ->whereIn('process_id', (array) $this->value()); + }); + } else { + $this->applyQueryBuilderMethod($query); + } + } + + private function filterByRelationship(Builder $query): void + { + $relationshipName = $this->relationshipSubjectTypeParts()[0]; + $query->whereHas($relationshipName, function ($rel) { + $this->applyQueryBuilderMethod($rel); + }); + } + + private function filterByRequestData(Builder $query): void + { + $query->whereHas('processRequest', function ($rel) { + $this->applyQueryBuilderMethod($rel); + }); + } +} diff --git a/ProcessMaker/Filters/CasesFilter.php b/ProcessMaker/Filters/CasesFilter.php new file mode 100644 index 0000000000..dbab3e3fc4 --- /dev/null +++ b/ProcessMaker/Filters/CasesFilter.php @@ -0,0 +1,15 @@ +subjectType === self::TYPE_STATUS) { + return 'valueAliasStatus'; + } + + return null; + } +} diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 528995037b..843a468549 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -2,260 +2,23 @@ namespace ProcessMaker\Filters; -use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Arr; -use ProcessMaker\Models\ProcessRequestToken; -use ProcessMaker\Models\User; -use ProcessMaker\Traits\InteractsWithRawFilter; -class Filter +class Filter extends BaseFilter { - use InteractsWithRawFilter; - public const TYPE_PARTICIPANTS = 'Participants'; - public const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; - - public const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; - - public const TYPE_STATUS = 'Status'; - - public const TYPE_ALTERNATIVE = 'Alternative'; - - public const TYPE_FIELD = 'Field'; - public const TYPE_PROCESS = 'Process'; public const TYPE_RELATIONSHIP = 'Relationship'; - public string|null $subjectValue; - - public string $subjectType; - - public string $operator; - - public $value; - - public array $or; - - public array $operatorWhitelist = [ - '=', - '!=', - '>', - '<', - '>=', - '<=', - 'between', - 'in', - 'contains', - 'regex', - 'starts_with', - ]; - - public function __construct($definition) - { - $this->subjectType = $definition['subject']['type']; - $this->subjectValue = Arr::get($definition, 'subject.value'); - $this->operator = $definition['operator']; - $this->value = $definition['value']; - $this->or = Arr::get($definition, 'or', []); - - $this->detectRawValue(); - } - - public static function filter(Builder $query, string|array $filterDefinitions): void - { - if (is_string($filterDefinitions)) { - $filterDefinitions = json_decode($filterDefinitions, true); - } - - if (!$filterDefinitions) { - return; - } - - $query->where(function ($query) use ($filterDefinitions) { - foreach ($filterDefinitions as $filter) { - (new self($filter))->addToQuery($query); - } - }); - } - - public function addToQuery(Builder $query): void - { - if (!empty($this->or)) { - $query->where(fn ($query) => $this->apply($query)); - } else { - $this->apply($query); - } - } - - private function apply($query): void - { - if ($valueAliasMethod = $this->valueAliasMethod()) { - $this->valueAliasAdapter($valueAliasMethod, $query); - } elseif ($this->subjectType === self::TYPE_PROCESS) { - $this->filterByProcessId($query); - } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { - $this->filterByRelationship($query); - } elseif ($this->isJsonData() && $query->getModel() instanceof ProcessRequestToken) { - $this->filterByRequestData($query); - } else { - $this->applyQueryBuilderMethod($query); - } - - if (!empty($this->or)) { - $query->orWhere(function ($orQuery) { - foreach ($this->or as $or) { - (new self($or))->addToQuery($orQuery); - } - }); - } - } - - private function applyQueryBuilderMethod($query) - { - $method = $this->method(); - - if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { - $query->$method( - $this->subject(), - $this->value(), - ); - } elseif ($this->isJsonData()) { - $this->manuallyAddJsonWhere($query); - } else { - $query->$method( - $this->subject(), - $this->operator(), - $this->value(), - ); - } - } - - /** - * We must do this manually because Laravel bindings cast - * floats/doubles to strings and that wont work to compare - * json values - * - * @param [type] $query - * @return void - */ - private function manuallyAddJsonWhere($query): void - { - $parts = explode('.', $this->subjectValue); - - array_shift($parts); - - $selector = implode('"."', $parts); - $operator = $this->operator(); - $value = $this->value(); - - if (!is_numeric($value)) { - $value = DB::connection()->getPdo()->quote($value); - } - - if ($operator === 'like') { - // For JSON data is required to do a CAST in order to make insensitive the comparison - $query->whereRaw( - "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" - ); - } else { - $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); - } - } - - private function operator() - { - if (!in_array($this->operator, $this->operatorWhitelist)) { - abort(422, "Invalid operator: {$this->operator}"); - } - - if ($this->operator === 'contains' || $this->operator === 'starts_with') { - return 'like'; - } - - if ($this->operator === 'regex') { - $this->operator = 'REGEXP'; - } - - return $this->operator; - } - - private function method() - { - switch($this->operator) { - case 'in': - $method = 'whereIn'; - if ($this->isJsonData()) { - $method = 'whereJsonContains'; - } - break; - case 'between': - $method = 'whereBetween'; - break; - default: - $method = 'where'; - } - - return $method; - } - - private function isJsonData() - { - return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); - } - - private function subject() - { - if ($this->isJsonData()) { - return str_replace('.', '->', $this->subjectValue); - } - - if ($this->subjectType === self::TYPE_PARTICIPANTS) { - return 'user_id'; - } - - if ($this->subjectType === self::TYPE_PROCESS) { - return 'process_id'; - } - - if ($this->subjectType === self::TYPE_RELATIONSHIP) { - return $this->relationshipSubjectTypeParts()[1]; - } - - return $this->subjectValue; - } - - private function relationshipSubjectTypeParts(): array - { - return explode('.', $this->subjectValue); - } - - public function value() - { - if ($this->operator === 'contains') { - return '%' . $this->value . '%'; - } - - if ($this->operator === 'starts_with') { - return $this->value . '%'; - } - - if ($this->filteringWithRawValue()) { - return $this->getRawValue(); - } - - return $this->value; - } - /** * Forward Status and Participant subjects to PMQL methods on the models. * * For now, we only need Participants and Status because Request and Requester * are columns on the tables (process_request_id and user_id). */ - private function valueAliasMethod() + protected function valueAliasMethod() { $method = null; @@ -279,66 +42,4 @@ private function valueAliasMethod() return $method; } - - private function valueAliasAdapter(string $method, Builder $query): void - { - $operator = $this->operator(); - - if ($operator === 'in') { - $operator = '='; - } - - $values = (array) $this->value(); - $expression = (object) ['operator' => $operator]; - $model = $query->getModel(); - - if ($method === 'valueAliasParticipant') { - $values = $this->convertUserIdsToUsernames($values); - } - - foreach ($values as $i => $value) { - if ($i === 0) { - $query->where($model->$method($value, $expression)); - } else { - $query->orWhere($model->$method($value, $expression)); - } - } - } - - private function convertUserIdsToUsernames($values) - { - return array_map(function ($value) { - $username = User::find($value)?->username; - - return isset($username) ? $username : $value; - }, $values); - } - - private function filterByProcessId(Builder $query): void - { - if ($query->getModel() instanceof ProcessRequestToken) { - $query->whereIn('process_request_id', function ($query) { - $query->select('id') - ->from('process_requests') - ->whereIn('process_id', (array) $this->value()); - }); - } else { - $this->applyQueryBuilderMethod($query); - } - } - - private function filterByRelationship(Builder $query): void - { - $relationshipName = $this->relationshipSubjectTypeParts()[0]; - $query->whereHas($relationshipName, function ($rel) { - $this->applyQueryBuilderMethod($rel); - }); - } - - private function filterByRequestData(Builder $query): void - { - $query->whereHas('processRequest', function ($rel) { - $this->applyQueryBuilderMethod($rel); - }); - } } diff --git a/ProcessMaker/Http/Controllers/Api/CommentController.php b/ProcessMaker/Http/Controllers/Api/CommentController.php index de799861b3..1f9724b32c 100644 --- a/ProcessMaker/Http/Controllers/Api/CommentController.php +++ b/ProcessMaker/Http/Controllers/Api/CommentController.php @@ -29,7 +29,7 @@ class CommentController extends Controller * * @param Request $request * - * @return \ProcessMaker\Http\Resources\ApiCollection + * @return ApiCollection * * @return \Illuminate\Http\Response */ @@ -87,6 +87,48 @@ public function index(Request $request) return new ApiCollection($response); } + /** + * Display comments related to the case + * + * @param Request $request + * + * @return ApiCollection + * + * @return \Illuminate\Http\Response + */ + public function getCommentsByCase(Request $request) + { + $request->validate([ + 'case_number' => 'required|integer', + ]); + + $query = Comment::query() + ->with('user') + ->with('repliedMessage'); + + $flag = 'visible'; + if (\Auth::user()->is_administrator) { + $flag = 'all'; + } + $query->hidden($flag); + $caseNumber = $request->input('case_number', null); + + $query->where('case_number', $caseNumber); + + if ($request->has('type')) { + $types = explode(',', $request->input('type')); + $query->whereIn('type', $types); + } + + $response = + $query->orderBy( + $request->input('order_by', 'created_at'), + $request->input('order_direction', 'ASC') + )->paginate($request->input('per_page', 100)); + + return new ApiCollection($response); + } + private function authorizeComment(Request $request) { $request->validate([ diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php index 4bfc464c9e..3e0d9002d4 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php @@ -839,4 +839,43 @@ public function endEventDestination(ProcessRequest $request) return response()->json(['message' => __('End event found'), 'data' => $data]); } + + /** + * This endpoint returns requests by case number + * + * @param Request $request + * + * @return ApiCollection + */ + public function getRequestsByCase(Request $request, User $user = null) + { + if (!$user) { + $user = Auth::user(); + } + + // Validate the inputs, including optional ones + $request->validate([ + 'case_number' => 'required|integer', + 'order_by' => 'nullable|string|in:id,name,status,user_id,initiated_at,participants', + 'order_direction' => 'nullable|string|in:asc,desc', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer', + ]); + + $query = ProcessRequest::forUser($user); + + // Filter by case_number + $query->filterByCaseNumber($request); + + // Apply ordering only if a valid order_by field is provided + $query->applyOrdering($request); + $response = $query->applyPagination($request); + + // Get activeTasks and participants + $response = $response->map(function ($processRequest) use ($request) { + return new ProcessRequestResource($processRequest); + }); + + return new ApiCollection($response); + } } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php index 4e31aad618..f40b96d704 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php @@ -295,7 +295,7 @@ private function saveUploadedFile(UploadedFile $file, ProcessRequest $processReq $errors = []; $this->validateFile($file, $errors); if (count($errors) > 0) { - return abort(response($errors , 422)); + return abort(response($errors, 422)); } $parentId = $processRequest->parent_request_id; @@ -423,7 +423,7 @@ public function destroy(Request $laravel_request, ProcessRequest $request, $file private function validateFile(UploadedFile $file, &$errors) { if (strtolower($file->getClientOriginalExtension() === 'pdf')) { - $this->validatePDFFile($file, $errors); + $this->validatePDFFile($file, $errors); } return $errors; diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index 2b25834bd5..d09dddc352 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -19,6 +19,7 @@ use ProcessMaker\Http\Resources\ApiResource; use ProcessMaker\Http\Resources\Task as Resource; use ProcessMaker\Http\Resources\TaskCollection; +use ProcessMaker\Jobs\CaseUpdate; use ProcessMaker\Listeners\HandleRedirectListener; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; @@ -53,6 +54,15 @@ class TaskController extends Controller 'Completed' => 'CLOSED', ]; + protected $defaultCase = [ + 'id', // Task # + 'element_name', // Task Name + 'user_id', // Participant + 'process_id', // Process + 'due_at', // Due At + 'process_request_id', // Request Id # + ]; + /** * Display a listing of the resource. * @@ -149,6 +159,62 @@ public function index(Request $request, $getTotal = false, User $user = null) return new TaskCollection($response); } + /** + * Get the task list related to the case + * @param Request $request + * @param User $user used by Saved Search package to return accurate counts + * @return array + */ + public function getTasksByCase(Request $request, User $user = null) + { + if (!$user) { + $user = Auth::user(); + } + + // Validate the inputs, including optional ones + $request->validate([ + 'case_number' => 'required|integer', + 'status' => 'nullable|string|in:ACTIVE,CLOSED', + 'order_by' => 'nullable|string|in:id,element_name,due_at,user.lastname,process.name', + 'order_direction' => 'nullable|string|in:asc,desc', + 'page' => 'nullable|integer|min:1', + 'per_page' => 'nullable|integer', + 'includeScreen' => 'sometimes|boolean', + ]); + + $includeScreen = $request->input('includeScreen', false); + + // Get only the columns defined + $query = ProcessRequestToken::select($this->defaultCase); + // Filter by case_number + $query->filterByCaseNumber($request); + // Filter by status + $query->filterByStatus($request); + // Return the process information + $query->getProcess(); + // Return the user information + $query->getUser(); + // Filter only the task related to the user + $this->applyForCurrentUser($query, $user); + // Exclude non visible task + $this->excludeNonVisibleTasks($query, $request); + // Apply ordering only if a valid order_by field is provided + $query->applyOrdering($request); + + try { + $response = $query->applyPagination($request); + + if ($includeScreen) { + $response = $this->addTaskData($response); + } + $response->inOverdue = 0; + } catch (QueryException $e) { + return $this->handleQueryException($e); + } + + return new TaskCollection($response); + } + /** * Display the specified resource. * @TODO remove this method,view and route this is not a used file @@ -253,7 +319,11 @@ public function update(Request $request, ProcessRequestToken $task) $userToAssign = $request->input('user_id'); $task->reassign($userToAssign, $request->user()); - return new Resource($task->refresh()); + $taskRefreshed = $task->refresh(); + + CaseUpdate::dispatch($task->processRequest, $taskRefreshed); + + return new Resource($taskRefreshed); } else { return abort(422); } diff --git a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php new file mode 100644 index 0000000000..668a4bdc7a --- /dev/null +++ b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php @@ -0,0 +1,194 @@ +caseRepository = $caseRepository; + } + + /* The comment block you provided is a PHPDoc block. It is used to document the purpose and usage of a method in PHP + code. In this specific block: */ + /** + * Get a list of all started cases. + * + * @param Request $request + * + * @queryParam userId int Filter by user ID. + * @queryParam status string Filter by case status. + * @queryParam sortBy string Sort by field:asc,field2:desc,... + * @queryParam filterBy array Filter by field=value&field2=value2&... + * @queryParam search string Search by case number or case title. + * @queryParam pageSize int Number of items per page. + * @queryParam page int Page number. + * + * @return array + */ + public function getAllCases(CaseListRequest $request): JsonResponse + { + $query = $this->caseRepository->getAllCases($request); + + return $this->paginateResponse($query); + } + + /** + * Get a list of all started cases. + * + * @param Request $request + * + * @queryParam userId int Filter by user ID. + * @queryParam sortBy string Sort by field:asc,field2:desc,... + * @queryParam filterBy array Filter by field=value&field2=value2&... + * @queryParam search string Search by case number or case title. + * @queryParam pageSize int Number of items per page. + * @queryParam page int Page number. + * + * @return array + */ + public function getInProgress(CaseListRequest $request): JsonResponse + { + // The status parameter should never be considered as a filter for this list. + $request->merge(['status' => null]); + + // Get query + $query = $this->caseRepository->getInProgressCases($request); + + return $this->paginateResponse($query); + } + + /** + * Get a list of all started cases. + * + * @param Request $request + * + * @queryParam userId int Filter by user ID. + * @queryParam sortBy string Sort by field:asc,field2:desc,... + * @queryParam filterBy array Filter by field=value&field2=value2&... + * @queryParam search string Search by case number or case title. + * @queryParam pageSize int Number of items per page. + * @queryParam page int Page number. + * + * @return array + */ + public function getCompleted(CaseListRequest $request): JsonResponse + { + // The status parameter should never be considered as a filter for this list. + $request->merge(['status' => null]); + + // Get query + $query = $this->caseRepository->getCompletedCases($request); + + return $this->paginateResponse($query); + } + + /** + * Get "my cases" counters + * + * @param CaseListRequest $request + * + * @return JsonResponse + */ + public function getMyCasesCounters(CaseListRequest $request): JsonResponse + { + // Load user object + if ($request->filled('userId')) { + $userId = $request->get('userId'); + $user = User::find($userId); + } else { + $user = Auth::user(); + } + + // Initializing variables + $totalAllCases = null; + $totalMyCases = null; + $totalInProgress = null; + $totalCompleted = null; + $totalMyRequest = null; + + // Check permission + if ($user->hasPermission('view-all_cases')) { + // The total number of cases recorded in the platform. User Id send is overridden. + $request->merge(['userId' => null]); + $queryAllCases = $this->caseRepository->getAllCases($request); + $totalAllCases = $queryAllCases->count(); + } + + // Restore user id + $request->merge(['userId' => $user->id]); + + // The total number of cases recorded by the user making the request. + $queryMyCases = $this->caseRepository->getAllCases($request); + $totalMyCases = $queryMyCases->count(); + + // The number of In Progress cases started by the user making the request. + $queryInProgressCases = $this->caseRepository->getInProgressCases($request); + $totalInProgress = $queryInProgressCases->count(); + + // The number of Completed cases started by the user making the request. + $queryCompletedCases = $this->caseRepository->getCompletedCases($request); + $totalCompleted = $queryCompletedCases->count(); + + // Check permission + if ($user->hasPermission('view-my_requests')) { + // Only in progress requests + $requestAux = new Request(); + $requestAux->replace(['type' => 'in_progress']); + + // The number of requests for user making the request. + $totalMyRequest = (new ProcessRequestController)->index($requestAux, true, $user); + } + + // Build response + return response()->json([ + 'totalAllCases' => $totalAllCases, + 'totalMyCases' => $totalMyCases, + 'totalInProgress' => $totalInProgress, + 'totalCompleted' => $totalCompleted, + 'totalMyRequest' => $totalMyRequest, + ]); + } + + /** + * Handle pagination and return JSON response. + * + * @param Builder $query + * + * @return JsonResponse + */ + private function paginateResponse(Builder $query): JsonResponse + { + $pageSize = $this->request->get('pageSize', self::DEFAULT_PAGE_SIZE); + $data = $query->paginate($pageSize); + // Get all the participants ids from the data + $users = $this->caseRepository->getUsers($data); + + $pagination = CaseResource::customCollection($data, $users); + + return response()->json([ + 'data' => $pagination->items(), + 'meta' => [ + 'total' => $pagination->total(), + 'perPage' => $pagination->perPage(), + 'currentPage' => $pagination->currentPage(), + 'lastPage' => $pagination->lastPage(), + ], + ]); + } +} diff --git a/ProcessMaker/Http/Controllers/CasesController.php b/ProcessMaker/Http/Controllers/CasesController.php new file mode 100644 index 0000000000..7e0b59cb55 --- /dev/null +++ b/ProcessMaker/Http/Controllers/CasesController.php @@ -0,0 +1,200 @@ +only(['id', 'username', 'fullname', 'firstname', 'lastname', 'avatar']); + // This is a temporary API the engine team will provide the new + return view('cases.casesMain', compact('currentUser')); + } + /** + * Cases Detail + * + * @param ProcessRequest $request + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function edit(ProcessRequest $request) + { + if (!request()->input('skipInterstitial') && $request->status === 'ACTIVE') { + $startEvent = $request->tokens()->orderBy('id')->first(); + if ($startEvent) { + $definition = $startEvent->getDefinition(); + $allowInterstitial = false; + if (isset($definition['allowInterstitial'])) { + $allowInterstitial = filter_var( + $definition['allowInterstitial'], + FILTER_VALIDATE_BOOLEAN, + FILTER_NULL_ON_FAILURE + ); + } + if ($allowInterstitial && $request->user_id == Auth::id() && request()->has('fromTriggerStartEvent')) { + $active = $request->tokens() + ->where('user_id', Auth::id()) + ->where('element_type', 'task') + ->where('status', 'ACTIVE') + ->orderBy('id')->first(); + + // If the interstitial is enabled on the start event, then use it as the task + if ($active) { + $task = $allowInterstitial ? $startEvent : $active; + } else { + $task = $startEvent; + } + + return redirect(route('tasks.edit', [ + 'task' => $task->getKey(), + ])); + } + } + } + + $userHasCommentsForRequest = Comment::where('commentable_type', ProcessRequest::class) + ->where('commentable_id', $request->id) + ->where('body', 'like', '%{{' . \Auth::user()->id . '}}%') + ->count() > 0; + + $requestMedia = $request->media()->get()->pluck('id'); + + $userHasCommentsForMedia = Comment::where('commentable_type', \ProcessMaker\Models\Media::class) + ->whereIn('commentable_id', $requestMedia) + ->where('body', 'like', '%{{' . \Auth::user()->id . '}}%') + ->count() > 0; + + if (!$userHasCommentsForMedia && !$userHasCommentsForRequest) { + $this->authorize('view', $request); + } + + $request->participants; + $request->user; + $request->summary = $request->summary(); + + if ($request->status === 'CANCELED' && $request->process->cancel_screen_id) { + $request->summary_screen = $request->process->cancelScreen; + } else { + $request->summary_screen = $request->getSummaryScreen(); + } + $request->request_detail_screen = Screen::find($request->process->request_detail_screen_id); + + $canCancel = Auth::user()->can('cancel', $request->processVersion); + $canViewComments = (Auth::user()->hasPermissionsFor('comments')->count() > 0) || class_exists(PackageServiceProvider::class); + $canManuallyComplete = Auth::user()->is_administrator && $request->status === 'ERROR'; + $canRetry = false; + + if ($canManuallyComplete) { + $retry = RetryProcessRequest::for($request); + + $canRetry = $retry->hasRetriableTasks() && + !$retry->hasNonRetriableTasks() && + !$retry->isChildRequest(); + } + + $files = \ProcessMaker\Models\Media::getFilesRequest($request); + + $canPrintScreens = $this->canUserPrintScreen($request); + + $manager = app(ScreenBuilderManager::class); + event(new ScreenBuilderStarting($manager, ($request->summary_screen) ? $request->summary_screen->type : 'FORM')); + + $addons = []; + $dataActionsAddons = []; + + $isProcessManager = $request->process?->manager_id === Auth::user()->id; + + $eligibleRollbackTask = null; + $errorTask = RollbackProcessRequest::getErrorTask($request); + if ($errorTask) { + $eligibleRollbackTask = RollbackProcessRequest::eligibleRollbackTask($errorTask); + } + $this->summaryScreenTranslation($request); + return view('cases.edit', compact( + 'request', + 'files', + 'canCancel', + 'canViewComments', + 'canManuallyComplete', + 'canRetry', + 'manager', + 'canPrintScreens', + 'addons', + 'isProcessManager', + 'eligibleRollbackTask', + 'errorTask', + )); + } + + /** + * the user may or may not print forms + * + * @param ProcessRequest $request + * @return bool + */ + private function canUserPrintScreen(ProcessRequest $request) + { + //validate user is administrator + if (Auth::user()->is_administrator) { + return true; + } + + //validate user is participant or requester + if (in_array(Auth::user()->id, $request->participants()->get()->pluck('id')->toArray())) { + return true; + } + + // Any user with permissions Edit Request Data, Edit Task Data and view All Requests + if (Auth::user()->can('view-all_requests') && Auth::user()->can('edit-request_data') && Auth::user()->can('edit-task_data')) { + return true; + } + + return false; + } + + /** + * Translates the summary screen strings + * @param ProcessRequest $request + * @return void + */ + public function summaryScreenTranslation(ProcessRequest $request): void + { + if ($request->summary_screen) { + $processTranslation = new ProcessTranslation($request->process); + $translatedConf = $processTranslation->applyTranslations($request->summary_screen); + $request->summary_screen['config'] = $translatedConf; + } + } +} diff --git a/ProcessMaker/Http/Controllers/RequestController.php b/ProcessMaker/Http/Controllers/RequestController.php index e0c2e6032b..573a014b7a 100644 --- a/ProcessMaker/Http/Controllers/RequestController.php +++ b/ProcessMaker/Http/Controllers/RequestController.php @@ -47,7 +47,7 @@ public function index($type = null) $this->authorize('view-all_requests'); } - $title = 'My Cases'; + $title = 'My Request'; $types = ['all'=>'All Requests', 'in_progress'=>'Requests In Progress', 'completed'=>'Completed Requests']; diff --git a/ProcessMaker/Http/Middleware/GenerateMenus.php b/ProcessMaker/Http/Middleware/GenerateMenus.php index 413de790c5..13dbbe1fde 100644 --- a/ProcessMaker/Http/Middleware/GenerateMenus.php +++ b/ProcessMaker/Http/Middleware/GenerateMenus.php @@ -36,10 +36,10 @@ public function handle(Request $request, Closure $next) ['route' => 'process.browser.index', 'id' => 'process-browser'] )->active('process-browser/*'); }); - $menu->group(['prefix' => 'requests'], function ($request_items) { + $menu->group(['prefix' => 'cases'], function ($request_items) { $request_items->add( __('Cases'), - ['route' => 'cases.index', 'id' => 'requests'] + ['route' => 'cases-main.index', 'id' => 'cases'] )->active('cases/*'); }); //@TODO change the index to the correct blade @@ -142,25 +142,53 @@ public function handle(Request $request, Closure $next) $submenu = $menu->add(__('Processes')); }); Menu::make('sidebar_request', function ($menu) { - $submenu = $menu->add(__('Cases')); - $submenu->add(__('My Cases'), [ - 'route' => ['cases_by_type', ''], + $submenu = $menu->add(__('Requests')); + $submenu->add(__('My Requests'), [ + 'route' => ['requests_by_type', ''], 'icon' => 'fa-id-badge', ]); $submenu->add(__('In Progress'), [ - 'route' => ['cases_by_type', 'in_progress'], + 'route' => ['requests_by_type', 'in_progress'], 'icon' => 'fa-clipboard-list', ]); $submenu->add(__('Completed'), [ - 'route' => ['cases_by_type', 'completed'], + 'route' => ['requests_by_type', 'completed'], 'icon' => 'fa-clipboard-check', ]); if (\Auth::check() && \Auth::user()->can('view-all_requests')) { + $submenu->add(__('All Requests'), [ + 'route' => ['requests_by_type', 'all'], + 'icon' => 'fa-clipboard', + ]); + } + }); + + Menu::make('sidebar_cases', function ($menu) { + $submenu = $menu->add(__('Cases')); + $submenu->add(__('My Cases'), [ + 'route' => ['cases-main.index', ''], + 'icon' => 'fa-user', + ]); + $submenu->add(__('In Progress'), [ + 'route' => ['cases-main.index', 'in_progress'], + 'icon' => 'fa-list', + ]); + $submenu->add(__('Completed'), [ + 'route' => ['cases-main.index', 'completed'], + 'icon' => 'fa-check-circle', + ]); + if (\Auth::check() && \Auth::user()->can('view-all_cases')) { $submenu->add(__('All Cases'), [ - 'route' => ['cases_by_type', 'all'], + 'route' => ['cases-main.index', 'all'], 'icon' => 'fa-clipboard', ]); } + if (\Auth::check() && \Auth::user()->can('view-my_requests')) { + $submenu->add(__('My Requests'), [ + 'route' => ['requests_by_type', ''], + 'icon' => 'fa-play', + ]); + } }); Menu::make('sidebar_processes', function ($menu) { diff --git a/ProcessMaker/Http/Requests/CaseListRequest.php b/ProcessMaker/Http/Requests/CaseListRequest.php new file mode 100644 index 0000000000..51708ecf9c --- /dev/null +++ b/ProcessMaker/Http/Requests/CaseListRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + return [ + 'userId' => 'sometimes|integer', + 'status' => 'sometimes|in:IN_PROGRESS,COMPLETED', + 'sortBy' => ['sometimes', 'string', new SortBy], + 'filterBy' => 'sometimes|json', + 'search' => 'sometimes|string', + 'pageSize' => 'sometimes|integer|min:1', + 'page' => 'sometimes|integer|min:1', + ]; + } +} diff --git a/ProcessMaker/Http/Resources/V1_1/CaseResource.php b/ProcessMaker/Http/Resources/V1_1/CaseResource.php new file mode 100644 index 0000000000..d74efdbb35 --- /dev/null +++ b/ProcessMaker/Http/Resources/V1_1/CaseResource.php @@ -0,0 +1,76 @@ +$field->toArray(); + $data[$field] = $this->getParticipantData($participants); + + continue; + } + + $data[$field] = $this->$field; + } + + return $data; + } + + /** + * Transform participants using the users collection. + * + * @param array $participants The participants array. + * @return array The transformed participants. + */ + private function getParticipantData(array $participants): array + { + return array_map(fn($participant) => self::$users->get($participant), $participants); + } + + /** + * New resource collection method that accepts a users collection. + * + * @param mixed $resource The resource. + * @param Collection $users The users collection. + * @return AnonymousResourceCollection The anonymous resource collection. + */ + public static function customCollection($resource, $users): AnonymousResourceCollection + { + self::$users = $users; + + return parent::collection($resource); + } +} diff --git a/ProcessMaker/Jobs/CaseStore.php b/ProcessMaker/Jobs/CaseStore.php new file mode 100644 index 0000000000..04aeaa289a --- /dev/null +++ b/ProcessMaker/Jobs/CaseStore.php @@ -0,0 +1,32 @@ +create($this->instance); + } +} diff --git a/ProcessMaker/Jobs/CaseUpdate.php b/ProcessMaker/Jobs/CaseUpdate.php new file mode 100644 index 0000000000..005d347aff --- /dev/null +++ b/ProcessMaker/Jobs/CaseUpdate.php @@ -0,0 +1,33 @@ +update($this->instance, $this->token); + } +} diff --git a/ProcessMaker/Jobs/CaseUpdateStatus.php b/ProcessMaker/Jobs/CaseUpdateStatus.php new file mode 100644 index 0000000000..225408f6e1 --- /dev/null +++ b/ProcessMaker/Jobs/CaseUpdateStatus.php @@ -0,0 +1,32 @@ +updateStatus($this->instance); + } +} diff --git a/ProcessMaker/Models/CaseParticipated.php b/ProcessMaker/Models/CaseParticipated.php new file mode 100644 index 0000000000..206c4de742 --- /dev/null +++ b/ProcessMaker/Models/CaseParticipated.php @@ -0,0 +1,64 @@ + AsCollection::class, + 'requests' => AsCollection::class, + 'request_tokens' => AsCollection::class, + 'tasks' => AsCollection::class, + 'participants' => AsCollection::class, + ]; + + protected $dates = [ + 'initiated_at', + 'completed_at', + ]; + + protected $attributes = [ + 'keywords' => '', + ]; + + protected static function newFactory(): Factory + { + return CaseParticipatedFactory::new(); + } + + /** + * Get the user that owns the case. + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/ProcessMaker/Models/CaseStarted.php b/ProcessMaker/Models/CaseStarted.php new file mode 100644 index 0000000000..81c2781a32 --- /dev/null +++ b/ProcessMaker/Models/CaseStarted.php @@ -0,0 +1,60 @@ + AsCollection::class, + 'requests' => AsCollection::class, + 'request_tokens' => AsCollection::class, + 'tasks' => AsCollection::class, + 'participants' => AsCollection::class, + ]; + + protected $dates = [ + 'initiated_at', + 'completed_at', + ]; + + protected static function newFactory(): Factory + { + return CaseStartedFactory::new(); + } + + /** + * Get the user that owns the case. + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/ProcessMaker/Models/ProcessRequest.php b/ProcessMaker/Models/ProcessRequest.php index 989521a8b9..6ddb886dcc 100644 --- a/ProcessMaker/Models/ProcessRequest.php +++ b/ProcessMaker/Models/ProcessRequest.php @@ -165,8 +165,6 @@ class ProcessRequest extends ProcessMakerModel implements ExecutionInstanceInter 'participants', ]; - const DEFAULT_CASE_TITLE = 'Case #{{_request.case_number}}'; - /** * Determine whether the item should be indexed. * @@ -1002,7 +1000,7 @@ public function getCaseTitleFromProcess(): string $caseTitle = $this->process()->select('case_title')->first()->case_title; } - return $caseTitle ?: self::DEFAULT_CASE_TITLE; + return $caseTitle ?: $this->name; } /** @@ -1091,4 +1089,36 @@ public function getElementDestination(): ?array return $endEvents->first()->elementDestination; } + + /** + * Scope apply order + */ + public function scopeApplyOrdering($query, $request) + { + $orderBy = $request->input('order_by', 'name'); + $orderDirection = $request->input('order_direction', 'asc'); + + return $query->orderBy($orderBy, $orderDirection); + } + + /** + * Scope apply pagination + */ + public function scopeApplyPagination($query, $request) + { + $page = $request->input('page', 1); + $perPage = $request->input('per_page', 10); + + return $query->paginate($perPage); + } + + /** + * Scope to filter by case_number + */ + public function scopeFilterByCaseNumber($query, $request) + { + $caseNumber = $request->input('case_number'); + + return $query->where('case_number', $caseNumber); + } } diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index 18de925842..6363f31355 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -289,6 +289,70 @@ public function assignableUsers() return new TokenAssignableUsers($query, $this); } + /** + * Scope to filter by case_number through the processRequest relationship + */ + public function scopeFilterByCaseNumber($query, $request) + { + $caseNumber = $request->input('case_number'); + + return $query->whereHas('processRequest', function ($query) use ($caseNumber) { + $query->where('case_number', $caseNumber); + }); + } + + /** + * Scope to filter by status + */ + public function scopeFilterByStatus($query, $request) + { + $status = $request->input('status', 'ACTIVE'); + + return $query->where('status', $status); + } + + /** + * Scope get process information + */ + public function scopeGetProcess($query) + { + return $query->with(['process' => function ($query) { + $query->select('id', 'name'); + }]); + } + + /** + * Scope get user information + */ + public function scopeGetUser($query) + { + return $query->with(['user' => function ($query) { + $query->select('id', 'firstname', 'lastname', 'username', 'avatar'); + }]); + } + + /** + * Scope apply order + */ + public function scopeApplyOrdering($query, $request) + { + $orderBy = $request->input('order_by', 'due_at'); + $orderDirection = $request->input('order_direction', 'asc'); + + return $query->orderBy($orderBy, $orderDirection); + } + + /** + * Scope apply pagination + */ + public function scopeApplyPagination($query, $request) + { + $page = $request->input('page', 1); + $perPage = $request->input('per_page', 10); + + return $query->paginate($perPage); + } + /** * Returns either the owner element or its properties * diff --git a/ProcessMaker/Observers/UserObserver.php b/ProcessMaker/Observers/UserObserver.php index 36c14ca046..d8e45ef942 100644 --- a/ProcessMaker/Observers/UserObserver.php +++ b/ProcessMaker/Observers/UserObserver.php @@ -37,6 +37,7 @@ public function created(User $user): void { $perList = [ 'view-process-catalog', + 'view-my_requests', ]; $permissionIds = Permission::whereIn('name', $perList)->pluck('id')->toArray(); $user->permissions()->attach($permissionIds); diff --git a/ProcessMaker/Repositories/CaseApiRepository.php b/ProcessMaker/Repositories/CaseApiRepository.php new file mode 100644 index 0000000000..2e5cbc2aa1 --- /dev/null +++ b/ProcessMaker/Repositories/CaseApiRepository.php @@ -0,0 +1,266 @@ +defaultFields); + $this->applyFilters($request, $query); + + return $query; + } + + /** + * Get all cases in progress + * + * @param Request $request + * + * @return Builder + */ + public function getInProgressCases(Request $request): Builder + { + $query = CaseParticipated::select($this->defaultFields) + ->where('case_status', 'IN_PROGRESS'); + $this->applyFilters($request, $query); + + return $query; + } + + /** + * Get all completed cases + * + * @param Request $request + * + * @return Builder + */ + public function getCompletedCases(Request $request): Builder + { + $query = CaseParticipated::select($this->defaultFields) + ->where('case_status', 'COMPLETED'); + $this->applyFilters($request, $query); + + return $query; + } + + /** + * Apply filters to the query. + * + * @param Request $request + * @param Builder $query + * + * @return void + */ + protected function applyFilters(Request $request, Builder $query): void + { + if ($request->filled('userId')) { + $query->where('user_id', $request->get('userId')); + } + + if ($request->filled('status')) { + $query->where('case_status', $request->get('status')); + } + + $this->search($request, $query); + $this->filterBy($request, $query); + $this->sortBy($request, $query); + } + + /** + * Search by case number or case title. + + * @param Request $request: Query parameter format: search=keyword + * @param Builder $query + * + * @return void + */ + public function search(Request $request, Builder $query): void + { + if ($request->filled('search')) { + $search = $request->get('search'); + + if (is_numeric($search)) { + $search = CaseUtils::CASE_NUMBER_PREFIX . $search; + } else { + // Remove special characters except another languages + $search = preg_replace(['/[^\p{L}\p{N}\s\']/u', '/\s{2,}/'], [' ', ' '], $search); + } + + // Add a plus (+) to the beginning and an asterisk (*) to the end of each word + $search = preg_replace_callback("/\b[\w']+\b/u", function($matches) { + return '+' . $matches[0] . '*'; + }, $search); + + $query->whereFullText($this->searchableFields, $search, ['mode' => 'boolean']); + } + } + + /** + * Filter the query. + * + * @param Request $request: Query parameter format: filterBy[field]=value&filterBy[field2]=value2&... + * @param Builder $query + * @param array $dateFields List of date fields in current model + * + * @return void + */ + public function filterBy(Request $request, Builder $query): void + { + // Check if filterBy exists in the request + if (!$request->has('filterBy')) { + return; + } + + // Get the filter input and apply the filter only if it's not empty + $filters = $request->input('filterBy', ''); + if (empty($filters)) { + return; + } + + // Apply the filters to the query + $this->executeFilters($query, $filters); + } + + /** + * Execute advanced filters to the query. + * + * @param Builder $query + * @param array|string $filters + * @return void + */ + protected function executeFilters(Builder $query, $filters): void + { + CasesFilter::filter($query, $filters); + } + + /** + * Sort the query. + * + * @param Request $request: Query parameter format: sortBy=field:asc,field2:desc,... + * @param Builder $query + * + * @return void + */ + public function sortBy(Request $request, Builder $query): void + { + if ($request->filled('sortBy')) { + $sort = explode(',', $request->get('sortBy')); + + foreach ($sort as $value) { + $sort = explode(':', $value); + $field = $sort[0]; + $order = $sort[1] ?? self::DEFAULT_SORT_DIRECTION; + + if (!in_array($field, $this->sortableFields)) { + throw new CaseValidationException("Sort by field $field is not allowed."); + } + + $query->orderBy($field, $order); + } + } + } + + /** + * Get users by participant IDs. + * + * @param Collection $data + * @return Collection The users collection grouped by user ID. + */ + public function getUsers($data): Collection + { + try { + $participantIds = $data->pluck('participants') + ->collapse() + ->unique() + ->values(); + + return DB::table('users') + ->select([ + 'id', + DB::raw('TRIM(CONCAT(firstname, " ", lastname)) as name'), + 'title', + 'avatar', + ]) + ->whereIn('id', $participantIds) + ->get() + ->keyBy('id'); + } catch (QueryException $e) { + \Log::error($e->getMessage()); + } catch (\Exception $e) { + \Log::error($e->getMessage()); + } + + return collect(); + } +} diff --git a/ProcessMaker/Repositories/CaseParticipatedRepository.php b/ProcessMaker/Repositories/CaseParticipatedRepository.php new file mode 100644 index 0000000000..b1cdb1201b --- /dev/null +++ b/ProcessMaker/Repositories/CaseParticipatedRepository.php @@ -0,0 +1,121 @@ +checkIfCaseParticipatedExist($token->user->id, $case->case_number)) { + return; + } + + try { + CaseParticipated::create([ + 'user_id' => $token->user->id, + 'case_number' => $case->case_number, + 'case_title' => $case->case_title, + 'case_title_formatted' => $case->case_title_formatted, + 'case_status' => $case->case_status, + 'processes' => CaseUtils::storeProcesses($token->processRequest, collect()), + 'requests' => CaseUtils::storeRequests($token->processRequest, collect()), + 'request_tokens' => CaseUtils::storeRequestTokens($token->getKey(), collect()), + 'tasks' => CaseUtils::storeTasks($token, collect()), + 'participants' => $case->participants, + 'initiated_at' => $case->initiated_at, + 'completed_at' => null, + 'keywords' => $case->keywords, + ]); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } + + /** + * Update the case participated. + * + * @param CaseStarted $case + * @param TokenInterface $token + * @return void + */ + public function update(CaseStarted $case, TokenInterface $token) + { + try { + if (!$this->checkIfCaseParticipatedExist($token->user->id, $case->case_number)) { + return; + } + + $this->caseParticipated->updateOrFail([ + 'case_title' => $case->case_title, + 'case_title_formatted' => $case->case_title_formatted, + 'case_status' => $case->case_status, + 'processes' => CaseUtils::storeProcesses($token->processRequest, $this->caseParticipated->processes), + 'requests' => CaseUtils::storeRequests($token->processRequest, $this->caseParticipated->requests), + 'request_tokens' => CaseUtils::storeRequestTokens($token->getKey(), $this->caseParticipated->request_tokens), + 'tasks' => CaseUtils::storeTasks($token, $this->caseParticipated->tasks), + 'participants' => $case->participants, + 'keywords' => $case->keywords, + ]); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } + + /** + * Update the status of a case participated. + * + * @param int $caseNumber + * @param array $statusData + * @return void + */ + public function updateStatus(int $caseNumber, array $statusData) + { + try { + CaseParticipated::where('case_number', $caseNumber) + ->update($statusData); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } + + /** + * Check if a case participated exists. + * If it exists, store the instance in the property. + * The property is used to update the JSON fields of the case participated. + * + * @param int $userId + * @param int $caseNumber + * + * @return bool + */ + private function checkIfCaseParticipatedExist(int $userId, int $caseNumber): bool + { + $this->caseParticipated = CaseParticipated::where('user_id', $userId) + ->where('case_number', $caseNumber) + ->first(); + + return !is_null($this->caseParticipated); + } +} diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseRepository.php new file mode 100644 index 0000000000..07a8eeabc1 --- /dev/null +++ b/ProcessMaker/Repositories/CaseRepository.php @@ -0,0 +1,202 @@ +caseParticipatedRepository = new CaseParticipatedRepository(); + } + + /** + * Store a new case started. + * + * @param ExecutionInstanceInterface $instance + * @return void + */ + public function create(ExecutionInstanceInterface $instance): void + { + if (is_null($instance->case_number)) { + Log::error('case number is required, method=create, instance=' . $instance->getKey()); + + return; + } + + if ($this->checkIfCaseStartedExist($instance->case_number)) { + $this->updateSubProcesses($instance); + + return; + } + + try { + CaseStarted::create([ + 'case_number' => $instance->case_number, + 'user_id' => $instance->user_id, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => $instance->status === self::CASE_STATUS_ACTIVE ? 'IN_PROGRESS' : $instance->status, + 'processes' => CaseUtils::storeProcesses($instance, collect()), + 'requests' => CaseUtils::storeRequests($instance, collect()), + 'request_tokens' => [], + 'tasks' => [], + 'participants' => [], + 'initiated_at' => $instance->initiated_at, + 'completed_at' => null, + 'keywords' => $instance->case_title, + ]); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } + + /** + * Update the case started. + * + * @param ExecutionInstanceInterface $instance + * @param TokenInterface $token + * @return void + */ + public function update(ExecutionInstanceInterface $instance, TokenInterface $token): void + { + if (!$this->checkIfCaseStartedExist($instance->case_number)) { + Log::error('case started not found, method=update, instance=' . $instance->getKey()); + + return; + } + + try { + $this->case->case_title = $instance->case_title; + $this->case->case_status = $instance->status === self::CASE_STATUS_ACTIVE ? 'IN_PROGRESS' : $instance->status; + $this->case->request_tokens = CaseUtils::storeRequestTokens($token->getKey(), $this->case->request_tokens); + $this->case->tasks = CaseUtils::storeTasks($token, $this->case->tasks); + $this->case->keywords = $instance->case_title; + + $this->updateParticipants($token); + + $this->case->saveOrFail(); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } + + /** + * Update the status of a case started. + * + * @param ExecutionInstanceInterface $instance + * @return void + */ + public function updateStatus(ExecutionInstanceInterface $instance): void + { + // If a sub-process is completed, do not update the case started status + if (!is_null($instance->parent_request_id)) { + return; + } + + try { + $data = [ + 'case_status' => $instance->status, + ]; + + if ($instance->status === 'COMPLETED') { + $data['completed_at'] = Carbon::now(); + } + + // Update the case started and case participated + CaseStarted::where('case_number', $instance->case_number)->update($data); + $this->caseParticipatedRepository->updateStatus($instance->case_number, $data); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } + + /** + * Update the participants of the case started. + * + * @param TokenInterface $token + * @return void + */ + private function updateParticipants(TokenInterface $token): void + { + $user = $token->user; + + if (!$user) { + return; + } + + $participantExists = $this->case->participants->contains($user->id); + + if (!$participantExists) { + $this->case->participants->push($user->id); + + $this->caseParticipatedRepository->create($this->case, $token); + } + + $this->caseParticipatedRepository->update($this->case, $token); + } + + /** + * Check if the case started exist. + * + * @param int|null $caseNumber + * @return bool + */ + private function checkIfCaseStartedExist(int | null $caseNumber): bool + { + if (is_null($caseNumber)) { + return false; + } + + $this->case = CaseStarted::where('case_number', $caseNumber)->first(); + + return !is_null($this->case); + } + + /** + * Update the processes and requests of the case started. + * + * @param ExecutionInstanceInterface $instance + * @return void + */ + private function updateSubProcesses(ExecutionInstanceInterface $instance): void + { + if (is_null($instance->parent_request_id)) { + return; + } + + try { + // Store the sub-processes and requests + $this->case->processes = CaseUtils::storeProcesses($instance, $this->case->processes); + $this->case->requests = CaseUtils::storeRequests($instance, $this->case->requests); + + $this->case->saveOrFail(); + } catch (\Exception $e) { + Log::error('CaseException: ' . $e->getMessage()); + Log::error('CaseException: ' . $e->getTraceAsString()); + } + } +} diff --git a/ProcessMaker/Repositories/CaseSyncRepository.php b/ProcessMaker/Repositories/CaseSyncRepository.php new file mode 100644 index 0000000000..06a9b2deab --- /dev/null +++ b/ProcessMaker/Repositories/CaseSyncRepository.php @@ -0,0 +1,191 @@ +whereIn('id', $requestIds) + ->get(); + + $successes = []; + $errors = []; + + foreach ($requests as $instance) { + try { + if (!is_null($instance->parent_request_id)) { + continue; + } + + $csProcesses = CaseUtils::storeProcesses($instance, collect()); + $csRequests = CaseUtils::storeRequests($instance, collect()); + $csRequestTokens = collect(); + $csTasks = collect(); + $participants = $instance->participants->map->only('id', 'fullName', 'title', 'avatar'); + $status = $instance->status === CaseRepository::CASE_STATUS_ACTIVE ? 'IN_PROGRESS' : $instance->status; + + $cpData = self::prepareCaseStartedData($instance, $status, $participants); + + self::processTokens($instance, $cpData, $csRequestTokens, $csTasks); + + self::processChildRequests($instance, $cpData, $csProcesses, $csRequests, $participants, $csRequestTokens, $csTasks); + + $caseStarted = CaseStarted::updateOrCreate( + ['case_number' => $instance->case_number], + [ + 'user_id' => $instance->user_id, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => $status, + 'processes' => $csProcesses, + 'requests' => $csRequests, + 'request_tokens' => $csRequestTokens, + 'tasks' => $csTasks, + 'participants' => $participants, + 'initiated_at' => $instance->initiated_at, + 'completed_at' => $instance->completed_at, + 'keywords' => CaseUtils::getCaseNumberByKeywords($instance->case_number) . ' ' . $instance->case_title, + ], + ); + + $successes[] = $caseStarted->case_number; + } catch (\Exception $e) { + $errors[] = $instance->case_number . ' ' . $e->getMessage(); + } + } + + return [ + 'successes' => $successes, + 'errors' => $errors, + ]; + } + + /** + * Prepare the case started data. + * + * @param ProcessRequest $instance + * @param string $status + * @param Collection $participants + * @return array + */ + private static function prepareCaseStartedData($instance, $status, $participants) + { + return [ + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => $status, + 'processes' => CaseUtils::storeProcesses($instance, collect()), + 'requests' => CaseUtils::storeRequests($instance, collect()), + 'request_tokens' => collect(), + 'tasks' => collect(), + 'participants' => $participants, + 'initiated_at' => $instance->initiated_at, + 'completed_at' => $instance->completed_at, + 'keywords' => CaseUtils::getCaseNumberByKeywords($instance->case_number) . ' ' . $instance->case_title, + ]; + } + + /** + * Process the tokens. + * + * @param ProcessRequest $instance + * @param array $cpData + * @param Collection $csRequestTokens + * @param Collection $csTasks + * @return void + */ + private static function processTokens($instance, &$cpData, &$csRequestTokens, &$csTasks) + { + foreach ($instance->tokens as $token) { + if (in_array($token->element_type, CaseUtils::ALLOWED_REQUEST_TOKENS)) { + $cpData['processes'] = CaseUtils::storeProcesses($instance, $cpData['processes']); + $cpData['requests'] = CaseUtils::storeRequests($instance, $cpData['requests']); + $cpData['request_tokens'] = CaseUtils::storeRequestTokens($token->getKey(), $cpData['request_tokens']); + $cpData['tasks'] = CaseUtils::storeTasks($token, $cpData['tasks']); + + $csRequestTokens = CaseUtils::storeRequestTokens($token->getKey(), $csRequestTokens); + $csTasks = CaseUtils::storeTasks($token, $csTasks); + + self::syncCasesParticipated($instance->case_number, $token->user_id, $cpData); + } + } + } + + /** + * Process the child requests. + * + * @param ProcessRequest $instance + * @param array $cpData + * @param Collection $csProcesses + * @param Collection $csRequests + * @param Collection $participants + * @param Collection $csRequestTokens + * @param Collection $csTasks + * @return void + */ + private static function processChildRequests( + $instance, &$cpData, &$csProcesses, &$csRequests, &$participants, &$csRequestTokens, &$csTasks + ) { + foreach ($instance->childRequests as $subProcess) { + $cpData['processes'] = CaseUtils::storeProcesses($subProcess, collect()); + $cpData['requests'] = CaseUtils::storeRequests($subProcess, collect()); + $cpData['request_tokens'] = collect(); + $cpData['tasks'] = collect(); + + $csProcesses = CaseUtils::storeProcesses($subProcess, $csProcesses); + $csRequests = CaseUtils::storeRequests($subProcess, $csRequests); + + $participants = $participants + ->merge($subProcess->participants->map->only('id', 'fullName', 'title', 'avatar')) + ->unique('id') + ->values(); + + foreach ($subProcess->tokens as $spToken) { + if (in_array($spToken->element_type, CaseUtils::ALLOWED_REQUEST_TOKENS)) { + $cpData['processes'] = CaseUtils::storeProcesses($subProcess, $cpData['processes']); + $cpData['requests'] = CaseUtils::storeRequests($subProcess, $cpData['requests']); + $cpData['request_tokens'] = CaseUtils::storeRequestTokens($spToken->getKey(), $cpData['request_tokens']); + $cpData['tasks'] = CaseUtils::storeTasks($spToken, $cpData['tasks']); + + $csRequestTokens = CaseUtils::storeRequestTokens($spToken->getKey(), $csRequestTokens); + $csTasks = CaseUtils::storeTasks($spToken, $csTasks); + + self::syncCasesParticipated($instance->case_number, $spToken->user_id, $cpData); + } + } + } + } + + /** + * Sync the cases participated. + * + * @param CaseStarted $caseStarted + * @param TokenInterface $token + * @return void + */ + private static function syncCasesParticipated($caseNumber, $userId, $data) + { + CaseParticipated::updateOrCreate( + [ + 'case_number' => $caseNumber, + 'user_id' => $userId, + ], + $data, + ); + } +} diff --git a/ProcessMaker/Repositories/CaseUtils.php b/ProcessMaker/Repositories/CaseUtils.php new file mode 100644 index 0000000000..aaa296ed82 --- /dev/null +++ b/ProcessMaker/Repositories/CaseUtils.php @@ -0,0 +1,102 @@ + self::CASE_NUMBER_PREFIX . substr($caseNumber, 0, $i), + range(1, strlen($caseNumber)) + ); + + return implode(' ', $keywords); + } + + /** + * Store processes. + * + * @param ExecutionInstanceInterface $instance + * @param Collection $processes + * @return Collection + */ + public static function storeProcesses(ExecutionInstanceInterface $instance, Collection $processes) + { + return $processes->push([ + 'id' => $instance->process->id, + 'name' => $instance->process->name, + ]) + ->unique('id') + ->values(); + } + + /** + * Store requests. + * + * @param ExecutionInstanceInterface $instance + * @param Collection $requests + * @return Collection + */ + public static function storeRequests(ExecutionInstanceInterface $instance, Collection $requests) + { + return $requests->push([ + 'id' => $instance->id, + 'name' => $instance->name, + 'parent_request_id' => $instance?->parentRequest?->id, + ]) + ->unique('id') + ->values(); + } + + /** + * Store request tokens. + * + * @param int $tokenId + * @param Collection $requestTokens + * @return Collection + */ + public static function storeRequestTokens(int $tokenId, Collection $requestTokens) + { + return $requestTokens->push($tokenId) + ->unique() + ->values(); + } + + /** + * Store tasks. + * + * @param TokenInterface $token + * @param Collection $tasks + * @return Collection + */ + public static function storeTasks(TokenInterface $token, Collection $tasks) + { + if (in_array($token->element_type, self::ALLOWED_ELEMENT_TYPES)) { + return $tasks->push([ + 'id' => $token->getKey(), + 'element_id' => $token->element_id, + 'name' => $token->element_name, + 'process_id' => $token->process_id, + ]) + ->unique('id') + ->values(); + } + + return $tasks; + } +} diff --git a/ProcessMaker/Repositories/ExecutionInstanceRepository.php b/ProcessMaker/Repositories/ExecutionInstanceRepository.php index 5c6641ddb0..f8110986f1 100644 --- a/ProcessMaker/Repositories/ExecutionInstanceRepository.php +++ b/ProcessMaker/Repositories/ExecutionInstanceRepository.php @@ -3,6 +3,8 @@ namespace ProcessMaker\Repositories; use Carbon\Carbon; +use ProcessMaker\Jobs\CaseStore; +use ProcessMaker\Jobs\CaseUpdateStatus; use ProcessMaker\Models\ProcessCollaboration; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Nayra\Contracts\Bpmn\ParticipantInterface; @@ -184,6 +186,8 @@ public function persistInstanceCreated(ExecutionInstanceInterface $instance) // Set id $instance->setId($instance->getKey()); + CaseStore::dispatch($instance); + // Persists collaboration $this->persistCollaboration($instance); } @@ -208,6 +212,8 @@ public function persistInstanceError(ExecutionInstanceInterface $instance) $instance->status = 'ERROR'; $instance->mergeLatestStoredData(); $instance->saveOrFail(); + + CaseUpdateStatus::dispatch($instance); } /** @@ -255,6 +261,8 @@ public function persistInstanceCompleted(ExecutionInstanceInterface $instance) $instance->completed_at = Carbon::now(); $instance->mergeLatestStoredData(); $instance->saveOrFail(); + + CaseUpdateStatus::dispatch($instance); } /** diff --git a/ProcessMaker/Repositories/TokenRepository.php b/ProcessMaker/Repositories/TokenRepository.php index ac00c94301..5e9808ee1c 100644 --- a/ProcessMaker/Repositories/TokenRepository.php +++ b/ProcessMaker/Repositories/TokenRepository.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Mustache_Engine; +use ProcessMaker\Jobs\CaseUpdate; use ProcessMaker\Mail\TaskActionByEmail; use ProcessMaker\Models\ProcessAbeRequestToken; use ProcessMaker\Models\ProcessCollaboration; @@ -162,6 +163,9 @@ public function persistActivityActivated(ActivityInterface $activity, TokenInter $token->setId($token->getKey()); $request = $token->getInstance(); $request->notifyProcessUpdated('ACTIVITY_ACTIVATED', $token); + + CaseUpdate::dispatch($request, $token); + if (!is_null($user)) { $this->validateAndSendActionByEmail($activity, $token, $user->email); } diff --git a/ProcessMaker/Rules/SortBy.php b/ProcessMaker/Rules/SortBy.php new file mode 100644 index 0000000000..2fb3398141 --- /dev/null +++ b/ProcessMaker/Rules/SortBy.php @@ -0,0 +1,25 @@ + 'IN_PROGRESS', + 'completed' => 'COMPLETED', + 'error' => 'ERROR', + 'canceled' => 'CANCELED', + ]; + + $value = mb_strtolower($value); + + return function ($query) use ($value, $statusMap, $expression) { + if (array_key_exists($value, $statusMap)) { + $value = $statusMap[$value]; + } + $query->where('case_status', $expression->operator, $value); + }; + } +} diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index e4d171cc45..62ac5055e1 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -7,6 +7,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use ProcessMaker\Filters\Filter; +use ProcessMaker\Managers\DataManager; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; @@ -17,18 +18,18 @@ trait TaskControllerIndexMethods private function indexBaseQuery($request) { $query = ProcessRequestToken::exclude(['data'])->with([ - 'processRequest' => fn($q) => $q->exclude(['data']), + 'processRequest' => fn ($q) => $q->exclude(['data']), // review if bpmn is reuiqred here process - 'process' => fn($q) => $q->exclude(['svg', 'warnings']), + 'process' => fn ($q) => $q->exclude(['svg', 'warnings']), // review if bpmn is reuiqred here processRequest.process - 'processRequest.process' => fn($q) => $q->exclude(['svg', 'warnings']), + 'processRequest.process' => fn ($q) => $q->exclude(['svg', 'warnings']), // The following lines use to much memory but reduce the number of queries // bpmn is required here in processRequest.processVersion // 'processRequest.processVersion' => fn($q) => $q->exclude(['svg', 'warnings']), // review if bpmn is reuiqred here processRequest.processVersion.process // 'processRequest.processVersion.process' => fn($q) => $q->exclude(['svg', 'warnings']), 'user', - 'draft' + 'draft', ]); $include = $request->input('include') ? explode(',', $request->input('include')) : []; @@ -123,6 +124,18 @@ private function applyDefaultFiltering($query, $column, $filterByFields, $fieldF $query->where(is_string($key) ? $key : $column, $operator, $fieldFilter); } + private function addTaskData($response) + { + $dataManager = new DataManager(); + $response->getCollection()->transform(function ($row) use ($dataManager) { + $row->taskData = $dataManager->getData($row); + + return $row; + }); + + return $response; + } + private function excludeNonVisibleTasks($query, $request) { $nonSystem = filter_var($request->input('non_system'), FILTER_VALIDATE_BOOLEAN); diff --git a/composer.json b/composer.json index 2156dfe769..bb2f84ef8e 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "jenssegers/agent": "^2.6", "laravel/framework": "^10.19", "laravel/horizon": "^5.12", + "laravel/pail": "*", "laravel/passport": "^11.5", "laravel/scout": "^9.8", "laravel/telescope": "^4.12", diff --git a/composer.lock b/composer.lock index d4c21081d7..ec292a362f 100644 --- a/composer.lock +++ b/composer.lock @@ -3484,6 +3484,84 @@ }, "time": "2023-11-23T15:47:58+00:00" }, + { + "name": "laravel/pail", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "c22fe771277971eb9cd224955996bcf39c1a710d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/c22fe771277971eb9cd224955996bcf39c1a710d", + "reference": "c22fe771277971eb9cd224955996bcf39c1a710d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-pcntl": "*", + "illuminate/console": "^10.24|^11.0", + "illuminate/contracts": "^10.24|^11.0", + "illuminate/log": "^10.24|^11.0", + "illuminate/process": "^10.24|^11.0", + "illuminate/support": "^10.24|^11.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/pint": "^1.13", + "orchestra/testbench": "^8.12|^9.0", + "pestphp/pest": "^2.20", + "pestphp/pest-plugin-type-coverage": "^2.3", + "phpstan/phpstan": "^1.10", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2024-05-08T18:19:39+00:00" + }, { "name": "laravel/passport", "version": "v11.10.0", diff --git a/database/factories/CaseParticipatedFactory.php b/database/factories/CaseParticipatedFactory.php new file mode 100644 index 0000000000..2c39ca285b --- /dev/null +++ b/database/factories/CaseParticipatedFactory.php @@ -0,0 +1,78 @@ + + */ +class CaseParticipatedFactory extends Factory +{ + protected $model = CaseParticipated::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $users = User::get(); + + $caseTitle = fake()->words(4, true); + $caseNumber = fake()->unique()->randomNumber(); + + return [ + 'user_id' => $users->random()->id, + 'case_number' => $caseNumber, + 'case_title' => $caseTitle, + 'case_title_formatted' => fake()->words(3, true), + 'case_status' => fake()->randomElement(['IN_PROGRESS', 'COMPLETED']), + 'processes' => array_map(function() { + return [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(2, true), + ]; + }, range(1, 3)), + 'requests' => [ + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(2, true), + 'parent_request' => fake()->randomNumber(), + ], + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(3, true), + 'parent_request' => fake()->randomNumber(), + ], + ], + 'request_tokens' => array_map(fn() => fake()->randomElement([ + fake()->randomNumber(), + fake()->randomNumber(), + fake()->randomNumber(), + ]), range(1, 3)), + 'tasks' => [ + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(4, true), + ], + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(3, true), + ], + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(2, true), + ], + ], + 'participants' => array_map(fn() => fake()->randomElement($users->pluck('id')->toArray()), range(1, 3)), + 'initiated_at' => fake()->dateTime(), + 'completed_at' => fake()->dateTime(), + 'keywords' => CaseUtils::getCaseNumberByKeywords($caseNumber) . ' ' . $caseTitle, + ]; + } +} diff --git a/database/factories/CaseStartedFactory.php b/database/factories/CaseStartedFactory.php new file mode 100644 index 0000000000..c20a3c9363 --- /dev/null +++ b/database/factories/CaseStartedFactory.php @@ -0,0 +1,78 @@ + + */ +class CaseStartedFactory extends Factory +{ + protected $model = CaseStarted::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $users = User::get(); + + $caseTitle = fake()->words(4, true); + $caseNumber = fake()->unique()->randomNumber(); + + return [ + 'case_number' => $caseNumber, + 'user_id' => $users->random()->id, + 'case_title' => $caseTitle, + 'case_title_formatted' => $caseTitle, + 'case_status' => fake()->randomElement(['IN_PROGRESS', 'COMPLETED']), + 'processes' => array_map(function() { + return [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(2, true), + ]; + }, range(1, 3)), + 'requests' => [ + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(2, true), + 'parent_request' => fake()->randomNumber(), + ], + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(3, true), + 'parent_request' => fake()->randomNumber(), + ], + ], + 'request_tokens' => array_map(fn() => fake()->randomElement([ + fake()->randomNumber(), + fake()->randomNumber(), + fake()->randomNumber(), + ]), range(1, 3)), + 'tasks' => [ + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(4, true), + ], + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(3, true), + ], + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(2, true), + ], + ], + 'participants' => array_map(fn() => fake()->randomElement($users->pluck('id')->toArray()), range(1, 3)), + 'initiated_at' => fake()->dateTime(), + 'completed_at' => fake()->dateTime(), + 'keywords' => CaseUtils::getCaseNumberByKeywords($caseNumber) . ' ' . $caseTitle, + ]; + } +} diff --git a/database/migrations/2024_09_09_181717_create_cases_started_table.php b/database/migrations/2024_09_09_181717_create_cases_started_table.php new file mode 100644 index 0000000000..e2a380298e --- /dev/null +++ b/database/migrations/2024_09_09_181717_create_cases_started_table.php @@ -0,0 +1,49 @@ +id(); + $table->unsignedInteger('case_number')->unique(); + $table->unsignedInteger('user_id'); + $table->string('case_title', 255); + $table->text('case_title_formatted'); + $table->string('case_status', 20); + $table->json('processes'); + $table->json('requests'); + $table->json('request_tokens'); + $table->json('tasks'); + $table->json('participants'); + $table->timestamp('initiated_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + $table->text('keywords'); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + $table->index(['case_number']); + $table->index(['user_id', 'case_status', 'created_at']); + $table->index(['user_id', 'case_status', 'updated_at']); + + $table->fullText('case_title'); + $table->fullText('keywords'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cases_started'); + } +}; diff --git a/database/migrations/2024_09_12_172734_create_cases_participated_table.php b/database/migrations/2024_09_12_172734_create_cases_participated_table.php new file mode 100644 index 0000000000..2d0eef7dc3 --- /dev/null +++ b/database/migrations/2024_09_12_172734_create_cases_participated_table.php @@ -0,0 +1,49 @@ +id(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('case_number'); + $table->string('case_title', 255); + $table->text('case_title_formatted'); + $table->string('case_status', 20); + $table->json('processes'); + $table->json('requests'); + $table->json('request_tokens'); + $table->json('tasks'); + $table->json('participants'); + $table->timestamp('initiated_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + $table->text('keywords'); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + $table->index(['user_id', 'case_number']); + $table->index(['user_id', 'case_status', 'created_at']); + $table->index(['user_id', 'case_status', 'completed_at']); + + $table->fullText('case_title'); + $table->fullText('keywords'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cases_participated'); + } +}; diff --git a/database/migrations/2024_10_04_154956_add_case_number_to_comments_table.php b/database/migrations/2024_10_04_154956_add_case_number_to_comments_table.php new file mode 100644 index 0000000000..fb41fb6520 --- /dev/null +++ b/database/migrations/2024_10_04_154956_add_case_number_to_comments_table.php @@ -0,0 +1,31 @@ +unsignedInteger('case_number')->nullable(); + + $table->index('type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comments', function (Blueprint $table) { + $table->dropColumn('case_number'); + + $table->dropIndex(['type']); + }); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index f0e3c0b198..8acb45e4da 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -3,7 +3,9 @@ namespace Database\Seeders; use Illuminate\Database\Seeder; +use ProcessMaker\Models\Group; use ProcessMaker\Models\Permission; +use ProcessMaker\Models\User; class PermissionSeeder extends Seeder { @@ -83,8 +85,10 @@ class PermissionSeeder extends Seeder 'Username and Password' => [ 'edit-user-and-password', ], - 'Requests' => [ + 'Cases and Requests' => [ + 'view-all_cases', 'view-all_requests', + 'view-my_requests', 'edit-request_data', 'edit-task_data', ], @@ -114,16 +118,24 @@ class PermissionSeeder extends Seeder ], ]; + private $defaultPermissions = [ + 'view-my_requests', + ]; + public function run($seedUser = null) { foreach ($this->permissionGroups as $groupName => $permissions) { foreach ($permissions as $permissionString) { - Permission::updateOrCreate([ + $permission = Permission::updateOrCreate([ 'name' => $permissionString, ], [ 'title' => ucwords(preg_replace('/(\-|_)/', ' ', $permissionString)), 'group' => $groupName, ]); + + if (in_array($permissionString, $this->defaultPermissions)) { + $this->assignDefaultPermission($permission); + } } } @@ -134,4 +146,26 @@ public function run($seedUser = null) $seedUser->save(); } } + + /** + * Assign default permission to users and groups. + */ + private function assignDefaultPermission(Permission $permission): void + { + $userIds = User::nonSystem()->pluck('id'); + $groupIds = Group::pluck('id'); + + // Define the chunk size for the permission assignment + $chunkSize = 500; + + // Attach user IDs in chunks + $userIds->chunk($chunkSize)->each(function ($chunk) use ($permission) { + $permission->users()->attach($chunk); + }); + + // Attach group IDs in chunks + $groupIds->chunk($chunkSize)->each(function ($chunk) use ($permission) { + $permission->groups()->attach($chunk); + }); + } } diff --git a/package.json b/package.json index fbf201eddc..65f373fb4d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@babel/eslint-parser": "^7.15.8", "@babel/preset-env": "^7.23.9", "accounting": "^0.4.1", + "autoprefixer": "^10.4.20", "babel-plugin-istanbul": "^6.1.1", "chartjs-plugin-colorschemes": "^0.4.0", "cross-env": "^7.0.3", @@ -38,9 +39,11 @@ "laravel-mix": "^6.0.49", "moment": "^2.30.1", "moment-timezone": "^0.5.45", + "postcss": "^8.4.45", "resolve-url-loader": "^3.1.2", "sass": "^1.77.4", "sass-loader": "^12.6.0", + "tailwindcss": "^3.4.10", "vue-loader": "^15.10.0", "vue-template-compiler": "^2.7.16" }, @@ -61,7 +64,6 @@ "@processmaker/vue-form-elements": "0.60.0", "@processmaker/vue-multiselect": "2.3.0", "@tinymce/tinymce-vue": "2.0.0", - "autoprefixer": "10.4.5", "axios": "^0.27.2", "bootstrap": "^4.5.3", "bootstrap-vue": "^2.18.1", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..33ad091d26 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/resources/js/components/shared/FilterTable.vue b/resources/js/components/shared/FilterTable.vue index aafdb15225..9e49f82b07 100644 --- a/resources/js/components/shared/FilterTable.vue +++ b/resources/js/components/shared/FilterTable.vue @@ -382,6 +382,13 @@ export default { right: 7px; background-color: #F2F8FE } +.pm-table-ellipsis-column{ + text-transform: capitalize; +} +.pm-table-column-header-text { + overflow: hidden; + text-overflow: ellipsis; +} .pm-table-ellipsis-column .pm-table-filter-button { opacity: 0; visibility: hidden; diff --git a/resources/js/requests/components/RequestsListing.vue b/resources/js/requests/components/RequestsListing.vue index ef273f8e53..dabd3b3b8a 100644 --- a/resources/js/requests/components/RequestsListing.vue +++ b/resources/js/requests/components/RequestsListing.vue @@ -275,6 +275,13 @@ export default { return this.$props.columns; } return [ + { + label: "Request ID", + field: "id", + sortable: true, + default: true, + width: 115, + }, { label: "Case #", field: "case_number", @@ -408,7 +415,7 @@ export default { return ` - ${value.case_title_formatted || value.case_title || ""} + ${value.case_title_formatted || value.case_title || value.name} `; }, formatParticipants(participants) { @@ -622,15 +629,6 @@ export default { } }; - diff --git a/resources/js/tasks/components/TasksList.vue b/resources/js/tasks/components/TasksList.vue index 92f9192799..76c159ae43 100644 --- a/resources/js/tasks/components/TasksList.vue +++ b/resources/js/tasks/components/TasksList.vue @@ -924,7 +924,7 @@ export default { }; - diff --git a/resources/jscomposition/base/form/InputLeading.vue b/resources/jscomposition/base/form/InputLeading.vue new file mode 100644 index 0000000000..5724ed5aea --- /dev/null +++ b/resources/jscomposition/base/form/InputLeading.vue @@ -0,0 +1,65 @@ + + diff --git a/resources/jscomposition/base/form/index.js b/resources/jscomposition/base/form/index.js new file mode 100644 index 0000000000..8eec1e2f5b --- /dev/null +++ b/resources/jscomposition/base/form/index.js @@ -0,0 +1,9 @@ +import Dropdown from "./Dropdown.vue"; +import InputLeading from "./InputLeading.vue"; + +export default {} + +export { + Dropdown, + InputLeading +} \ No newline at end of file diff --git a/resources/jscomposition/base/index.js b/resources/jscomposition/base/index.js new file mode 100644 index 0000000000..ddbe2a67b6 --- /dev/null +++ b/resources/jscomposition/base/index.js @@ -0,0 +1,7 @@ +export * from "./buttons/index" +export * from "./cards/index" +export * from "./form/index" +export * from "./table/index" +export * from "./ui/index" + +export default {} diff --git a/resources/jscomposition/base/table/BaseTable.vue b/resources/jscomposition/base/table/BaseTable.vue new file mode 100644 index 0000000000..6e0b02a8a4 --- /dev/null +++ b/resources/jscomposition/base/table/BaseTable.vue @@ -0,0 +1,120 @@ + + + diff --git a/resources/jscomposition/base/table/ContainerRow.vue b/resources/jscomposition/base/table/ContainerRow.vue new file mode 100644 index 0000000000..0e960b83b0 --- /dev/null +++ b/resources/jscomposition/base/table/ContainerRow.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/jscomposition/base/table/TCell.vue b/resources/jscomposition/base/table/TCell.vue new file mode 100644 index 0000000000..81aba43a3c --- /dev/null +++ b/resources/jscomposition/base/table/TCell.vue @@ -0,0 +1,70 @@ + + + diff --git a/resources/jscomposition/base/table/THeader.vue b/resources/jscomposition/base/table/THeader.vue new file mode 100644 index 0000000000..be3c1e0588 --- /dev/null +++ b/resources/jscomposition/base/table/THeader.vue @@ -0,0 +1,58 @@ + + + diff --git a/resources/jscomposition/base/table/TRow.vue b/resources/jscomposition/base/table/TRow.vue new file mode 100644 index 0000000000..7506895657 --- /dev/null +++ b/resources/jscomposition/base/table/TRow.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/jscomposition/base/table/composables/columnComposable.js b/resources/jscomposition/base/table/composables/columnComposable.js new file mode 100644 index 0000000000..dde1e46c5c --- /dev/null +++ b/resources/jscomposition/base/table/composables/columnComposable.js @@ -0,0 +1,52 @@ +import { ref, onUnmounted } from "vue"; + +export default {}; +/** + * This composable only works with columns in AppTable + * @param {*} column is a ref variable, come from App Table + * @param {*} tableName + * @returns + */ +export const columnResizeComposable = (column) => { + const startX = ref(0); + const startWidth = ref(0); + const isResizing = ref(false); + + //Resize the column value + const doResize = (event) => { + if (isResizing.value) { + const diff = event.pageX - startX.value; + const min = 30; + const currentWidth = Math.max(min, startWidth.value + diff); + + column.width = currentWidth; + } + }; + + const stopResize = () => { + if (isResizing.value) { + document.removeEventListener("mousemove", doResize); + document.removeEventListener("mouseup", stopResize); + isResizing.value = false; + } + }; + + // Init the events in mousemove and finish in mouseup event + const startResize = (event, index) => { + isResizing.value = true; + startX.value = event.pageX; + startWidth.value = column.width || 200; + + document.addEventListener("mousemove", doResize); + document.addEventListener("mouseup", stopResize); + }; + + onUnmounted(() => { + document.removeEventListener("mousemove", doResize); + document.removeEventListener("mouseup", stopResize); + }); + + return { + startResize, + }; +}; diff --git a/resources/jscomposition/base/table/index.js b/resources/jscomposition/base/table/index.js new file mode 100644 index 0000000000..c290a40e63 --- /dev/null +++ b/resources/jscomposition/base/table/index.js @@ -0,0 +1,9 @@ +import BaseTable from "./BaseTable.vue"; +import Pagination from "../../system/table/Pagination.vue"; +import TCell from "./TCell.vue"; +import THeader from "./THeader.vue"; +import TRow from "./TRow.vue"; + +export default {}; + +export { BaseTable, Pagination, TCell, THeader, TRow }; diff --git a/resources/jscomposition/base/ui/AppAvatar.vue b/resources/jscomposition/base/ui/AppAvatar.vue new file mode 100644 index 0000000000..185a9a4eb8 --- /dev/null +++ b/resources/jscomposition/base/ui/AppAvatar.vue @@ -0,0 +1,35 @@ + + diff --git a/resources/jscomposition/base/ui/AppPopover.vue b/resources/jscomposition/base/ui/AppPopover.vue new file mode 100644 index 0000000000..df33d1e9df --- /dev/null +++ b/resources/jscomposition/base/ui/AppPopover.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/resources/jscomposition/base/ui/Badge.vue b/resources/jscomposition/base/ui/Badge.vue new file mode 100644 index 0000000000..f1f6c173b4 --- /dev/null +++ b/resources/jscomposition/base/ui/Badge.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/jscomposition/base/ui/CollapsableContainer.vue b/resources/jscomposition/base/ui/CollapsableContainer.vue new file mode 100644 index 0000000000..b3fc74dcae --- /dev/null +++ b/resources/jscomposition/base/ui/CollapsableContainer.vue @@ -0,0 +1,49 @@ + + + diff --git a/resources/jscomposition/base/ui/index.js b/resources/jscomposition/base/ui/index.js new file mode 100644 index 0000000000..3ecdf6bdb9 --- /dev/null +++ b/resources/jscomposition/base/ui/index.js @@ -0,0 +1,10 @@ +import Badge from "./Badge.vue"; +import AppAvatar from "./AppAvatar.vue"; +import AppPopover from "./AppPopover.vue"; +import CollapsableContainer from "./CollapsableContainer.vue"; + +export default {}; + +export { + Badge, AppAvatar, AppPopover, CollapsableContainer, +}; diff --git a/resources/jscomposition/cases/casesDetail/api/index.js b/resources/jscomposition/cases/casesDetail/api/index.js new file mode 100644 index 0000000000..1f982e1a0c --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/api/index.js @@ -0,0 +1,58 @@ +import { api } from "../variables"; + +const getData = async () => { + const objectsList = []; + + for (let i = 0; i <= 31; i += 1) { + const obj = { + id: `${i}`, + case_number: 100, + element_name: `Case Title ${i}`, + process: { + name: `Process ${i}`, + }, + user: { + fullname: `Avatar ${i}`, + }, + current_task: `Task ${i}`, + status: "IN_PROGRESS", + started: `21/21/${i}`, + due_at: `21/21/${i}`, + completed_date: `21/21/${i}`, + screen_id: 4, + }; + + objectsList.push(obj); + } + + return objectsList; +}; + +export const getDataRequests = async ({ params, pagination }) => { + const response = await api.get("requests-by-case", { + params: { + ...params, + ...pagination, + }, + }); + + return response.data.data; +}; + +export const getDataTask = async ({ params, pagination }) => { + const response = await api.get("tasks-by-case/", { + params: { + ...params, + ...pagination, + }, + }); + + return response.data.data; +}; +const getScreenData = (id) => { + const response = ProcessMaker.apiClient.get(`screens/${id}`); + + return response; +}; + +export { getData, getScreenData }; diff --git a/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue b/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue new file mode 100644 index 0000000000..d244d7550e --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue @@ -0,0 +1,50 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/CompletedForms.vue b/resources/jscomposition/cases/casesDetail/components/CompletedForms.vue new file mode 100644 index 0000000000..df5fae7851 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/CompletedForms.vue @@ -0,0 +1,58 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/DisplayForm.vue b/resources/jscomposition/cases/casesDetail/components/DisplayForm.vue new file mode 100644 index 0000000000..3ffb06bc86 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/DisplayForm.vue @@ -0,0 +1,44 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/EllipsisMenu.vue b/resources/jscomposition/cases/casesDetail/components/EllipsisMenu.vue new file mode 100644 index 0000000000..4376c3bf79 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/EllipsisMenu.vue @@ -0,0 +1,54 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/RequestTable.vue b/resources/jscomposition/cases/casesDetail/components/RequestTable.vue new file mode 100644 index 0000000000..c3b38c5c19 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/RequestTable.vue @@ -0,0 +1,67 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/TabHistory.vue b/resources/jscomposition/cases/casesDetail/components/TabHistory.vue new file mode 100644 index 0000000000..6a8adf283a --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/TabHistory.vue @@ -0,0 +1,35 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/Tabs.vue b/resources/jscomposition/cases/casesDetail/components/Tabs.vue new file mode 100644 index 0000000000..5c9cc509c6 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/Tabs.vue @@ -0,0 +1,64 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/components/TaskTable.vue b/resources/jscomposition/cases/casesDetail/components/TaskTable.vue new file mode 100644 index 0000000000..60276e8a0a --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/components/TaskTable.vue @@ -0,0 +1,72 @@ + + + diff --git a/resources/jscomposition/cases/casesDetail/config/columns.js b/resources/jscomposition/cases/casesDetail/config/columns.js new file mode 100644 index 0000000000..7203deca15 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/config/columns.js @@ -0,0 +1,186 @@ +import { + LinkCell, + StatusCell, + TruncatedOptionsCell, + CollapseFormCell, +} from "../../../system/index"; + +export default {}; + +// Column for Task +const taskNumberColumn = () => ({ + field: "case_number", + header: "Tasks #", + resizable: true, + width: 200, + filter: true, + formatter:(row, column, columns)=>{ + return row.element_name; + }, + cellRenderer: () => ({ + component: LinkCell, + params: { + click: (row, column, columns) => { + window.document.location = `/tasks/${row.id}/edit`; + }, + }, + }), +}); + +const taskNameColumn = () => ({ + field: "element_name", + header: "Task Name", + resizable: true, + width: 200, + filter: true, + cellRenderer: () => ({ + component: LinkCell, + params: { + click: (row, column, columns) => { + window.document.location = `/tasks/${row.id}/edit`; + }, + }, + }), +}); + +const processNameColumn = () => ({ + field: "process.name", + header: "Process", + resizable: true, + width: 200, +}); + +const assignedColumn = () => ({ + field: "user.fullname", + header: "Assigned", + resizable: true, + width: 200, + filter: true, +}); + +const dueDateColumn = () => ({ + field: "due_at", + header: "Due Date", + resizable: true, + width: 200, + filter: true, +}); + +// Columns for Requests +const requestIdColumn = () => ({ + field: "id", + header: "Request ID", + resizable: true, + filter: { type: "sortable" }, + width: 80, + cellRenderer: () => ({ + component: LinkCell, + params: { + click: (row, column, columns) => { + window.document.location = `/requests/${row.id}`; + }, + }, + }), +}); + +const processRequestColumn = () => ({ + field: "name", + header: "Process Name", + resizable: true, + width: 200, + filter: { type: "sortable" }, + cellRenderer: () => ({ + component: LinkCell, + params: { + click: (row, column, columns) => { + window.document.location = `/requests/${row.id}`; + }, + }, + }), +}); + +const taskColumn = () => ({ + field: "active_tasks", + header: "Task", + resizable: true, + width: 140, + formatter:(row, column, columns)=>{ + return row.active_tasks.length? row.active_tasks[0].element_name : ""; + }, + cellRenderer: () => ({ + component: TruncatedOptionsCell, + params: { + click: (option, row, column, columns) => { + window.document.location = `/tasks/${option.id}/edit`; + }, + formatterOptions:(option, row, column, columns)=>{ + return option.element_name; + } + }, + }), +}); + +const statusColumn = () => ({ + field: "status", + header: "Status", + filter: { type: "sortable" }, + resizable: true, + width: 140, + cellRenderer: () => ({ + component: StatusCell, + }), +}); + +const startedColumn = () => ({ + field: "initiated_at", + header: "Started", + filter: { type: "sortable" }, + resizable: true, + width: 200, +}); + +const completedDateColumn = () => ({ + field: "completed_date", + header: "Completed Date", + resizable: true, + width: 200, +}); + +const actionColumn = () => ({ + field: "", + header: "", + resizable: false, + width: 50, + cellRenderer: () => CollapseFormCell, + params: {}, +}); + +export const getColumns = (type) => { + const columns = { + tasks: [ + taskNumberColumn(), + taskNameColumn(), + processNameColumn(), + assignedColumn(), + dueDateColumn(), + ], + requests: [ + requestIdColumn(), + processRequestColumn(), + taskColumn(), + statusColumn(), + startedColumn(), + ], + completed_forms: [ + actionColumn(), + taskNumberColumn(), + taskNameColumn(), + processNameColumn(), + assignedColumn(), + completedDateColumn(), + dueDateColumn(), + ], + }; + + return columns[type]; +}; diff --git a/resources/jscomposition/cases/casesDetail/edit.js b/resources/jscomposition/cases/casesDetail/edit.js new file mode 100644 index 0000000000..50e0eae0fc --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/edit.js @@ -0,0 +1,380 @@ +import Vue from "vue"; +import CaseDetail from "./components/CaseDetail.vue"; +import Tabs from "./components/Tabs.vue"; +import Timeline from "../../../js/components/Timeline.vue"; +import { CollapsableContainer } from "../../base"; + +Vue.component("Timeline", Timeline); + +new Vue({ + el: "#case-detail", + components: { CaseDetail, Tabs, CollapsableContainer }, + data() { + return { + activeTab: "pending", + showCancelRequest: false, + fieldsToUpdate: [], + jsonData: "", + selectedData: "", + monacoLargeOptions: { + automaticLayout: true, + }, + showJSONEditor: false, + data, + requestId, + request, + files, + refreshTasks: 0, + canCancel, + canViewPrint, + status: "ACTIVE", + userRequested: [], + errorLogs, + disabled: false, + retryDisabled: false, + packages: [], + processId, + canViewComments, + isObjectLoading: false, + showTree: false, + showTabs: true, + showInfo: true, + tabDefault: "details", + tabs: [ + { + name: "Details", href: "#details", current: "details", show: true, content: null, + }, + { + name: "Comments", href: "#comments", current: "comments", show: true, content: null, + }, + ], + headerModel: false, + }; + }, + computed: { + activeErrors() { + return this.request.status === "ERROR"; + }, + activePending() { + return this.request.status === "ACTIVE"; + }, + /** + * Get the list of participants in the request. + * + */ + participants() { + return this.request.participants; + }, + /** + * Request Summary - that is blank place holder if there are in progress tasks, + * if the request is completed it will show key value pairs. + * + */ + showSummary() { + return this.request.status === "ACTIVE" || this.request.status === "COMPLETED" || this.request.status === "CANCELED"; + }, + /** + * Show tasks if request status is not completed or pending + * + */ + showTasks() { + return this.request.status !== "COMPLETED" && this.request.status !== "PENDING"; + }, + /** + * If the screen summary is configured. + */ + showScreenSummary() { + return this.request.summary_screen !== null; + }, + /** + * Get the summary of the Request. + * + */ + summary() { + return this.request.summary; + }, + /** + * Get Screen summary + * */ + screenSummary() { + return this.request.summary_screen; + }, + /** + * prepare data screen + */ + dataSummary() { + const options = {}; + this.request.summary.forEach((option) => { + if (option.type === "datetime") { + options[option.key] = moment(option.value) + .tz(window.ProcessMaker.user.timezone) + .format("MM/DD/YYYY HH:mm"); + } else if (option.type === "date") { + options[option.key] = moment(option.value) + .tz(window.ProcessMaker.user.timezone) + .format("MM/DD/YYYY"); + } else { + options[option.key] = option.value; + } + }); + return options; + }, + /** + * If the screen request detail is configured. + */ + showScreenRequestDetail() { + return !!this.request.request_detail_screen; + }, + /** + * Get Screen request detail + */ + screenRequestDetail() { + return this.request.request_detail_screen ? this.request.request_detail_screen.config : null; + }, + classStatusCard() { + const header = { + ACTIVE: "active-style", + COMPLETED: "active-style", + CANCELED: "canceled-style ", + ERROR: "canceled-style", + }; + return `card-header text-status ${header[this.request.status.toUpperCase()]}`; + }, + labelDate() { + const label = { + ACTIVE: "In Progress Since", + COMPLETED: "Completed On", + CANCELED: "Canceled ", + ERROR: "Failed On", + }; + return label[this.request.status.toUpperCase()]; + }, + statusDate() { + const status = { + ACTIVE: this.request.created_at, + COMPLETED: this.request.completed_at, + CANCELED: this.request.updated_at, + ERROR: this.request.updated_at, + }; + + return status[this.request.status.toUpperCase()]; + }, + statusLabel() { + const status = { + ACTIVE: this.$t("In Progress"), + COMPLETED: this.$t("Completed"), + CANCELED: this.$t("Canceled"), + ERROR: this.$t("Error"), + }; + + return status[this.request.status.toUpperCase()]; + }, + requestBy() { + return [this.request.user]; + }, + panCommentInVueOptionsComponents() { + return "pan-comment" in Vue.options.components; + }, + }, + mounted() { + this.packages = window.ProcessMaker.requestShowPackages; + this.listenRequestUpdates(); + this.cleanScreenButtons(); + this.editJsonData(); + }, + methods: { + switchTab(tab) { + this.activeTab = tab; + if (tab === "overview") { + this.isObjectLoading = true; + } + ProcessMaker.EventBus.$emit("tab-switched", tab); + }, + switchTabInfo(tab) { + this.showInfo = !this.showInfo; + if (window.Intercom) { + window.Intercom("update", { hide_default_launcher: tab === "comments" }); + } + }, + onLoadedObject() { + this.isObjectLoading = false; + }, + requestStatusClass(status) { + const bubbleColor = { + active: "text-success", + inactive: "text-danger", + error: "text-danger", + draft: "text-warning", + archived: "text-info", + completed: "text-primary", + }; + return `fas fa-circle ${bubbleColor[status.toLowerCase()]} small`; + }, + // Data editor + updateRequestData() { + const data = JSON.parse(this.jsonData); + ProcessMaker.apiClient.put(`requests/${this.requestId}`, { + data, + }).then(() => { + this.fieldsToUpdate.splice(0); + ProcessMaker.alert(this.$t("The request data was saved."), "success"); + }); + }, + saveJsonData() { + try { + const value = JSON.parse(this.jsonData); + this.updateRequestData(); + } catch (e) { + // Invalid data + } + }, + editJsonData() { + this.jsonData = JSON.stringify(this.data, null, 4); + }, + /** + * Refresh the Request details. + * + */ + refreshRequest() { + this.$refs.pending.fetch(); + this.$refs.completed.fetch(); + ProcessMaker.apiClient.get(`requests/${this.requestId}`, { + params: { + include: "participants,user,summary,summaryScreen", + }, + }).then((response) => { + for (const attribute in response.data) { + this.updateModel(this.request, attribute, response.data[attribute]); + } + this.refreshTasks++; + }); + }, + /** + * Update a model property. + * + */ + updateModel(obj, prop, value, defaultValue) { + const descriptor = Object.getOwnPropertyDescriptor(obj, prop); + value = value !== undefined ? value : (descriptor ? obj[prop] : defaultValue); + if (descriptor && !(descriptor.get instanceof Function)) { + delete obj[prop]; + Vue.set(obj, prop, value); + } else if (descriptor && obj[prop] !== value) { + Vue.set(obj, prop, value); + } + }, + /** + * Listen for Request updates. + * + */ + listenRequestUpdates() { + const userId = document.head.querySelector("meta[name=\"user-id\"]").content; + Echo.private(`ProcessMaker.Models.User.${userId}`).notification((token) => { + if (token.request_id === this.requestId) { + this.refreshRequest(); + } + }); + }, + /** + * disable buttons in screen + */ + cleanScreenButtons() { + if (this.showScreenSummary) { + this.$refs.screen.config[0].items.forEach((item) => { + item.config.disabled = true; + if (item.component === "FormButton") { + item.config.event = ""; + item.config.variant = `${item.config.variant} disabled`; + } + }); + } + }, + okCancel() { + // single click + if (this.disabled) { + return; + } + this.disabled = true; + ProcessMaker.apiClient.put(`requests/${this.requestId}`, { + status: "CANCELED", + }).then(() => { + ProcessMaker.alert(this.$t("The request was canceled."), "success"); + window.location.reload(); + }).catch(() => { + this.disabled = false; + }); + }, + onCancel() { + ProcessMaker.confirmModal( + this.$t("Caution!"), + this.$t("Are you sure you want cancel this request?"), + "", + () => { + this.okCancel(); + }, + ); + }, + completeRequest() { + ProcessMaker.confirmModal( + this.$t("Caution!"), + this.$t("Are you sure you want to complete this request?"), + "", + () => { + ProcessMaker.apiClient.put(`requests/${this.requestId}`, { + status: "COMPLETED", + }).then(() => { + ProcessMaker.alert(this.$t("Request Completed"), "success"); + window.location.reload(); + }); + }, + ); + }, + retryRequest() { + const apiRequest = () => { + this.retryDisabled = true; + let success = true; + + ProcessMaker.apiClient.put(`requests/${this.requestId}/retry`).then((response) => { + if (response.status !== 200) { + return; + } + + const { message } = response.data; + success = response.data.success || false; + + if (success) { + if (Array.isArray(message)) { + for (const line of message) { + ProcessMaker.alert(this.$t(line), "success"); + } + } + } else { + ProcessMaker.alert(this.$t("Request could not be retried"), "danger"); + } + }).finally(() => setTimeout(() => window.location.reload(), success ? 3000 : 1000)); + }; + + ProcessMaker.confirmModal( + this.$t("Confirm"), + this.$t("Are you sure you want to retry this request?"), + "default", + apiRequest, + ); + }, + rollback(errorTaskId, rollbackToName) { + ProcessMaker.confirmModal( + this.$t("Confirm"), + this.$t( + "Are you sure you want to rollback to the task @{{name}}? Warning! This request will continue as the current published process version.", + { name: rollbackToName }, + ), + "default", + () => { + ProcessMaker.apiClient.post(`tasks/${errorTaskId}/rollback`).then((response) => { + window.location.reload(); + }); + }, + ); + }, + }, +}); diff --git a/resources/jscomposition/cases/casesDetail/variables/index.js b/resources/jscomposition/cases/casesDetail/variables/index.js new file mode 100644 index 0000000000..70f1e26039 --- /dev/null +++ b/resources/jscomposition/cases/casesDetail/variables/index.js @@ -0,0 +1,11 @@ +export default {}; + +export const getRequestId = () => requestId; + +export const getRequestStatus = () => request.status; + +export const getComentableType = () => comentable_type; + +export const getProcessName = () => request.process.name; + +export const api = window.ProcessMaker?.apiClient; diff --git a/resources/jscomposition/cases/casesMain/App.vue b/resources/jscomposition/cases/casesMain/App.vue new file mode 100644 index 0000000000..6f6b358b21 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/App.vue @@ -0,0 +1,18 @@ + + diff --git a/resources/jscomposition/cases/casesMain/CasesDataSection.vue b/resources/jscomposition/cases/casesMain/CasesDataSection.vue new file mode 100644 index 0000000000..709b3250a8 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/CasesDataSection.vue @@ -0,0 +1,125 @@ + + diff --git a/resources/jscomposition/cases/casesMain/CasesMain.vue b/resources/jscomposition/cases/casesMain/CasesMain.vue new file mode 100644 index 0000000000..acfa1a808e --- /dev/null +++ b/resources/jscomposition/cases/casesMain/CasesMain.vue @@ -0,0 +1,88 @@ + + diff --git a/resources/jscomposition/cases/casesMain/api/index.js b/resources/jscomposition/cases/casesMain/api/index.js new file mode 100644 index 0000000000..909283bd76 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/api/index.js @@ -0,0 +1,563 @@ +import { api } from "../variables"; + +export default {}; + +// Method to get data case list - change with processmaker API +export const getData = async () => { + const objects_list = []; + + for (let i = 0; i <= 31; i++) { + const obj = { + caseNumber: `${i}`, + caseTitle: `Case Title ${i}`, + process: `Process ${i}`, + task: `Task ${i}`, + participants: `Avatar ${i}`, + status: `badge ${i}`, + started: `21/21/${i}`, + completed: `22/22/${i}`, + }; + + objects_list.push(obj); + } + + return objects_list; +}; + +export const allCasesData = () => ({ + data: [ + { + case_number: 0, + user_id: 3, + case_title: "aut enim natus", + case_title_formatted: "ratione repellat rerum", + case_status: "COMPLETED", + processes: [ + { id: 5, name: "eligendi ut" }, + { id: 29869, name: "dolorem qui" }, + { id: 3, name: "accusantium consectetur" }, + ], + requests: [ + { + id: 92570, + name: "delectus voluptatem", + parent_request: 24, + }, + { id: 8846, name: "est accusamus culpa", parent_request: 2 }, + ], + request_tokens: null, + tasks: [ + { id: "123", name: "libero tenetur quos quibusdam" }, + { id: "node_2329", name: "modi voluptas quo" }, + { id: "node_4561", name: "asperiores tenetur" }, + ], + participants: [{ id: 25, name: "Dr. Madie Predovic PhD" }], + initiated_at: "1997-08-01T19:59:17.000000Z", + completed_at: "2015-11-10T21:25:11.000000Z", + }, + { + case_number: 1, + user_id: 3, + case_title: "et tempora omnis", + case_title_formatted: "odio reprehenderit eum", + case_status: "IN_PROGRESS", + processes: [ + { id: 39969254, name: "aut itaque" }, + { id: 316545, name: "voluptatem optio" }, + { id: 540006393, name: "et dolor" }, + ], + requests: [ + { id: 689, name: "voluptas aut", parent_request: 185512574 }, + { + id: 3262060, + name: "adipisci est qui", + parent_request: 93397820, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_8166", name: "asperiores qui qui molestias" }, + { id: "node_4102", name: "non repudiandae aut" }, + { id: "node_4351", name: "ducimus facilis" }, + ], + participants: [ + { id: 122025, name: "Jaylin Heaney" }, + { id: 6, name: "Jessika Heller" }, + { id: 748506844, name: "Jillian Gibson" }, + ], + initiated_at: "1980-12-28T08:22:58.000000Z", + completed_at: "2012-07-05T12:27:04.000000Z", + }, + { + case_number: 2, + user_id: 3, + case_title: "qui cum amet", + case_title_formatted: "ad ab modi", + case_status: "COMPLETED", + processes: [ + { id: 6624, name: "velit voluptatibus" }, + { id: 34, name: "consequatur quis" }, + { id: 527764, name: "sint dolores" }, + ], + requests: [ + { id: 57, name: "impedit ducimus", parent_request: 476 }, + { id: 4, name: "qui dolorum non", parent_request: 688 }, + ], + request_tokens: null, + tasks: [ + { id: "node_1050", name: "maiores vel iste a" }, + { id: "node_7764", name: "vel nesciunt ratione" }, + { id: "node_4394", name: "molestiae qui" }, + ], + participants: [ + { id: 8, name: "Kristofer Crist" }, + { id: 163202910, name: "Leonard Bergnaum DDS" }, + { id: 444, name: "Mr. Arvid Schiller MD" }, + ], + initiated_at: "1983-05-19T16:24:24.000000Z", + completed_at: "2019-09-07T09:29:56.000000Z", + }, + { + case_number: 3, + user_id: 3, + case_title: "voluptatem quidem quia", + case_title_formatted: "qui officiis sapiente", + case_status: "COMPLETED", + processes: [ + { id: 92, name: "repellendus voluptatibus" }, + { id: 880611150, name: "architecto est" }, + { id: 9284, name: "eligendi veniam" }, + ], + requests: [ + { id: 3, name: "et aliquid", parent_request: 34377361 }, + { + id: 400403, + name: "consequatur vel magni", + parent_request: 81981614, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_1259", name: "et temporibus totam quia" }, + { id: "node_6096", name: "perferendis animi sapiente" }, + { id: "node_3455", name: "ut occaecati" }, + ], + participants: [ + { id: 448166, name: "Eldon Cartwright DVM" }, + { id: 62433751, name: "Miss Ofelia Grimes" }, + { id: 96612, name: "Dr. Wilburn Treutel" }, + ], + initiated_at: "2006-05-21T07:20:51.000000Z", + completed_at: "1982-03-29T07:36:25.000000Z", + }, + { + case_number: 4, + user_id: 1, + case_title: "sit ut sit", + case_title_formatted: "voluptatem deleniti commodi", + case_status: "IN_PROGRESS", + processes: [ + { id: 767, name: "cupiditate in" }, + { id: 38716, name: "recusandae sequi" }, + { id: 5101818, name: "ut atque" }, + ], + requests: [ + { id: 845, name: "est voluptates", parent_request: 991 }, + { + id: 6922, + name: "repellat deserunt vitae", + parent_request: 7, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_9443", name: "et dignissimos quibusdam esse" }, + { id: "node_3803", name: "fuga voluptatem ratione" }, + { id: "node_6273", name: "non omnis" }, + ], + participants: [ + { id: 2, name: "Allene Purdy" }, + { id: 13471022, name: "Rhiannon Beer DDS" }, + { id: 493335, name: "Emma Lemke PhD" }, + ], + initiated_at: "1989-01-02T20:52:49.000000Z", + completed_at: "1990-07-04T19:11:55.000000Z", + }, + { + case_number: 5, + user_id: 1, + case_title: "est est ad", + case_title_formatted: "dicta vel molestiae", + case_status: "IN_PROGRESS", + processes: [ + { id: 69224450, name: "vero ea" }, + { id: 2477, name: "excepturi voluptatem" }, + { id: 11857, name: "aut occaecati" }, + ], + requests: [ + { + id: 95, + name: "molestias voluptatem", + parent_request: 6682658, + }, + { id: 9706673, name: "et quasi ipsum", parent_request: 2 }, + ], + request_tokens: null, + tasks: [ + { id: "node_1023", name: "quia a molestiae labore" }, + { id: "node_8478", name: "assumenda omnis quis" }, + { id: "node_1863", name: "repellendus saepe" }, + ], + participants: [ + { id: 5762571, name: "Prof. Chad Ledner" }, + { id: 9550, name: "Leta Wunsch Jr." }, + { id: 6589672, name: "Mr. Raul Turcotte" }, + ], + initiated_at: "2018-07-11T00:44:04.000000Z", + completed_at: "1977-04-21T20:33:50.000000Z", + }, + { + case_number: 6, + user_id: 1, + case_title: "ullam error doloribus", + case_title_formatted: "officiis molestiae ut", + case_status: "IN_PROGRESS", + processes: [ + { id: 5181637, name: "delectus dolores" }, + { id: 58250809, name: "fuga beatae" }, + { id: 93813951, name: "atque neque" }, + ], + requests: [ + { + id: 6694520, + name: "nihil aperiam", + parent_request: 525456, + }, + { + id: 9067565, + name: "beatae voluptatem dolorem", + parent_request: 0, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_8227", name: "totam rerum aut ipsum" }, + { id: "node_5132", name: "culpa nisi deleniti" }, + { id: "node_7065", name: "exercitationem minus" }, + ], + participants: [ + { id: 37343205, name: "Abbey Fay" }, + { id: 9459, name: "Dr. Ricardo Bernier" }, + { id: 790987, name: "Baylee Brekke" }, + ], + initiated_at: "2022-12-11T20:48:55.000000Z", + completed_at: "2021-11-07T18:15:58.000000Z", + }, + { + case_number: 7, + user_id: 3, + case_title: "eaque amet repellendus", + case_title_formatted: "et debitis sit", + case_status: "IN_PROGRESS", + processes: [ + { id: 328969, name: "facere sit" }, + { id: 30, name: "aut omnis" }, + { id: 261429681, name: "rerum est" }, + ], + requests: [ + { id: 5157906, name: "voluptate ratione", parent_request: 0 }, + { + id: 60923, + name: "voluptatem eius ipsa", + parent_request: 27, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_8511", name: "cumque exercitationem quia sit" }, + { id: "node_4341", name: "et qui aliquid" }, + { id: "node_8437", name: "rerum exercitationem" }, + ], + participants: [ + { id: 7556, name: "Bonnie Altenwerth" }, + { id: 442, name: "Prof. Nathanael Vandervort" }, + { id: 23533498, name: "Kirk Pfeffer" }, + ], + initiated_at: "1974-10-13T02:35:47.000000Z", + completed_at: "1976-05-06T00:54:14.000000Z", + }, + { + case_number: 8, + user_id: 3, + case_title: "reiciendis optio dicta", + case_title_formatted: "voluptas omnis culpa", + case_status: "IN_PROGRESS", + processes: [ + { id: 44327628, name: "quod illum" }, + { id: 5080232, name: "in omnis" }, + { id: 54, name: "veritatis qui" }, + ], + requests: [ + { + id: 4444, + name: "corrupti adipisci", + parent_request: 2310454, + }, + { + id: 562211360, + name: "voluptatibus id omnis", + parent_request: 4, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_8141", name: "fugiat tenetur nihil pariatur" }, + { id: "node_1343", name: "omnis labore illo" }, + { id: "node_0356", name: "aut aspernatur" }, + ], + participants: [ + { id: 2, name: "Luella Gislason" }, + { id: 47785406, name: "Miss Janae Turner" }, + { id: 96768614, name: "Amya Larson" }, + ], + initiated_at: "1978-01-13T20:53:00.000000Z", + completed_at: "2003-05-09T01:51:49.000000Z", + }, + { + case_number: 9, + user_id: 1, + case_title: "sint eius corporis", + case_title_formatted: "eaque ea quas", + case_status: "COMPLETED", + processes: [ + { id: 219515988, name: "consequatur maiores" }, + { id: 174836, name: "soluta sed" }, + { id: 9, name: "hic architecto" }, + ], + requests: [ + { id: 3084, name: "tempore incidunt", parent_request: 30470 }, + { + id: 2, + name: "neque aut suscipit", + parent_request: 81171529, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_7893", name: "quos ipsam odit quia" }, + { id: "node_0942", name: "et voluptatem perferendis" }, + { id: "node_3020", name: "voluptas recusandae" }, + ], + participants: [ + { id: 8, name: "Kira Buckridge" }, + { id: 34, name: "Briana Rath" }, + { id: 3526, name: "Mr. Franco Veum" }, + ], + initiated_at: "1988-02-11T06:46:46.000000Z", + completed_at: "1997-11-09T18:06:35.000000Z", + }, + { + case_number: 11, + user_id: 3, + case_title: "facere accusantium sequi", + case_title_formatted: "inventore et sequi", + case_status: "IN_PROGRESS", + processes: [ + { id: 94422, name: "sapiente culpa" }, + { id: 4, name: "vero dolorum" }, + { id: 294713669, name: "sit dolor" }, + ], + requests: [ + { + id: 898, + name: "voluptatum perferendis", + parent_request: 116178, + }, + { + id: 96313178, + name: "dolor quis ad", + parent_request: 480366, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_3665", name: "eligendi explicabo suscipit sed" }, + { id: "node_7456", name: "eaque pariatur consectetur" }, + { id: "node_8114", name: "dignissimos occaecati" }, + ], + participants: [ + { id: 52560, name: "Turner Schuppe" }, + { id: 16577119, name: "Chasity Reinger" }, + { id: 116, name: "Victor Padberg" }, + ], + initiated_at: "1995-05-26T12:28:06.000000Z", + completed_at: "1995-12-13T11:27:23.000000Z", + }, + { + case_number: 12, + user_id: 1, + case_title: "quasi perspiciatis ut", + case_title_formatted: "perferendis non ut", + case_status: "IN_PROGRESS", + processes: [ + { id: 284983263, name: "dolorem soluta" }, + { id: 95252498, name: "quos aut" }, + { id: 1115, name: "sed saepe" }, + ], + requests: [ + { id: 17469, name: "tenetur temporibus", parent_request: 3 }, + { + id: 94439, + name: "odio accusantium sed", + parent_request: 189566, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_6891", name: "unde ratione ab quia" }, + { id: "node_2660", name: "tenetur odio sed" }, + { id: "node_5814", name: "ut unde" }, + ], + participants: [ + { id: 44, name: "Kathlyn Johns IV" }, + { id: 447100, name: "Mr. Jamie Yundt" }, + { id: 6992588, name: "Cade McCullough" }, + ], + initiated_at: "1994-05-27T02:03:26.000000Z", + completed_at: "2021-01-29T20:34:17.000000Z", + }, + { + case_number: 13, + user_id: 1, + case_title: "recusandae quas provident", + case_title_formatted: "placeat veniam fugiat", + case_status: "IN_PROGRESS", + processes: [ + { id: 16, name: "ad ratione" }, + { id: 605, name: "molestiae libero" }, + { id: 57, name: "quia aspernatur" }, + ], + requests: [ + { id: 4536, name: "sit quia", parent_request: 78 }, + { id: 7982002, name: "ea ut itaque", parent_request: 7 }, + ], + request_tokens: null, + tasks: [ + { id: "node_1228", name: "error vero exercitationem in" }, + { id: "node_1423", name: "et quia voluptas" }, + { id: "node_8448", name: "consequatur ipsa" }, + ], + participants: [ + { id: 629, name: "Mrs. Vivianne Kling Sr." }, + { id: 170285, name: "Anthony Reichert" }, + { id: 845, name: "Miss Cayla Hyatt DVM" }, + ], + initiated_at: "1994-01-07T14:19:05.000000Z", + completed_at: "1976-07-19T06:02:22.000000Z", + }, + { + case_number: 14, + user_id: 1, + case_title: "dolore expedita possimus", + case_title_formatted: "quia consequuntur blanditiis", + case_status: "COMPLETED", + processes: [ + { id: 96, name: "deleniti nam" }, + { id: 79, name: "totam aut" }, + { id: 142841245, name: "commodi quod" }, + ], + requests: [ + { id: 346, name: "eum consequatur", parent_request: 78984 }, + { + id: 353990, + name: "aut dolorem deleniti", + parent_request: 6272, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_0731", name: "quia excepturi ea aspernatur" }, + { id: "node_4862", name: "iure fugit sed" }, + { id: "node_6267", name: "repellendus fugiat" }, + ], + participants: [ + { id: 76277, name: "Madonna Purdy" }, + { id: 28625426, name: "Mr. Timmothy Ankunding MD" }, + { id: 377993949, name: "Johann Stoltenberg" }, + ], + initiated_at: "1972-11-14T13:12:41.000000Z", + completed_at: "1989-03-29T06:06:42.000000Z", + }, + { + case_number: 15, + user_id: 3, + case_title: "adipisci quisquam nulla", + case_title_formatted: "velit facere nihil", + case_status: "IN_PROGRESS", + processes: [ + { id: 770828, name: "recusandae saepe" }, + { id: 5361354, name: "quia voluptatem" }, + { id: 47187, name: "earum molestiae" }, + ], + requests: [ + { id: 4160, name: "voluptas qui", parent_request: 50115 }, + { + id: 3757, + name: "sunt delectus perspiciatis", + parent_request: 942749, + }, + ], + request_tokens: null, + tasks: [ + { id: "node_7354", name: "commodi sint aliquid et" }, + { id: "node_0561", name: "quod similique eum" }, + { id: "node_1082", name: "soluta ut" }, + ], + participants: [ + { id: 53865127, name: "Marcellus Bailey" }, + { id: 4, name: "Adrianna Leannon" }, + { id: 86, name: "Dr. Luis Miller Jr." }, + ], + initiated_at: "1980-10-10T09:54:40.000000Z", + completed_at: "1990-08-24T08:24:20.000000Z", + }, + ], + meta: { + total: 1000, perPage: 15, currentPage: 1, lastPage: 67, + }, +}); + +export const getAllData = async ({ type, page, perPage }) => { + const response = []; + const allData = allCasesData(); + + for (let index = 0; index < perPage; index += 1) { + const idxLooper = index % (allData.data.length - 1); + + const item = allData.data[idxLooper]; + item.case_number = index + 100 * page; + item.case_title = `${type} ${page} ${item.case_title}`; + item.case_title_formatted = `${type} ${page} ${item.case_title_formatted}`; + response.push(item); + } + + return response; +}; + +const services = { + completed: "get_completed", + in_progress: "get_in_progress", + all: "get_all_cases", +}; + +export const getCaseData = async (service, data) => { + const response = await api.get(`/api/1.1/cases/${services[service] || "get_all_cases"}`, data); + + return response.data; +}; + +export const getCounters = async (data) => { + const response = await api.get("/api/1.1/cases/get_my_cases_counters", data); + + return response.data; +}; diff --git a/resources/jscomposition/cases/casesMain/components/AppCounters.vue b/resources/jscomposition/cases/casesMain/components/AppCounters.vue new file mode 100644 index 0000000000..203bede000 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/components/AppCounters.vue @@ -0,0 +1,63 @@ + + + diff --git a/resources/jscomposition/cases/casesMain/components/AvatarContainer.vue b/resources/jscomposition/cases/casesMain/components/AvatarContainer.vue new file mode 100644 index 0000000000..03a1b14a30 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/components/AvatarContainer.vue @@ -0,0 +1,30 @@ + + diff --git a/resources/jscomposition/cases/casesMain/components/BadgesSection.vue b/resources/jscomposition/cases/casesMain/components/BadgesSection.vue new file mode 100644 index 0000000000..ce6c5cafda --- /dev/null +++ b/resources/jscomposition/cases/casesMain/components/BadgesSection.vue @@ -0,0 +1,50 @@ + + + diff --git a/resources/jscomposition/cases/casesMain/components/CaseFilter.vue b/resources/jscomposition/cases/casesMain/components/CaseFilter.vue new file mode 100644 index 0000000000..5f348dc5cf --- /dev/null +++ b/resources/jscomposition/cases/casesMain/components/CaseFilter.vue @@ -0,0 +1,44 @@ + + diff --git a/resources/jscomposition/cases/casesMain/config/columns.js b/resources/jscomposition/cases/casesMain/config/columns.js new file mode 100644 index 0000000000..336e22ff2f --- /dev/null +++ b/resources/jscomposition/cases/casesMain/config/columns.js @@ -0,0 +1,199 @@ +import moment from "moment"; + +import { + CaseTitleCell, + TruncatedOptionsCell, + ParticipantsCell, + StatusCell, + LinkCell, +} from "../../../system/index"; +import { formatDate } from "../utils"; + +export default {}; +/** + * Example Column + * field: String + * header: String + * headerFormatter: callback + * resizable: Boolean + * visible: Callback + * formatter: Callback - Build the value in the cell + * width: Number + * cellRenderer: Object Vue to custom the cell + * filter: This attribute is optional + */ + +// My cases: [Case#, Case Title, Process, Task, Participants, Status, Started, Completed] +// In Progress : [ Case#, Case Title, Process, Task, Participants, Status, Started] +// Completed : [Case#, Case Title, Process, Task, Participants, Status, Started, Completed] +// AllCases : [Case#, Case Title, Process, Task, Participants, Status, Started, Completed] +// AllRequest : [Case#, Case Title, Process, Task, Participants, Status, Started, Completed] + +export const caseNumberColumn = () => ({ + field: "case_number", + header: "Case #", + resizable: true, + width: 100, + filter: { + dataType: "string", + operators: ["=", ">", ">=", "in", "between"], + }, + cellRenderer: () => ({ + component: LinkCell, + params: { + click: (row, column, columns) => { + window.document.location = `/cases/${row.case_number}`; + }, + }, + }), +}); + +export const caseTitleColumn = () => ({ + field: "case_title", + header: "Case Title", + resizable: true, + width: 200, + cellRenderer: () => ({ + component: CaseTitleCell, + params: { + click: (row, column, columns) => { + window.document.location = `/cases/${row.case_number}`; + }, + }, + }), + filter: { + dataType: "string", + operators: ["=", ">", ">=", "in", "between"], + }, +}); + +export const processColumn = () => ({ + field: "processes", + header: "Process", + resizable: true, + width: 200, + cellRenderer: () => ({ + component: TruncatedOptionsCell, + params: { + click: (option, row, column, columns) => { + window.document.location = `/tasks/${option.id}/edit`; + }, + formatterOptions: (option, row, column, columns) => option.name, + }, + }), +}); + +export const taskColumn = () => ({ + field: "tasks", + header: "Task", + resizable: true, + width: 200, + cellRenderer: () => ({ + component: TruncatedOptionsCell, + params: { + click: (option, row, column, columns) => { + window.document.location = `/tasks/${option.id}/edit`; + }, + formatterOptions: (option, row, column, columns) => option.name, + }, + }), +}); + +export const participantsColumn = () => ({ + field: "participants", + header: "Participants", + resizable: true, + width: 200, + cellRenderer: () => ({ + component: ParticipantsCell, + params: { + click: (option, row, column, columns) => { + window.document.location = `/profile/${option.id}`; + }, + }, + }), +}); + +export const statusColumn = () => ({ + field: "case_status", + header: "Status", + resizable: true, + width: 200, + cellRenderer: () => ({ + component: StatusCell, + }), + filter: { + dataType: "string", + operators: ["="], + }, +}); + +export const startedColumn = () => ({ + field: "initiated_at", + header: "Started", + resizable: true, + width: 200, + formatter: (row, column, columns) => formatDate(row.initiated_at, "datetime"), + filter: { + dataType: "datetime", + operators: ["between", ">", ">=", "<", "<="], + }, +}); + +export const completedColumn = () => ({ + field: "completed_at", + header: "Completed", + resizable: true, + width: 200, + formatter: (row, column, columns) => formatDate(row.completed_at, "datetime"), + filter: { + dataType: "datetime", + operators: ["between", ">", ">=", "<", "<="], + }, +}); + +export const getColumns = (type) => { + const columnsDefinition = { + default: [ + caseNumberColumn(), + caseTitleColumn(), + processColumn(), + taskColumn(), + participantsColumn(), + statusColumn(), + startedColumn(), + completedColumn(), + ], + in_progress: [ + caseNumberColumn(), + caseTitleColumn(), + processColumn(), + taskColumn(), + participantsColumn(), + statusColumn(), + startedColumn(), + ], + completed: [ + caseNumberColumn(), + caseTitleColumn(), + processColumn(), + taskColumn(), + participantsColumn(), + statusColumn(), + startedColumn(), + completedColumn(), + ], + all: [ + caseNumberColumn(), + caseTitleColumn(), + processColumn(), + taskColumn(), + participantsColumn(), + statusColumn(), + startedColumn(), + completedColumn(), + ], + }; + + return columnsDefinition[type] || columnsDefinition.default; +}; diff --git a/resources/jscomposition/cases/casesMain/config/index.js b/resources/jscomposition/cases/casesMain/config/index.js new file mode 100644 index 0000000000..963420fb99 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/config/index.js @@ -0,0 +1,2 @@ +export * from "./columns"; +export * from "./badges"; diff --git a/resources/jscomposition/cases/casesMain/main.js b/resources/jscomposition/cases/casesMain/main.js new file mode 100644 index 0000000000..97c89abc83 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/main.js @@ -0,0 +1,19 @@ +import App from "./App.vue"; +import { routes } from "./routes"; + +Vue.use(VueRouter); + +const router = new VueRouter({ + mode: "history", + base: "/", + routes, +}); + +new Vue({ + el: "#cases-main", + router, + components: { + App, + }, + render: (h) => h(App), +}); diff --git a/resources/jscomposition/cases/casesMain/routes.js b/resources/jscomposition/cases/casesMain/routes.js new file mode 100644 index 0000000000..cea03cba52 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/routes.js @@ -0,0 +1,27 @@ +import CasesMain from "./CasesMain.vue"; +import CasesDataSection from "./CasesDataSection.vue"; + +export default {}; + +export const routes = [ + { + name: "cases", + path: "/cases", + component: CasesMain, + props(route) { + return {}; + }, + children: [ + { + name: "cases-request", + path: ":id?", + component: CasesDataSection, + props(route) { + return { + listId: route.params?.id || "", + }; + }, + }, + ], + }, +]; diff --git a/resources/jscomposition/cases/casesMain/utils/counters.js b/resources/jscomposition/cases/casesMain/utils/counters.js new file mode 100644 index 0000000000..ac017b1e6c --- /dev/null +++ b/resources/jscomposition/cases/casesMain/utils/counters.js @@ -0,0 +1,53 @@ +import { t } from "i18next"; + +export default {}; + +export const formatCounters = (data) => { + const counters = [ + { + header: t("My cases"), + body: data.totalMyCases.toString(), + color: "amber", + icon: "far fa-user", + url: "/cases", + }, + { + header: t("In progress"), + body: data.totalInProgress.toString(), + color: "green", + icon: "fas fa-list", + url: "/cases/in_progress", + }, + { + header: t("Completed"), + body: data.totalCompleted.toString(), + color: "blue", + icon: "far fa-check-circle", + url: "/cases/completed", + }, + ]; + + if (data.totalAllCases) { + counters.push({ + header: t("All cases"), + body: data.totalAllCases.toString(), + color: "purple", + icon: "far fa-clipboard", + url: "/cases/all", + }); + } + + if (data.totalMyRequest) { + counters.push({ + header: t("My requests"), + body: data.totalMyRequest.toString(), + color: "gray", + icon: "fas fa-play", + url: () => { + window.location.href = "/requests"; + }, + }); + } + + return counters; +}; diff --git a/resources/jscomposition/cases/casesMain/utils/date.js b/resources/jscomposition/cases/casesMain/utils/date.js new file mode 100644 index 0000000000..a65b805672 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/utils/date.js @@ -0,0 +1,27 @@ +import moment from "moment"; + +export const formatDate = (value, format) => { + let config = "DD/MM/YYYY hh:mm"; + if ( + typeof ProcessMaker !== "undefined" + && ProcessMaker.user + && ProcessMaker.user.datetime_format + ) { + if (format === "datetime") { + config = ProcessMaker.user.datetime_format; + } + if (format === "date") { + config = ProcessMaker.user.datetime_format.replace( + /[\sHh:msaAzZ]/g, + "", + ); + } + } + if (value) { + if (moment(value).isValid()) { + return moment(value).format(config); + } + return value; + } + return "-"; +}; diff --git a/resources/jscomposition/cases/casesMain/utils/filters.js b/resources/jscomposition/cases/casesMain/utils/filters.js new file mode 100644 index 0000000000..7ceec1fc92 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/utils/filters.js @@ -0,0 +1,56 @@ +import { formatDate } from "./date"; + +export const formatFilters = (filters) => { + const response = filters.map((e) => { + let { value } = e; + + if (!e.operator) { + return null; + } + + if (e.operator === "between" || e.operator === "in") { + value = e.value.map((o) => o.value); + } + + return { + subject: { + type: "Field", + value: e.field, + }, + operator: e.operator, + value, + }; + }); + + return response.filter((e) => e); +}; + +// Format filters to convert in badges data +export const formatFilterBadges = (filters, columns) => { + const response = filters.map((e) => { + let { value } = e; + const col = columns.find((c) => c.field === e.field); + + if (!e.operator) { + return null; + } + + if (e.operator === "between" || e.operator === "in") { + value = e.value.map((o) => o.value); + } + + // Format datetime value to badges + if (col.filter.dataType === "datetime") { + value = value.find ? value.map((o) => formatDate(o)) : formatDate(value); + } + + return { + fieldName: col.header, + field: e.field, + operator: e.operator, + value, + }; + }); + + return response.filter((e) => e); +}; diff --git a/resources/jscomposition/cases/casesMain/utils/index.js b/resources/jscomposition/cases/casesMain/utils/index.js new file mode 100644 index 0000000000..9112291da1 --- /dev/null +++ b/resources/jscomposition/cases/casesMain/utils/index.js @@ -0,0 +1,3 @@ +export * from "./counters"; +export * from "./filters"; +export * from "./date"; diff --git a/resources/jscomposition/cases/casesMain/variables/index.js b/resources/jscomposition/cases/casesMain/variables/index.js new file mode 100644 index 0000000000..7e195e678a --- /dev/null +++ b/resources/jscomposition/cases/casesMain/variables/index.js @@ -0,0 +1,5 @@ +export default {}; + +export const api = window.ProcessMaker?.apiClient; + +export const user = currentUser; diff --git a/resources/jscomposition/config/configBreadcrum.js b/resources/jscomposition/config/configBreadcrum.js new file mode 100644 index 0000000000..7cce51641b --- /dev/null +++ b/resources/jscomposition/config/configBreadcrum.js @@ -0,0 +1,9 @@ +export const configHomeBreadcrum = () => ({ + name: "", + icon: "fas fa-home", + href: "/", + current: false, + first: true, +}); + +export default {}; diff --git a/resources/jscomposition/config/index.js b/resources/jscomposition/config/index.js new file mode 100644 index 0000000000..e173e903ac --- /dev/null +++ b/resources/jscomposition/config/index.js @@ -0,0 +1 @@ +export * from "./configBreadcrum"; diff --git a/resources/jscomposition/system/Breadcrums.vue b/resources/jscomposition/system/Breadcrums.vue new file mode 100644 index 0000000000..9ee3d8277e --- /dev/null +++ b/resources/jscomposition/system/Breadcrums.vue @@ -0,0 +1,46 @@ + + diff --git a/resources/jscomposition/system/index.js b/resources/jscomposition/system/index.js new file mode 100644 index 0000000000..462c7f79f3 --- /dev/null +++ b/resources/jscomposition/system/index.js @@ -0,0 +1,7 @@ +import Breadcrums from "./Breadcrums.vue"; + +export * from "./table/index" + +export default {}; + +export { Breadcrums }; diff --git a/resources/jscomposition/system/table/FilterableTable.vue b/resources/jscomposition/system/table/FilterableTable.vue new file mode 100644 index 0000000000..396077aceb --- /dev/null +++ b/resources/jscomposition/system/table/FilterableTable.vue @@ -0,0 +1,87 @@ + + diff --git a/resources/jscomposition/system/table/Pagination.vue b/resources/jscomposition/system/table/Pagination.vue new file mode 100644 index 0000000000..de16c40ff0 --- /dev/null +++ b/resources/jscomposition/system/table/Pagination.vue @@ -0,0 +1,225 @@ + + + diff --git a/resources/jscomposition/system/table/SortTable.vue b/resources/jscomposition/system/table/SortTable.vue new file mode 100644 index 0000000000..3c1db18512 --- /dev/null +++ b/resources/jscomposition/system/table/SortTable.vue @@ -0,0 +1,46 @@ + + diff --git a/resources/jscomposition/system/table/cell/CaseTitleCell.vue b/resources/jscomposition/system/table/cell/CaseTitleCell.vue new file mode 100644 index 0000000000..0299bec34c --- /dev/null +++ b/resources/jscomposition/system/table/cell/CaseTitleCell.vue @@ -0,0 +1,59 @@ + + diff --git a/resources/jscomposition/system/table/cell/CollapseFormCell.vue b/resources/jscomposition/system/table/cell/CollapseFormCell.vue new file mode 100644 index 0000000000..ab2d971895 --- /dev/null +++ b/resources/jscomposition/system/table/cell/CollapseFormCell.vue @@ -0,0 +1,49 @@ + + + diff --git a/resources/jscomposition/system/table/cell/LinkCell.vue b/resources/jscomposition/system/table/cell/LinkCell.vue new file mode 100644 index 0000000000..777f0d2abc --- /dev/null +++ b/resources/jscomposition/system/table/cell/LinkCell.vue @@ -0,0 +1,45 @@ + + diff --git a/resources/jscomposition/system/table/cell/ParticipantsCell.vue b/resources/jscomposition/system/table/cell/ParticipantsCell.vue new file mode 100644 index 0000000000..4907726bd9 --- /dev/null +++ b/resources/jscomposition/system/table/cell/ParticipantsCell.vue @@ -0,0 +1,89 @@ + + diff --git a/resources/jscomposition/system/table/cell/StatusCell.vue b/resources/jscomposition/system/table/cell/StatusCell.vue new file mode 100644 index 0000000000..9556db9a3a --- /dev/null +++ b/resources/jscomposition/system/table/cell/StatusCell.vue @@ -0,0 +1,74 @@ + + diff --git a/resources/jscomposition/system/table/cell/TruncatedOptionsCell.vue b/resources/jscomposition/system/table/cell/TruncatedOptionsCell.vue new file mode 100644 index 0000000000..f581d2901c --- /dev/null +++ b/resources/jscomposition/system/table/cell/TruncatedOptionsCell.vue @@ -0,0 +1,116 @@ + + diff --git a/resources/jscomposition/system/table/cell/index.js b/resources/jscomposition/system/table/cell/index.js new file mode 100644 index 0000000000..218f02f1da --- /dev/null +++ b/resources/jscomposition/system/table/cell/index.js @@ -0,0 +1,15 @@ +import ParticipantsCell from "./ParticipantsCell.vue"; +import TruncatedOptionsCell from "./TruncatedOptionsCell.vue"; +import StatusCell from "./StatusCell.vue"; +import CaseTitleCell from "./CaseTitleCell.vue"; +import LinkCell from "./LinkCell.vue"; +import CollapseFormCell from "./CollapseFormCell.vue"; + +export { + ParticipantsCell, + TruncatedOptionsCell, + StatusCell, + CaseTitleCell, + LinkCell, + CollapseFormCell, +}; diff --git a/resources/jscomposition/system/table/filter/defaultFilter/FilterColumn.vue b/resources/jscomposition/system/table/filter/defaultFilter/FilterColumn.vue new file mode 100644 index 0000000000..4472d1e02b --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/FilterColumn.vue @@ -0,0 +1,117 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/FooterButtons.vue b/resources/jscomposition/system/table/filter/defaultFilter/FooterButtons.vue new file mode 100644 index 0000000000..a678920d39 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/FooterButtons.vue @@ -0,0 +1,26 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/SortingButtons.vue b/resources/jscomposition/system/table/filter/defaultFilter/SortingButtons.vue new file mode 100644 index 0000000000..bfc4d66f76 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/SortingButtons.vue @@ -0,0 +1,32 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/index.js b/resources/jscomposition/system/table/filter/defaultFilter/index.js new file mode 100644 index 0000000000..a3208c7eed --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/index.js @@ -0,0 +1,11 @@ +import FilterColumn from "./FilterColumn.vue"; +import FooterButtons from "./FooterButtons.vue"; +import SortingButtons from "./SortingButtons.vue"; + +export * from "./operator/index"; + +export { + FilterColumn, + FooterButtons, + SortingButtons, +}; diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/BetweenOperator.vue b/resources/jscomposition/system/table/filter/defaultFilter/operator/BetweenOperator.vue new file mode 100644 index 0000000000..ccbc05d467 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/BetweenOperator.vue @@ -0,0 +1,45 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/DateBetweenOperator.vue b/resources/jscomposition/system/table/filter/defaultFilter/operator/DateBetweenOperator.vue new file mode 100644 index 0000000000..24e0c3e1d5 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/DateBetweenOperator.vue @@ -0,0 +1,45 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/DateOperator.vue b/resources/jscomposition/system/table/filter/defaultFilter/operator/DateOperator.vue new file mode 100644 index 0000000000..ffa69b243e --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/DateOperator.vue @@ -0,0 +1,18 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/FilterOperator.vue b/resources/jscomposition/system/table/filter/defaultFilter/operator/FilterOperator.vue new file mode 100644 index 0000000000..58f953ac34 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/FilterOperator.vue @@ -0,0 +1,150 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/InOperator.vue b/resources/jscomposition/system/table/filter/defaultFilter/operator/InOperator.vue new file mode 100644 index 0000000000..08972c406a --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/InOperator.vue @@ -0,0 +1,70 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/InputOperator.vue b/resources/jscomposition/system/table/filter/defaultFilter/operator/InputOperator.vue new file mode 100644 index 0000000000..f2776f76d6 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/InputOperator.vue @@ -0,0 +1,21 @@ + + diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/index.js b/resources/jscomposition/system/table/filter/defaultFilter/operator/index.js new file mode 100644 index 0000000000..b1c78c6c8e --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/index.js @@ -0,0 +1,15 @@ +import BetweenOperator from "./BetweenOperator.vue"; +import FilterOperator from "./FilterOperator.vue"; +import InOperator from "./InOperator.vue"; +import InputOperator from "./InputOperator.vue"; +import DateOperator from "./DateOperator.vue"; +import DateBetweenOperator from "./DateBetweenOperator.vue"; + +export { + BetweenOperator, + FilterOperator, + InOperator, + InputOperator, + DateOperator, + DateBetweenOperator, +}; diff --git a/resources/jscomposition/system/table/filter/defaultFilter/operator/operatorConfig.js b/resources/jscomposition/system/table/filter/defaultFilter/operator/operatorConfig.js new file mode 100644 index 0000000000..4a6c626549 --- /dev/null +++ b/resources/jscomposition/system/table/filter/defaultFilter/operator/operatorConfig.js @@ -0,0 +1,39 @@ +import InputOperator from "./InputOperator.vue"; +import BetweenOperator from "./BetweenOperator.vue"; +import InOperator from "./InOperator.vue"; +import DateOperator from "./DateOperator.vue"; +import DateBetweenOperator from "./DateBetweenOperator.vue"; + +const operatorType = [ + { + operator: ["=", ">", "<", ">=", "<=", "contains", "regex"], + type: ["number", "string"], + component: () => InputOperator, + }, + { + operator: ["between"], + type: ["number", "string"], + component: () => BetweenOperator, + }, + { + operator: ["in"], + type: ["number", "string"], + component: () => InOperator, + }, + { + operator: [">", ">=", "<", "<="], + type: ["datetime"], + component: () => DateOperator, + }, + { + operator: ["between"], + type: ["datetime"], + component: () => DateBetweenOperator, + }, +]; + +export const getOperatorType = (operator = "=", type = "string") => { + const response = operatorType.find((e) => e.operator.includes(operator) && e.type.includes(type)); + + return response; +}; diff --git a/resources/jscomposition/system/table/filter/index.js b/resources/jscomposition/system/table/filter/index.js new file mode 100644 index 0000000000..72464fa69c --- /dev/null +++ b/resources/jscomposition/system/table/filter/index.js @@ -0,0 +1,2 @@ +export * from "./defaultFilter/index"; +export * from "./sortableFilter/index"; diff --git a/resources/jscomposition/system/table/filter/sortableFilter/SortableFilter.vue b/resources/jscomposition/system/table/filter/sortableFilter/SortableFilter.vue new file mode 100644 index 0000000000..94a930dd8c --- /dev/null +++ b/resources/jscomposition/system/table/filter/sortableFilter/SortableFilter.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/jscomposition/system/table/filter/sortableFilter/index.js b/resources/jscomposition/system/table/filter/sortableFilter/index.js new file mode 100644 index 0000000000..6d8f856462 --- /dev/null +++ b/resources/jscomposition/system/table/filter/sortableFilter/index.js @@ -0,0 +1,5 @@ +import SortableFilter from "./SortableFilter.vue"; + +export { + SortableFilter, +}; diff --git a/resources/jscomposition/system/table/index.js b/resources/jscomposition/system/table/index.js new file mode 100644 index 0000000000..f6470d7d1e --- /dev/null +++ b/resources/jscomposition/system/table/index.js @@ -0,0 +1,12 @@ +import FilterableTable from "./FilterableTable.vue"; +import SortTable from "./SortTable.vue"; +import Pagination from "./Pagination.vue"; + +export * from "./cell/index"; +export * from "./filter/index"; + +export { + FilterableTable, + SortTable, + Pagination, +}; diff --git a/resources/sass/tailwind.css b/resources/sass/tailwind.css new file mode 100644 index 0000000000..e66e36037c --- /dev/null +++ b/resources/sass/tailwind.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + a{ + color: #0872C2; + } + } \ No newline at end of file diff --git a/resources/views/cases/casesMain.blade.php b/resources/views/cases/casesMain.blade.php new file mode 100644 index 0000000000..5c945a6965 --- /dev/null +++ b/resources/views/cases/casesMain.blade.php @@ -0,0 +1,22 @@ +@extends('layouts.layout',['content_margin' => '', 'overflow-auto' => '']) + +@section('title') +@endsection + +@section('sidebar') +@include('layouts.sidebar', ['sidebar'=> Menu::get('sidebar_cases')]) +@endsection + +@section('content') +
+@endsection + +@section('js') + + +@endsection + +@section('css') +@endsection diff --git a/resources/views/cases/edit.blade.php b/resources/views/cases/edit.blade.php new file mode 100644 index 0000000000..810de3a7c4 --- /dev/null +++ b/resources/views/cases/edit.blade.php @@ -0,0 +1,185 @@ +@extends('layouts.layout',['content_margin' => '', 'overflow-auto' => '']) + +@section('title') + {{ __('Case Detail') }} +@endsection + +@section('sidebar') + @include('layouts.sidebar', ['sidebar' => Menu::get('sidebar_cases')]) +@endsection + +@section('breadcrumbs') + @include('shared.breadcrumbs', ['routes' => [ + __('Cases') => route('cases.index'), + ]]) +@endsection + +@section('content') +
+ + + + + + + +
+@endsection + +@section('js') + + +@endsection diff --git a/resources/views/layouts/layout.blade.php b/resources/views/layouts/layout.blade.php index 2fd4356d4c..301ddc5189 100644 --- a/resources/views/layouts/layout.blade.php +++ b/resources/views/layouts/layout.blade.php @@ -39,6 +39,7 @@ + @yield('css') @@ -805,6 +813,9 @@ classStatusCard() { }, ); }, + onReturnCase() { + window.open(`../cases/${request.case_number}`); + }, completeRequest() { ProcessMaker.confirmModal( this.$t('Caution!'), diff --git a/routes/api.php b/routes/api.php index 92f6e0ce7c..b0e50d5da6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -197,6 +197,7 @@ // Tasks Route::get('tasks', [TaskController::class, 'index'])->name('tasks.index'); // Already filtered in controller + Route::get('tasks-by-case', [TaskController::class, 'getTasksByCase'])->name('tasks.index.case'); Route::get('tasks/{task}', [TaskController::class, 'show'])->name('tasks.show')->middleware('can:view,task'); Route::get('tasks/{task}/screen_fields', [TaskController::class, 'getScreenFields'])->name('getScreenFields.show')->middleware('can:view,task'); Route::get('tasks/{task}/screens/{screen}', [TaskController::class, 'getScreen'])->name('tasks.get_screen')->middleware('can:viewScreen,task,screen'); @@ -221,7 +222,8 @@ Route::get('/tasks/rule-execution-log', [InboxRulesController::class, 'executionLog'])->name('inboxrules.execution-log'); // Cases - Route::get('cases', [ProcessRequestController::class, 'index'])->name('cases.index'); + //Route::get('cases', [ProcessRequestController::class, 'index'])->name('cases.index'); + Route::get('requests-by-case', [ProcessRequestController::class, 'getRequestsByCase'])->name('requests.getRequestsByCase'); // Requests Route::get('requests', [ProcessRequestController::class, 'index'])->name('requests.index'); // Already filtered in controller Route::get('requests/{process}/count', [ProcessRequestController::class, 'getCount'])->name('requests.count'); @@ -270,6 +272,7 @@ // Comments Route::get('comments', [CommentController::class, 'index'])->name('comments.index'); + Route::get('comments-by-case', [CommentController::class, 'getCommentsByCase'])->name('comments.index.case'); Route::get('comments/{comment}', [CommentController::class, 'show'])->name('comments.show'); Route::post('comments', [CommentController::class, 'store'])->name('comments.store'); Route::put('comments/{comment}', [CommentController::class, 'update'])->name('comments.update'); diff --git a/routes/v1_1/api.php b/routes/v1_1/api.php index d645fc10a5..a2b2328dc9 100644 --- a/routes/v1_1/api.php +++ b/routes/v1_1/api.php @@ -1,6 +1,7 @@ name('show.interstitial'); }); + + // Cases Endpoints + Route::name('cases.')->prefix('cases')->group(function () { + // Route to list all cases + Route::get('get_all_cases', [CaseController::class, 'getAllCases']) + ->name('all_cases'); + + // Route to list all in-progress cases + Route::get('get_in_progress', [CaseController::class, 'getInProgress']) + ->name('in_progress'); + + // Route to list all completed cases + Route::get('get_completed', [CaseController::class, 'getCompleted']) + ->name('completed'); + + // Route to get my cases counters + Route::get('get_my_cases_counters', [CaseController::class, 'getMyCasesCounters']) + ->name('my_cases_counters'); + }); + // Clipboard Endpoints Route::name('clipboard.')->prefix('clipboard')->group(function () { // Get clipboard by user diff --git a/routes/web.php b/routes/web.php index 73d17e71ee..9ae4b9b4f6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,6 +18,7 @@ use ProcessMaker\Http\Controllers\Auth\LoginController; use ProcessMaker\Http\Controllers\Auth\ResetPasswordController; use ProcessMaker\Http\Controllers\Auth\TwoFactorAuthController; +use ProcessMaker\Http\Controllers\CasesController; use ProcessMaker\Http\Controllers\Designer\DesignerController; use ProcessMaker\Http\Controllers\HomeController; use ProcessMaker\Http\Controllers\InboxRulesController; @@ -142,18 +143,18 @@ Route::post('/keep-alive', [LoginController::class, 'keepAlive'])->name('keep-alive'); // Cases - Route::get('cases', [RequestController::class, 'index'])->name('cases.index')->middleware('no-cache'); - Route::get('cases/{type?}', [RequestController::class, 'index'])->name('cases_by_type') - ->where('type', 'all|in_progress|completed') - ->middleware('no-cache'); + Route::get('cases', [CasesController::class, 'index'])->name('cases.index')->middleware('no-cache'); + Route::get('cases/{request}', [CasesController::class, 'edit'])->name('cases.edit'); + // This is a temporary API the engine team will create the API + Route::get('cases/{type?}', [CasesController::class, 'index'])->name('cases-main.index') + ->where('type', 'in_progress|completed|all') + ->middleware('no-cache'); // Requests - Route::get('requests', function () { - return redirect()->route('cases.index'); - })->name('requests.index')->middleware('no-cache'); - Route::get('requests/{type?}', function ($type = null) { - return redirect()->route('cases_by_type', ['type' => $type]); - })->where('type', 'all|in_progress|completed')->name('requests_by_type')->middleware('no-cache'); - + Route::get('requests', [RequestController::class, 'index']) + ->name('requests.index') + ->middleware('no-cache'); + Route::get('requests/{type?}', [RequestController::class, 'index']) + ->where('type', 'all|in_progress|completed')->name('requests_by_type')->middleware('no-cache'); Route::get('requests/{request}', [RequestController::class, 'show'])->name('requests.show'); Route::get('request/{request}/files/{media}', [RequestController::class, 'downloadFiles'])->middleware('can:view,request'); Route::get('requests/search', [RequestController::class, 'search'])->name('requests.search'); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000..170eca4655 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + prefix: "tw-", + content: [ + "./resources/**/*.blade.php", + "./resources/**/*.js", + "./resources/**/*.vue", + ], + theme: { + extend: {}, + }, + plugins: [], + safelist: [ + { + pattern: /(bg|outline|border|text)-(gray|purple|blue|amber|green|gray)-(100|200|300|400|500)/, + variants: ["hover"], + }, + ], +}; diff --git a/tests/Feature/Api/CommentTest.php b/tests/Feature/Api/CommentTest.php index a76dc1423c..5385578231 100644 --- a/tests/Feature/Api/CommentTest.php +++ b/tests/Feature/Api/CommentTest.php @@ -21,6 +21,8 @@ class CommentTest extends TestCase const API_TEST_URL = '/comments'; + const API_COMMENT_BY_CASE = '/comments-by-case'; + const STRUCTURE = [ 'id', 'user_id', @@ -220,4 +222,78 @@ public function testDeleteCommentNotExist() //Validate the header status code $response->assertStatus(405); } + + /** + * Test indexCase without case_number. + * + * @return void + */ + public function test_index_case_requires_case_number() + { + // Call the endpoint without the 'case_number' parameter + $response = $this->apiCall('GET', self::API_COMMENT_BY_CASE); + + // Check if the response returns a 400 error due to missing 'case_number' + $response->assertStatus(422) + ->assertJson(['message' => 'The Case number field is required.']); + } + + /** + * Test comments by case + */ + public function testGetCommentByTypeByCase() + { + $this->user = User::factory()->create([ + 'password' => Hash::make('password'), + 'is_administrator' => false, + ]); + // Create a request1 then a task related to the request + $request1 = ProcessRequest::factory()->create(); + $task1 = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->getKey(), + 'process_request_id' => $request1->getKey(), + ]); + Comment::factory()->count(2)->create([ + 'commentable_id' => $request1->getKey(), + 'commentable_type' => get_class($request1), + 'case_number' => $request1->case_number, + 'type' => 'LOG', + 'hidden' => false, + ]); + Comment::factory()->count(2)->create([ + 'commentable_id' => $task1->getKey(), + 'commentable_type' => get_class($task1), + 'case_number' => $request1->case_number, + 'type' => 'LOG', + 'hidden' => false, + ]); + // Create a request2 with the same case_number then a task related to the request + $request2 = ProcessRequest::factory()->create([ + 'parent_request_id' => $request1->getKey(), + ]); + $task2 = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->getKey(), + 'process_request_id' => $request2->getKey(), + ]); + Comment::factory()->count(2)->create([ + 'commentable_id' => $request2->getKey(), + 'commentable_type' => get_class($request2), + 'case_number' => $request1->case_number, + 'type' => 'LOG', + 'hidden' => false, + ]); + Comment::factory()->count(2)->create([ + 'commentable_id' => $task2->getKey(), + 'commentable_type' => get_class($task2), + 'case_number' => $request1->case_number, + 'type' => 'LOG', + 'hidden' => false, + ]); + // Load api + $filter = "?case_number=$request1->case_number"; + $response = $this->apiCall('GET', self::API_COMMENT_BY_CASE . $filter); + // Check if the response is successful and contains the expected tasks + $response->assertStatus(200); + $this->assertCount(8, $response->json('data')); + } } diff --git a/tests/Feature/Api/PermissionsTest.php b/tests/Feature/Api/PermissionsTest.php index 85dc86c365..23ae7f07f5 100644 --- a/tests/Feature/Api/PermissionsTest.php +++ b/tests/Feature/Api/PermissionsTest.php @@ -211,4 +211,55 @@ public function testCategoryPermission() $context('script', ScriptCategory::class); $context('screen', ScreenCategory::class); } + + /** + * Test if the created event in UserObserver assigns the correct permissions. + */ + public function testSetPermissionsViewMyRequestForUser() + { + $permissionName = 'view-my_requests'; + $permissionTitle = 'View My Requests'; + // Ensure permission is created without duplicates + Permission::firstOrCreate(['name' => $permissionName, 'title' => $permissionTitle]); + + // Create a user (this should trigger the observer) + $user = User::factory()->create(); + + // Assert the user has been assigned the correct permissions + $this->assertTrue($user->permissions()->where('name', $permissionName)->exists()); + } + + /** + * Test that the permissions are seeded and assigned to users and groups. + */ + public function testSetPermissionsViewMyRequestForUsersAndGroupCreated() + { + //Set up the users and groups + $users = User::factory()->count(5)->create(); + $groups = Group::factory()->count(3)->create(); + + //Run the seeder + $this->seed(PermissionSeeder::class); + $permissionName = 'view-my_requests'; + $permissionTitle = 'View My Requests'; + + //Verify that the permission exists + $this->assertDatabaseHas('permissions', [ + 'name' => $permissionName, + 'group' => 'Cases and Requests', + 'title' => $permissionTitle, + ]); + + //Verify that the permission is assigned to users + $permission = Permission::where('name', $permissionName)->first(); + $this->assertNotNull($permission); + foreach ($users as $user) { + $this->assertTrue($user->hasPermission($permissionName)); + } + + //Verify that the permission is assigned to groups + foreach ($groups as $group) { + $this->assertTrue($permission->groups->contains($group)); + } + } } diff --git a/tests/Feature/Api/ProcessRequestsTest.php b/tests/Feature/Api/ProcessRequestsTest.php index bc6c837089..438e97daef 100644 --- a/tests/Feature/Api/ProcessRequestsTest.php +++ b/tests/Feature/Api/ProcessRequestsTest.php @@ -30,6 +30,7 @@ class ProcessRequestsTest extends TestCase public $withPermissions = true; const API_TEST_URL = '/requests'; + const API_REQUESTS_BY_CASE = '/requests-by-case'; const STRUCTURE = [ 'id', @@ -986,4 +987,44 @@ public function testScreenRequested() $data = $response->json()['data']; $this->assertEmpty($data); } + + /** + * Get a list of Requests by Cases. + */ + public function testRequestByCase() + { + ProcessRequest::query()->delete(); + $request = ProcessRequest::factory()->create(); + ProcessRequest::factory()->count(9)->create([ + 'parent_request_id' => $request->id, + ]); + + $url = self::API_REQUESTS_BY_CASE . '?case_number=' . $request->case_number; + + $response = $this->apiCall('GET', $url); + + //Validate the header status code + $response->assertStatus(200); + + // Verify structure + $response->assertJsonStructure([ + 'data' => ['*' => self::STRUCTURE], + 'meta', + ]); + + // Verify count + $this->assertEquals(10, $response->json()['meta']['total']); + } + + /** + * Get a list of Requests by Cases. + */ + public function testRequestByCaseWithoutCaseNumber() + { + $response = $this->apiCall('GET', self::API_REQUESTS_BY_CASE); + + //Validate the header status code + $response->assertStatus(422); + $this->assertEquals('The Case number field is required.', $response->json()['message']); + } } diff --git a/tests/Feature/Api/TaskControllerTest.php b/tests/Feature/Api/TaskControllerTest.php new file mode 100644 index 0000000000..2c36069bb2 --- /dev/null +++ b/tests/Feature/Api/TaskControllerTest.php @@ -0,0 +1,142 @@ +create(); + Auth::login($user); + + // Call the endpoint without the 'case_number' parameter + $response = $this->apiCall('GET', self::API_TASK_BY_CASE); + + // Check if the response returns a 400 error due to missing 'case_number' + $response->assertStatus(422) + ->assertJson(['message' => 'The Case number field is required.']); + } + + /** + * Test indexCase returns active tasks related to the case_number. + * + * @return void + */ + public function test_index_case_returns_active_tasks_for_case_number() + { + // Simulate an authenticated user + $user = User::factory()->create(); + Auth::login($user); + + // Create a ProcessRequestToken associated with a specific case_number + $processRequest = ProcessRequest::factory()->create(); + ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'status' => 'ACTIVE', + 'process_request_id' => $processRequest->id, // id del ProcessRequest + ]); + + // Call the endpoint with the 'case_number' parameter + $filter = "?case_number=$processRequest->case_number"; + $response = $this->apiCall('GET', self::API_TASK_BY_CASE . $filter); + + // Check if the response is successful and contains the expected tasks + $response->assertStatus(200); + $this->assertCount(1, $response->json('data')); + $response->assertJsonStructure([ + 'data' => ['*' => self::TASK_BY_CASE_STRUCTURE], + 'meta', + ]); + } + + /** + * Test indexCase returns completed tasks related to the case_number. + * + * @return void + */ + public function test_index_case_returns_inactive_tasks_for_case_number() + { + // Simulate an authenticated user + $user = User::factory()->create(); + Auth::login($user); + + // Create a ProcessRequestToken associated with a specific case_number + $processRequest = ProcessRequest::factory()->create(); + ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, // id del ProcessRequest + ]); + + // Call the endpoint with the 'case_number' parameter + $filter = "?case_number=$processRequest->case_number&status=CLOSED"; + $response = $this->apiCall('GET', self::API_TASK_BY_CASE . $filter); + + // Check if the response is successful and contains the expected tasks + $response->assertStatus(200); + $this->assertCount(1, $response->json('data')); + $response->assertJsonStructure([ + 'data' => ['*' => self::TASK_BY_CASE_STRUCTURE], + 'meta', + ]); + } + + /** + * Test indexCase returns completed tasks related to the case_number. + * + * @return void + */ + public function test_index_cas_returns_with_data() + { + // Simulate an authenticated user + $user = User::factory()->create(); + Auth::login($user); + + // Create a ProcessRequestToken associated with a specific case_number + $processRequest = ProcessRequest::factory()->create(); + ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, // id del ProcessRequest + ]); + + // Call the endpoint with the 'case_number' parameter + $filter = "?case_number=$processRequest->case_number&status=CLOSED&includeScreen=" . true; + $response = $this->apiCall('GET', self::API_TASK_BY_CASE . $filter); + + // Check if the response is successful and contains the expected tasks + $response->assertStatus(200); + $this->assertCount(1, $response->json('data')); + $response->assertJsonStructure([ + 'data' => ['*' => array_merge(self::TASK_BY_CASE_STRUCTURE, ['taskData'])], + 'meta', + ]); + } +} diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index 3c9dbbd61d..e728593866 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -196,7 +196,7 @@ public function testDefaultValuesOfUser() 'lastname' => $faker->lastName(), 'email' => $faker->email(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), - 'password' => $faker->password(8) . 'A' . '1', + 'password' => $faker->password(8) . 'A' . '1' . '+', 'timezone' => 'America/Monterrey', ]); diff --git a/tests/Feature/Api/V1_1/CaseControllerSearchTest.php b/tests/Feature/Api/V1_1/CaseControllerSearchTest.php new file mode 100644 index 0000000000..75de41a10e --- /dev/null +++ b/tests/Feature/Api/V1_1/CaseControllerSearchTest.php @@ -0,0 +1,282 @@ +user = CaseControllerTest::createUser('user_a'); + } + + public function tearDown(): void + { + User::where('id', $this->user->id)->forceDelete(); + + parent::tearDown(); + } + + public function test_search_all_cases_by_case_number(): void + { + CaseControllerTest::createCasesStartedForUser($this->user->id, 5); + $caseNumber = 123456; + CaseControllerTest::createCasesStartedForUser($this->user->id, 1, ['case_number' => $caseNumber, 'keywords' => CaseUtils::getCaseNumberByKeywords($caseNumber)]); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(6, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => $caseNumber])); + $response->assertStatus(200); + $response->assertJsonCount(1, 'data'); + } + + public function test_search_in_progress_cases_by_case_number(): void + { + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 5, ['case_status' => 'IN_PROGRESS']); + $caseNumber = 123456; + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 1, [ + 'case_number' => $caseNumber, 'case_status' => 'IN_PROGRESS', 'keywords' => CaseUtils::getCaseNumberByKeywords($caseNumber), + ]); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress')); + $response->assertStatus(200); + $response->assertJsonCount(6, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['search' => $caseNumber])); + $response->assertStatus(200); + $response->assertJsonCount(1, 'data'); + } + + public function test_search_completed_cases_by_case_number(): void + { + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 5, ['case_status' => 'COMPLETED']); + $caseNumber = 987654; + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 1, [ + 'case_number' => $caseNumber, 'case_status' => 'COMPLETED', 'keywords' => CaseUtils::getCaseNumberByKeywords($caseNumber), + ]); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed')); + $response->assertStatus(200); + $response->assertJsonCount(6, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => $caseNumber])); + $response->assertStatus(200); + $response->assertJsonCount(1, 'data'); + } + + public function test_search_all_cases_by_case_title(): void + { + $caseTitle1 = 'Accident Insurance Registration Process'; + $caseTitle2 = 'ABE Spring'; + $caseTitle3 = 'Credit Evaluation Process'; + + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle1, 'keywords' => $caseTitle1]); + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle2, 'keywords' => $caseTitle2]); + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle3, 'keywords' => $caseTitle3]); + + $this->assertDatabaseCount('cases_started', 15); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(15, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'insurance'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'spri'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'accident registration'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'accident evaluation'])); + $response->assertStatus(200); + $response->assertJsonCount(0, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => '(credit)'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + public function test_search_in_progress_cases_by_case_title(): void + { + $caseTitle1 = 'Accident Insurance Registration Process'; + $caseTitle2 = 'ABE Spring'; + $caseTitle3 = 'Credit Evaluation Process'; + + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 10, ['case_title' => $caseTitle1, 'case_status' => 'IN_PROGRESS', 'keywords' => $caseTitle1]); + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 5, ['case_title' => $caseTitle2, 'case_status' => 'IN_PROGRESS', 'keywords' => $caseTitle2]); + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 5, ['case_title' => $caseTitle3, 'case_status' => 'IN_PROGRESS', 'keywords' => $caseTitle3]); + + $this->assertDatabaseCount('cases_participated', 20); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['pageSize' => 50])); + $response->assertStatus(200); + $response->assertJsonCount(20, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['search' => 'accident'])); + $response->assertStatus(200); + $response->assertJsonCount(10, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['search' => 'proc'])); + $response->assertStatus(200); + $response->assertJsonCount(15, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['search' => 'registration accident'])); + $response->assertStatus(200); + $response->assertJsonCount(10, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['search' => 'insurance abe'])); + $response->assertStatus(200); + $response->assertJsonCount(0, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['search' => '(evaluation)'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + public function test_search_completed_cases_by_case_title(): void + { + $caseTitle1 = 'Accident Insurance Registration Process'; + $caseTitle2 = 'ABE Spring'; + $caseTitle3 = 'Credit Evaluation Process 123-abc'; + + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 5, ['case_title' => $caseTitle1, 'case_status' => 'COMPLETED', 'keywords' => $caseTitle1]); + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 10, ['case_title' => $caseTitle2, 'case_status' => 'COMPLETED', 'keywords' => $caseTitle2]); + CaseControllerTest::createCasesParticipatedForUser($this->user->id, 5, ['case_title' => $caseTitle3, 'case_status' => 'COMPLETED', 'keywords' => $caseTitle3]); + + $this->assertDatabaseCount('cases_participated', 20); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['pageSize' => 30])); + $response->assertStatus(200); + $response->assertJsonCount(20, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => 'accident'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => 'ab'])); + $response->assertStatus(200); + $response->assertJsonCount(15, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => 'credit evaluation'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => 'proc spr'])); + $response->assertStatus(200); + $response->assertJsonCount(0, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => '(accident)'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed', ['search' => '(123-abc)'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + public function test_search_all_cases_special_by_dni(): void + { + $caseTitle1 = 'this is a ci 123456LP'; + $caseTitle2 = "John's Vacation"; + + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle1, 'keywords' => $caseTitle1]); + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle2, 'keywords' => $caseTitle2]); + + $this->assertDatabaseCount('cases_started', 10); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(10, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => '123456LP'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => "John's"])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + public function test_search_all_cases_special_by_japanese_characters(): void + { + $caseTitle1 = '信用評価プロセス'; + + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle1, 'keywords' => $caseTitle1]); + + $this->assertDatabaseCount('cases_started', 5); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => $caseTitle1])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => '信用'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + public function test_search_all_cases_special_by_thai_characters(): void + { + $caseTitle1 = 'กระบวนการประเมินเครดิต'; + + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle1, 'keywords' => $caseTitle1]); + + $this->assertDatabaseCount('cases_started', 5); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'กระบวนการ'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + public function test_search_all_cases_special_by_french_characters(): void + { + $caseTitle1 = "Processus du crédit"; + + CaseControllerTest::createCasesStartedForUser($this->user->id, 5, ['case_title' => $caseTitle1, 'keywords' => $caseTitle1]); + + $this->assertDatabaseCount('cases_started', 5); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'Processus'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => 'crédit'])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => "Processus crédit"])); + $response->assertStatus(200); + $response->assertJsonCount(5, 'data'); + } + + protected function connectionsToTransact() + { + return []; + } +} diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php new file mode 100644 index 0000000000..1e91200d7c --- /dev/null +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -0,0 +1,505 @@ +create([ + 'username' => $username, + 'password' => Hash::make($password), + 'status' => $status, + ]); + } + + public static function createCasesStartedForUser(int $userId, int $count = 1, $data = []) + { + return CaseStarted::factory()->count($count)->create(array_merge(['user_id' => $userId], $data)); + } + + public static function createCasesParticipatedForUser(int $userId, int $count = 1, $data = []) + { + return CaseParticipated::factory()->count($count)->create(array_merge(['user_id' => $userId], $data)); + } + + public function test_get_all_cases(): void + { + $userA = self::createUser('user_a'); + $cases = self::createCasesStartedForUser($userA->id, 10); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + } + + public function test_get_in_progress(): void + { + $userA = self::createUser('user_a'); + $cases = self::createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress')); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonMissing(['case_status' => 'COMPLETED']); + + // The status parameter should be ignored + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress'), ['status' => 'COMPLETED']); + $response->assertJsonCount($cases->count(), 'data'); + } + + public function test_get_completed(): void + { + $userA = self::createUser('user_a'); + $cases = self::createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'COMPLETED']); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed')); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonFragment(['case_status' => 'COMPLETED']); + $response->assertJsonMissing(['case_status' => 'IN_PROGRESS']); + + // The status parameter should be ignored + $response = $this->apiCall('GET', route('api.1.1.cases.completed'), ['status' => 'IN_PROGRESS']); + $response->assertJsonCount($cases->count(), 'data'); + } + + public function test_get_all_cases_by_users(): void + { + $userA = self::createUser('user_a'); + $userB = self::createUser('user_b'); + + $casesA = self::createCasesStartedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesB = self::createCasesStartedForUser($userB->id, 6, ['case_status' => 'COMPLETED']); + $casesC = self::createCasesStartedForUser($userA->id, 4, ['case_status' => 'IN_PROGRESS']); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + + $total = $casesA->count() + $casesB->count() + $casesC->count(); + $response->assertStatus(200); + $response->assertJsonCount($total, 'data'); + + $totalUserA = $casesA->count() + $casesC->count(); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['userId' => $userA->id])); + $response->assertStatus(200); + $response->assertJsonCount($totalUserA, 'data'); + $response->assertJsonMissing(['user_id' => $userB->id]); + + $totalUserB = $casesB->count(); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['userId' => $userB->id])); + $response->assertStatus(200); + $response->assertJsonCount($totalUserB, 'data'); + $response->assertJsonMissing(['user_id' => $userA->id]); + } + + public function test_get_all_cases_by_status(): void + { + $userA = self::createUser('user_a'); + $userB = self::createUser('user_b'); + + $casesA = self::createCasesStartedForUser($userA->id, 5, ['case_status' => 'COMPLETED']); + $casesB = self::createCasesStartedForUser($userB->id, 6, ['case_status' => 'IN_PROGRESS']); + $casesC = self::createCasesStartedForUser($userA->id, 4, ['case_status' => 'COMPLETED']); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['status' => 'IN_PROGRESS'])); + $response->assertStatus(200); + $response->assertJsonCount($casesB->count(), 'data'); + + $totalCompleted = $casesA->count() + $casesC->count(); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['status' => 'COMPLETED'])); + $response->assertStatus(200); + $response->assertJsonCount($totalCompleted, 'data'); + } + + public function test_get_in_progress_by_user(): void + { + $userA = self::createUser('user_a'); + $userB = self::createUser('user_b'); + $userC = self::createUser('user_c'); + $casesA = self::createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesB = self::createCasesParticipatedForUser($userB->id, 6, ['case_status' => 'IN_PROGRESS']); + $casesC = self::createCasesParticipatedForUser($userC->id, 4, ['case_status' => 'IN_PROGRESS']); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['userId' => $userA->id])); + $response->assertStatus(200); + $response->assertJsonCount($casesA->count(), 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['userId' => $userB->id])); + $response->assertStatus(200); + $response->assertJsonCount($casesB->count(), 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['userId' => $userC->id])); + $response->assertStatus(200); + $response->assertJsonCount($casesC->count(), 'data'); + } + + public function test_get_all_cases_sort_by_case_number(): void + { + $userA = self::createUser('user_a'); + $cases = self::createCasesStartedForUser($userA->id, 10); + + $casesSorted = $cases->sortBy('case_number'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'case_number:asc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.case_number', $casesSorted->first()->case_number); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'case_number:desc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.case_number', $casesSorted->last()->case_number); + } + + public function test_get_all_cases_sort_by_completed_at(): void + { + $userA = self::createUser('user_a'); + $cases = self::createCasesStartedForUser($userA->id, 10); + $casesSorted = $cases->sortBy('completed_at'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'completed_at:asc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.completed_at', $casesSorted->first()->completed_at->format('Y-m-d H:i:s')); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'completed_at:desc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.completed_at', $casesSorted->last()->completed_at->format('Y-m-d H:i:s')); + } + + public function test_get_all_cases_sort_by_invalid_field(): void + { + $invalidField = 'invalid_field'; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => $invalidField])); + $response->assertStatus(422); + $response->assertJsonPath('message', 'The sortBy must be a comma-separated list of field:asc|desc.'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => "$invalidField:asc"])); + $response->assertStatus(422); + $response->assertJsonFragment(['message' => "Sort by field $invalidField is not allowed."]); + } + + public function test_filter_by_case_number(): void + { + $userA = self::createUser('user_a'); + $caseNumber = 123456; + self::createCasesStartedForUser($userA->id, 5); + self::createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'case_number'], + 'operator' => '=', + 'value' => $caseNumber, + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + $response->assertJsonCount(1, 'data'); + $response->assertJsonFragment(['case_number' => $caseNumber]); + } + + public function test_filter_by_case_status(): void + { + $userA = self::createUser('user_a'); + $casesA = self::createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = self::createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('case_status', 'IN_PROGRESS')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonMissing(['case_status' => 'COMPLETED']); + } + + public function test_filter_by_user_and_case_status(): void + { + $userA = self::createUser('user_a'); + $casesA = self::createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = self::createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonFragment(['user_id' => $userA->id]); + } + + public function test_filter_by_user_case_status_and_created_at(): void + { + $userA = self::createUser('user_a'); + $casesA = self::createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = self::createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'created_at'], + 'operator' => '>', + 'value' => '2023-02-10', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonFragment(['user_id' => $userA->id]); + } + + public function test_filter_by_user_case_status_created_at_and_completed_at(): void + { + $userA = self::createUser('user_a'); + $casesA = self::createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = self::createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'created_at'], + 'operator' => '>', + 'value' => '2023-02-10', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'completed_at'], + 'operator' => '>', + 'value' => '2023-04-01', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count(); + + $response->assertJsonCount($total, 'data'); + $json = $response->json(); + $metaTotal = $json['meta']['total']; + $this->assertEquals($total, $metaTotal, 'The total count of cases does not match the expected value. ' . json_encode($json)); + } + + public function test_get_all_cases_filter_by_invalid_field(): void + { + $invalidField = 'invalid_field'; + $filterBy = ['filterBy' => '[invalid_json']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(422); + $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => [$invalidField => 'value']])); + $response->assertStatus(422); + $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); + } + + public function test_get_my_cases_counters_ok(): void + { + /** + * Creating missing permissions, probably this part should be removed when + * the permissions were added in another ticket + */ + Permission::create([ + 'name' => 'view-all_cases', + 'title' => 'View All Cases', + ]); + Permission::create([ + 'name' => 'view-my_requests', + 'title' => 'View My Requests', + ]); + + $userA = self::createUser('user_a'); + $userB = self::createUser('user_b'); + + $userA->giveDirectPermission('view-all_cases'); + $userA->giveDirectPermission('view-my_requests'); + $userB->giveDirectPermission('view-all_cases'); + $userB->giveDirectPermission('view-my_requests'); + + $casesA = self::createCasesStartedForUser($userA->id, 5, ['case_status' => 'COMPLETED']); + $casesB = self::createCasesStartedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesC = self::createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'COMPLETED']); + $casesD = self::createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + + $casesE = self::createCasesStartedForUser($userB->id, 5, ['case_status' => 'COMPLETED']); + $casesF = self::createCasesStartedForUser($userB->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesG = self::createCasesParticipatedForUser($userB->id, 5, ['case_status' => 'COMPLETED']); + $casesH = self::createCasesParticipatedForUser($userB->id, 5, ['case_status' => 'IN_PROGRESS']); + + $in_progress = ProcessRequest::factory(5)->create([ + 'status' => 'ACTIVE', + 'user_id' => $userA->id, + ]); + + $response = $this->apiCall('GET', route('api.1.1.cases.my_cases_counters'), ['userId' => $userA->id]); + + $response->assertStatus(200); + $response->assertJsonFragment(['totalAllCases' => 20]); + $response->assertJsonFragment(['totalMyCases' => 10]); + $response->assertJsonFragment(['totalInProgress' => 5]); + $response->assertJsonFragment(['totalCompleted' => 5]); + $response->assertJsonFragment(['totalMyRequest' => 5]); + } + + public function test_get_all_cases_participants(): void + { + $userA = $this->createUser('user_a'); + $userB = $this->createUser('user_b'); + + $casesA = $this->createCasesStartedForUser($userA->id, 1, ['case_status' => 'IN_PROGRESS', 'participants' => [$userA->id, $userB->id]]); + $casesB = $this->createCasesStartedForUser($userB->id, 1, ['case_status' => 'COMPLETED', 'participants' => [$userA->id]]); + $casesC = $this->createCasesStartedForUser($userA->id, 1, ['case_status' => 'IN_PROGRESS', 'participants' => [$userB->id]]); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + + $total = $casesA->count() + $casesB->count() + $casesC->count(); + $response->assertStatus(200); + $response->assertJsonCount($total, 'data'); + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'participants' => [ + '*' => [ + 'id', + 'name', + 'title', + 'avatar', + ], + ], + ], + ], + ]); + + $response->assertJsonFragment(['participants' => [ + [ + 'id' => $userA->id, + 'name' => $userA->fullname, + 'title' => $userA->title, + 'avatar' => $userA->avatar, + ], + [ + 'id' => $userB->id, + 'name' => $userB->fullname, + 'title' => $userB->title, + 'avatar' => $userB->avatar, + ], + ]]); + + $response->assertJsonPath('data.1.participants', [ + [ + 'id' => $userA->id, + 'name' => $userA->fullname, + 'title' => $userA->title, + 'avatar' => $userA->avatar, + ], + ]); + + $response->assertJsonPath('data.2.participants', [ + [ + 'id' => $userB->id, + 'name' => $userB->fullname, + 'title' => $userB->title, + 'avatar' => $userB->avatar, + ], + ]); + } +} diff --git a/tests/Feature/Cases/CaseExceptionTest.php b/tests/Feature/Cases/CaseExceptionTest.php new file mode 100644 index 0000000000..5bf3317dd4 --- /dev/null +++ b/tests/Feature/Cases/CaseExceptionTest.php @@ -0,0 +1,134 @@ +user = User::factory()->create(); + $this->process = Process::factory()->create(); + + $this->instance = ProcessRequest::factory()->create([ + 'user_id' => $this->user->id, + 'process_id' => $this->process->id, + ]); + $this->token = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->id, + 'process_request_id' => $this->instance->id, + 'element_type' => 'task', + ]); + + $this->instance2 = ProcessRequest::factory()->create([ + 'user_id' => $this->user->id, + 'process_id' => $this->process->id, + ]); + $this->token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->id, + 'process_request_id' => $this->instance2->id, + 'element_type' => 'task', + ]); + } + + public function test_create_case_missing_case_number(): void + { + $this->withoutExceptionHandling(); + + $this->instance->case_number = null; + $repo = new CaseRepository(); + $repo->create($this->instance); + + $this->assertDatabaseCount('cases_started', 0); + } + + public function test_create_case_missing_user_id(): void + { + $this->withoutExceptionHandling(); + + try { + $this->instance->user_id = null; + $repo = new CaseRepository(); + $repo->create($this->instance); + } catch (\Exception $e) { + $this->assertStringContainsString('Column \'user_id\' cannot be null', $e->getMessage()); + } + + $this->assertDatabaseCount('cases_started', 0); + } + + public function test_create_case_missing_case_title(): void + { + $this->withoutExceptionHandling(); + + try { + $this->instance->case_title = null; + $repo = new CaseRepository(); + $repo->create($this->instance); + } catch (\Exception $e) { + $this->assertStringContainsString('Column \'case_title\' cannot be null', $e->getMessage()); + } + + $this->assertDatabaseCount('cases_started', 0); + } + + public function test_update_case_missing_case_started(): void + { + $this->withoutExceptionHandling(); + + try { + $this->instance->case_title = null; + $repo = new CaseRepository(); + $repo->create($this->instance); + } catch (\Exception $e) { + $this->assertStringContainsString('Column \'case_title\' cannot be null', $e->getMessage()); + } + + $this->assertDatabaseCount('cases_started', 0); + + try { + $repo->update($this->instance, $this->token); + } catch (\Exception $e) { + $this->assertEquals( + 'case started not found, method=update, instance=' . $this->instance->getKey(), $e->getMessage() + ); + } + } + + public function test_artisan_sync_command_missing_ids(): void + { + $this->artisan('cases:sync') + ->expectsOutput('Please specify a list of request IDs.') + ->assertExitCode(0); + } + + public function test_artisan_sync_command_success(): void + { + $this->artisan('cases:sync --request_ids=' . $this->instance->id . ',' . $this->instance2->id) + ->expectsOutput('Case started synced ' . $this->instance->case_number) + ->expectsOutput('Case started synced ' . $this->instance2->case_number) + ->assertExitCode(0); + + $this->assertDatabaseCount('cases_started', 2); + } +} diff --git a/tests/Feature/Cases/CaseParticipatedTest.php b/tests/Feature/Cases/CaseParticipatedTest.php new file mode 100644 index 0000000000..01c3d21c49 --- /dev/null +++ b/tests/Feature/Cases/CaseParticipatedTest.php @@ -0,0 +1,368 @@ +create(); + $process = Process::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseHas('cases_started', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + ]); + + $this->assertDatabaseCount('cases_participated', 0); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + + $this->assertDatabaseCount('cases_participated', 1); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + 'processes->[0]->id' => $process->id, + 'processes->[0]->name' => $process->name, + 'requests->[0]->id' => $instance->id, + 'requests->[0]->name' => $instance->name, + 'requests->[0]->parent_request_id' => $instance->parent_request_id, + 'request_tokens->[0]' => $token->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + } + + public function test_create_multiple_case_participated() + { + $user = User::factory()->create(); + $process = Process::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseHas('cases_started', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + + $this->assertDatabaseCount('cases_participated', 1); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token2); + + $this->assertDatabaseCount('cases_participated', 1); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token->id, + 'request_tokens->[1]' => $token2->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + 'tasks->[1]->id' => $token2->id, + 'tasks->[1]->element_id' => $token2->element_id, + 'tasks->[1]->name' => $token2->element_name, + 'tasks->[1]->process_id' => $token2->process_id, + ]); + } + + public function test_update_case_participated_users() + { + $user = User::factory()->create(); + $user2 = User::factory()->create(); + $process = Process::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseHas('cases_started', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + + $this->assertDatabaseCount('cases_participated', 1); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user2->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user2->id, + 'case_number' => $instance->case_number, + 'case_title' => $instance->case_title, + 'case_title_formatted' => $instance->case_title_formatted, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token2->id, + 'tasks->[0]->id' => $token2->id, + 'tasks->[0]->element_id' => $token2->element_id, + 'tasks->[0]->name' => $token2->element_name, + 'tasks->[0]->process_id' => $token2->process_id, + ]); + } + + public function test_update_case_participated_user_tasks() + { + $user = User::factory()->create(); + $user2 = User::factory()->create(); + $process = Process::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + + $this->assertDatabaseCount('cases_participated', 1); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'request_tokens->[0]' => $token->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user2->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user2->id, + 'case_number' => $instance->case_number, + 'request_tokens->[0]' => $token2->id, + 'tasks->[0]->id' => $token2->id, + 'tasks->[0]->element_id' => $token2->element_id, + 'tasks->[0]->name' => $token2->element_name, + 'tasks->[0]->process_id' => $token2->process_id, + ]); + + $token3 = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token3); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'request_tokens->[0]' => $token->id, + 'request_tokens->[1]' => $token3->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + 'tasks->[1]->id' => $token3->id, + 'tasks->[1]->element_id' => $token3->element_id, + 'tasks->[1]->name' => $token3->element_name, + 'tasks->[1]->process_id' => $token3->process_id, + ]); + } + + public function test_update_case_participated_completed() + { + $user = User::factory()->create(); + $user2 = User::factory()->create(); + $process = Process::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + + $this->assertDatabaseCount('cases_participated', 1); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user2->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user2->id, + 'case_number' => $instance->case_number, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token2->id, + 'tasks->[0]->id' => $token2->id, + 'tasks->[0]->element_id' => $token2->element_id, + 'tasks->[0]->name' => $token2->element_name, + 'tasks->[0]->process_id' => $token2->process_id, + ]); + + $token3 = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token3); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'user_id' => $user->id, + 'case_number' => $instance->case_number, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token->id, + 'request_tokens->[1]' => $token3->id, + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + 'tasks->[1]->id' => $token3->id, + 'tasks->[1]->element_id' => $token3->element_id, + 'tasks->[1]->name' => $token3->element_name, + 'tasks->[1]->process_id' => $token3->process_id, + ]); + + $instance->status = 'COMPLETED'; + $repo->updateStatus($instance); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $instance->case_number, + 'case_status' => 'COMPLETED', + 'completed_at' => now(), + ]); + } +} diff --git a/tests/Feature/Cases/CaseStartedSubProcessTest.php b/tests/Feature/Cases/CaseStartedSubProcessTest.php new file mode 100644 index 0000000000..d1bfa23be9 --- /dev/null +++ b/tests/Feature/Cases/CaseStartedSubProcessTest.php @@ -0,0 +1,378 @@ +user = User::factory()->create(); + $this->process = Process::factory()->create(); + $this->parentRequest = ProcessRequest::factory()->create([ + 'user_id' => $this->user->id, + 'status' => 'ACTIVE', + 'process_id' => $this->process->id, + ]); + $this->parentToken = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->id, + 'process_request_id' => $this->parentRequest->id, + 'element_type' => 'task', + ]); + $this->subProcess = Process::factory()->create(); + $this->childRequest = ProcessRequest::factory()->create([ + 'user_id' => $this->user->id, + 'status' => 'ACTIVE', + 'parent_request_id' => $this->parentRequest->id, + 'process_id' => $this->subProcess->id, + ]); + $this->childToken = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->id, + 'process_request_id' => $this->childRequest->id, + 'element_type' => 'task', + ]); + + $this->user2 = User::factory()->create(); + $this->childToken2 = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user2->id, + 'process_request_id' => $this->childRequest->id, + 'element_type' => 'task', + ]); + } + + public function test_create_case_sub_process() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + + $repo = new CaseRepository(); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + } + + public function test_create_case_processes() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + + $repo = new CaseRepository(); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'processes->[0]->id' => $this->process->id, + 'processes->[0]->name' => $this->process->name, + 'processes->[1]->id' => $this->subProcess->id, + 'processes->[1]->name' => $this->subProcess->name, + ]); + } + + public function test_create_case_requests() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + + $repo = new CaseRepository(); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'requests->[0]->id' => $this->parentRequest->id, + 'requests->[0]->name' => $this->parentRequest->name, + 'requests->[0]->parent_request_id' => $this->parentRequest->parent_request_id, + 'requests->[1]->id' => $this->childRequest->id, + 'requests->[1]->name' => $this->childRequest->name, + 'requests->[1]->parent_request_id' => $this->childRequest->parent_request_id, + ]); + } + + public function test_create_case_request_tokens() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + + $repo = new CaseRepository(); + $repo->create($this->childRequest); + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $this->parentToken->id, + 'request_tokens->[1]' => $this->childToken->id, + ]); + } + + public function test_create_case_tasks() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + + $repo = new CaseRepository(); + $repo->create($this->childRequest); + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[0]->id' => $this->parentToken->id, + 'tasks->[0]->element_id' => $this->parentToken->element_id, + 'tasks->[0]->name' => $this->parentToken->element_name, + 'tasks->[0]->process_id' => $this->parentToken->process_id, + 'tasks->[1]->id' => $this->childToken->id, + 'tasks->[1]->element_id' => $this->childToken->element_id, + 'tasks->[1]->name' => $this->childToken->element_name, + 'tasks->[1]->process_id' => $this->childToken->process_id, + ]); + } + + public function test_create_case_participated_processes() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_participated', 0); + + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + $repo->update($this->childRequest, $this->childToken2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'processes->[0]->id' => $this->process->id, + 'processes->[0]->name' => $this->process->name, + 'processes->[1]->id' => $this->subProcess->id, + 'processes->[1]->name' => $this->subProcess->name, + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user2->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'processes->[0]->id' => $this->subProcess->id, + 'processes->[0]->name' => $this->subProcess->name, + ]); + } + + public function test_create_case_participated_requests() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_participated', 0); + + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + $repo->update($this->childRequest, $this->childToken2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'requests->[0]->id' => $this->parentRequest->id, + 'requests->[0]->name' => $this->parentRequest->name, + 'requests->[0]->parent_request_id' => $this->parentRequest->parent_request_id, + 'requests->[1]->id' => $this->childRequest->id, + 'requests->[1]->name' => $this->childRequest->name, + 'requests->[1]->parent_request_id' => $this->childRequest->parent_request_id, + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user2->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'requests->[0]->id' => $this->childRequest->id, + 'requests->[0]->name' => $this->childRequest->name, + 'requests->[0]->parent_request_id' => $this->childRequest->parent_request_id, + ]); + } + + public function test_create_case_participated_request_tokens() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_participated', 0); + + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + $repo->update($this->childRequest, $this->childToken2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $this->parentToken->id, + 'request_tokens->[1]' => $this->childToken->id, + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user2->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $this->childToken2->id, + ]); + } + + public function test_create_case_participated_tasks() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + $repo->create($this->childRequest); + + $this->assertDatabaseCount('cases_participated', 0); + + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + $repo->update($this->childRequest, $this->childToken2); + + $this->assertDatabaseCount('cases_participated', 2); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[0]->id' => $this->parentToken->id, + 'tasks->[0]->element_id' => $this->parentToken->element_id, + 'tasks->[0]->name' => $this->parentToken->element_name, + 'tasks->[0]->process_id' => $this->parentToken->process_id, + 'tasks->[1]->id' => $this->childToken->id, + 'tasks->[1]->element_id' => $this->childToken->element_id, + 'tasks->[1]->name' => $this->childToken->element_name, + 'tasks->[1]->process_id' => $this->childToken->process_id, + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user2->id, + 'case_title' => $this->parentRequest->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[0]->id' => $this->childToken2->id, + 'tasks->[0]->element_id' => $this->childToken2->element_id, + 'tasks->[0]->name' => $this->childToken2->element_name, + 'tasks->[0]->process_id' => $this->childToken2->process_id, + 'tasks->[1]->id' => null, + 'tasks->[1]->element_id' => null, + 'tasks->[1]->name' => null, + 'tasks->[1]->process_id' => null, + ]); + } + + public function test_update_case_participated_completed() + { + $repo = new CaseRepository(); + $repo->create($this->parentRequest); + $repo->create($this->childRequest); + + $repo->update($this->parentRequest, $this->parentToken); + $repo->update($this->childRequest, $this->childToken); + $repo->update($this->childRequest, $this->childToken2); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseCount('cases_participated', 2); + + $this->childRequest->status = 'COMPLETED'; + $repo->updateStatus($this->childRequest); + + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'case_status' => 'IN_PROGRESS', + 'completed_at' => null, + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_status' => 'IN_PROGRESS', + 'completed_at' => null, + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user2->id, + 'case_status' => 'IN_PROGRESS', + 'completed_at' => null, + ]); + + $this->parentRequest->status = 'COMPLETED'; + $repo->updateStatus($this->parentRequest); + + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $this->parentRequest->case_number, + 'case_status' => 'COMPLETED', + 'completed_at' => now(), + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user->id, + 'case_status' => 'COMPLETED', + 'completed_at' => now(), + ]); + $this->assertDatabaseHas('cases_participated', [ + 'case_number' => $this->parentRequest->case_number, + 'user_id' => $this->user2->id, + 'case_status' => 'COMPLETED', + 'completed_at' => now(), + ]); + } +} diff --git a/tests/Feature/Cases/CaseStartedTest.php b/tests/Feature/Cases/CaseStartedTest.php new file mode 100644 index 0000000000..ee5e6e8f97 --- /dev/null +++ b/tests/Feature/Cases/CaseStartedTest.php @@ -0,0 +1,396 @@ +create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + } + + public function test_create_multiple_cases() + { + $user = User::factory()->create(); + $instance1 = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + ]); + $instance2 = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance1); + $repo->create($instance2); + + $this->assertDatabaseCount('cases_started', 2); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance1->case_number, + 'user_id' => $user->id, + 'case_title' => $instance1->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance2->case_number, + 'user_id' => $user->id, + 'case_title' => $instance2->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + } + + public function test_create_case_started_processes() + { + $process = Process::factory()->create(); + + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'processes->[0]->id' => $process->id, + 'processes->[0]->name' => $process->name, + ]); + } + + public function test_create_case_started_requests() + { + $process = Process::factory()->create(); + + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'requests->[0]->id' => $instance->id, + 'requests->[0]->name' => $instance->name, + 'requests->[0]->parent_request_id' => $instance->parent_request_id ?? 0, + ]); + } + + public function test_update_case_started_request_tokens() + { + $process = Process::factory()->create(); + + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'request_tokens->[0]' => $token->id, + ]); + } + + public function test_update_case_started_tasks() + { + $process = Process::factory()->create(); + + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token2); + + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[1]->id' => $token2->id, + 'tasks->[1]->element_id' => $token2->element_id, + 'tasks->[1]->name' => $token2->element_name, + 'tasks->[1]->process_id' => $token2->process_id, + ]); + } + + public function test_update_case_started_script_tasks() + { + $process = Process::factory()->create(); + + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'scriptTask', + ]); + + $repo->update($instance, $token2); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'tasks->[1]->id' => null, + 'tasks->[1]->element_id' => null, + 'tasks->[1]->name' => null, + 'tasks->[1]->process_id' => null, + ]); + } + + public function test_update_case_started_participants() + { + $process = Process::factory()->create(); + + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + ]); + + $repo->update($instance, $token); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'participants->[0]' => $user->id, + ]); + + $user2 = User::factory()->create(); + $token2 = ProcessRequestToken::factory()->create([ + 'user_id' => $user2->id, + 'process_request_id' => $instance->id, + ]); + + $repo->update($instance, $token2); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'participants->[1]' => $user2->id, + ]); + } + + public function test_update_case_started_status() + { + $process = Process::factory()->create(); + $user = User::factory()->create(); + + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + 'process_id' => $process->id, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 1); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + ]); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'IN_PROGRESS', + 'completed_at' => null, + 'request_tokens->[0]' => $token->id, + ]); + + $instance->status = 'COMPLETED'; + $repo->updateStatus($instance, $token); + $this->assertDatabaseHas('cases_started', [ + 'case_number' => $instance->case_number, + 'user_id' => $user->id, + 'case_title' => $instance->case_title, + 'case_status' => 'COMPLETED', + 'completed_at' => now(), + 'tasks->[0]->id' => $token->id, + 'tasks->[0]->element_id' => $token->element_id, + 'tasks->[0]->name' => $token->element_name, + 'tasks->[0]->process_id' => $token->process_id, + ]); + } + + public function test_try_update_if_case_has_not_been_created() + { + $user = User::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => null, + ]); + + $repo = new CaseRepository(); + $repo->create($instance); + + $this->assertDatabaseCount('cases_started', 0); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'process_request_id' => $instance->id, + 'element_type' => 'task', + ]); + + $repo->update($instance, $token); + $this->assertDatabaseCount('cases_started', 0); + } +} diff --git a/tests/Feature/Cases/CasesJobTest.php b/tests/Feature/Cases/CasesJobTest.php new file mode 100644 index 0000000000..1f3783f773 --- /dev/null +++ b/tests/Feature/Cases/CasesJobTest.php @@ -0,0 +1,68 @@ +create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + ]); + + CaseStore::dispatch($instance); + + Queue::assertPushed(CaseStore::class, 1); + } + + public function test_handle_case_update_job() + { + Queue::fake(); + + $user = User::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + ]); + + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $instance->id, + ]); + + CaseUpdate::dispatch($instance, $token); + + Queue::assertPushed(CaseUpdate::class, 1); + } + + public function test_handle_case_update_status_job() + { + Queue::fake(); + + $user = User::factory()->create(); + $instance = ProcessRequest::factory()->create([ + 'user_id' => $user->id, + ]); + + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $instance->id, + ]); + + CaseUpdateStatus::dispatch($instance, $token); + + Queue::assertPushed(CaseUpdateStatus::class, 1); + } +} diff --git a/tests/Feature/RedirectTest.php b/tests/Feature/RedirectTest.php index f595a5100b..0da2c3e6b3 100644 --- a/tests/Feature/RedirectTest.php +++ b/tests/Feature/RedirectTest.php @@ -24,11 +24,11 @@ public function test401RedirectsToLogin() 'is_administrator' => false, ]); Auth::login($user); - $response = $this->get('/cases'); + $response = $this->get('/requests'); $response->assertStatus(200); $response->assertViewIs('requests.index'); Auth::logoutCurrentDevice(); - $response = $this->get('/cases'); + $response = $this->get('/requests'); //302 because we want to make sure they are being redirected $response->assertStatus(302); } diff --git a/tests/Feature/RequestTest.php b/tests/Feature/RequestTest.php index ee2138d301..f39421c5ec 100644 --- a/tests/Feature/RequestTest.php +++ b/tests/Feature/RequestTest.php @@ -45,7 +45,7 @@ class RequestTest extends TestCase public function testIndexRoute() { // get the URL - $response = $this->webCall('GET', '/cases'); + $response = $this->webCall('GET', '/requests'); $response->assertStatus(200); // check the correct view is called $response->assertViewIs('requests.index'); diff --git a/upgrades/2024_09_24_142322_update_request_permissions_group_name.php b/upgrades/2024_09_24_142322_update_request_permissions_group_name.php new file mode 100644 index 0000000000..0144917fbc --- /dev/null +++ b/upgrades/2024_09_24_142322_update_request_permissions_group_name.php @@ -0,0 +1,16 @@ +update(['group' => 'Cases and Requests']); + } +} diff --git a/webpack.mix.js b/webpack.mix.js index e664bf5044..89931e235e 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -121,7 +121,8 @@ mix .js("resources/js/requests/mobile.js", "public/js/requests/mobile.js") .js("resources/js/requests/show.js", "public/js/requests") .js("resources/js/requests/preview.js", "public/js/requests") - + .js("resources/jscomposition/cases/casesMain/main.js", "public/js/composition/cases/casesMain/main.js") + .js("resources/jscomposition/cases/casesDetail/edit.js", "public/js/composition/cases/casesDetail/edit.js") .js("resources/js/processes/translations/import.js", "public/js/processes/translations") .js("resources/js/processes-catalogue/index.js", "public/js/processes-catalogue/index.js") @@ -171,6 +172,9 @@ mix .sass("resources/sass/collapseDetails.scss", "public/css") .sass("resources/sass/app.scss", "public/css") .sass("resources/sass/admin/queues.scss", "public/css/admin") + .postCss("resources/sass/tailwind.css", "public/css", [ + require("tailwindcss"), + ]) .version(); mix.vue({ version: 2 });