diff --git a/ProcessMaker/Cache/AbstractCacheFactory.php b/ProcessMaker/Cache/AbstractCacheFactory.php
index b3779f3f8b..bfcf09c3c0 100644
--- a/ProcessMaker/Cache/AbstractCacheFactory.php
+++ b/ProcessMaker/Cache/AbstractCacheFactory.php
@@ -10,6 +10,7 @@
abstract class AbstractCacheFactory implements CacheFactoryInterface
{
protected static ?CacheInterface $testInstance = null;
+
protected static bool $storeMetrics = true;
/**
diff --git a/ProcessMaker/Filters/BaseFilter.php b/ProcessMaker/Filters/BaseFilter.php
index 247cb770d4..2c9acef720 100644
--- a/ProcessMaker/Filters/BaseFilter.php
+++ b/ProcessMaker/Filters/BaseFilter.php
@@ -25,6 +25,8 @@ abstract class BaseFilter
public const TYPE_FIELD = 'Field';
+ public const TYPE_STAGE = 'Stage';
+
public const TYPE_PROCESS = 'Process';
public const TYPE_PROCESS_NAME = 'ProcessName';
@@ -98,6 +100,8 @@ private function apply($query): void
{
if ($valueAliasMethod = $this->valueAliasMethod()) {
$this->valueAliasAdapter($valueAliasMethod, $query);
+ } elseif ($this->subjectType === self::TYPE_STAGE) {
+ $this->filterByStageId($query);
} elseif ($this->subjectType === self::TYPE_PROCESS) {
$this->filterByProcessId($query);
} elseif ($this->subjectValue === self::PROCESS_NAME_IN_REQUEST) {
@@ -124,6 +128,19 @@ private function apply($query): void
}
}
+ private function filterByStageId(Builder $query): void
+ {
+ if ($query->getModel() instanceof ProcessRequestToken) {
+ $query->whereIn('process_request_id', function ($query) {
+ $query->select('id')
+ ->from('process_requests')
+ ->whereIn('last_stage_id', (array) $this->value());
+ });
+ } else {
+ $query->whereIn('last_stage_id', (array) $this->value());
+ }
+ }
+
private function applyQueryBuilderMethod($query)
{
$method = $this->method();
diff --git a/ProcessMaker/Http/Controllers/Api/BookmarkController.php b/ProcessMaker/Http/Controllers/Api/BookmarkController.php
index 1dcaafed85..339109711b 100644
--- a/ProcessMaker/Http/Controllers/Api/BookmarkController.php
+++ b/ProcessMaker/Http/Controllers/Api/BookmarkController.php
@@ -43,6 +43,8 @@ public function index(Request $request)
// Get the launchpad configuration
$process->launchpad = ProcessLaunchpad::getLaunchpad($launchpad, $process->id);
$process->counts = $process->getCounts();
+ // Load Stages
+ $process->stagesSummary = $process->getStagesSummary($process->stages);
}
return new ProcessCollection($processes);
diff --git a/ProcessMaker/Http/Controllers/Api/CaseController.php b/ProcessMaker/Http/Controllers/Api/CaseController.php
new file mode 100644
index 0000000000..caaacfc14d
--- /dev/null
+++ b/ProcessMaker/Http/Controllers/Api/CaseController.php
@@ -0,0 +1,181 @@
+getSpecificCaseStages($case_number);
+
+ return response()->json($responseData);
+ }
+
+ $responseData = [
+ 'parentRequest' => [],
+ 'requestCount' => 0,
+ 'all_stages' => [],
+ 'current_stage' => [],
+ 'stages_per_case' => $this->getDefaultCaseStages(),
+ ];
+
+ return response()->json($responseData);
+ }
+
+ /**
+ * Get specific case stages information
+ * @param string $caseNumber The unique identifier of the case to retrieve stages for
+ * @return array
+ */
+ private function getSpecificCaseStages($caseNumber)
+ {
+ $allRequests = ProcessRequest::where('case_number', $caseNumber)->get();
+ // Check if any requests were found
+ if ($allRequests->isEmpty()) {
+ return $this->getDefaultCaseStages();
+ }
+ $parentRequest = null;
+ $requestCount = $allRequests->count();
+ // Search the parent request parent_request_id and load $request
+ foreach ($allRequests as $request) {
+ if (is_null($request->parent_request_id)) {
+ $parentRequest = $request;
+ break;
+ }
+ }
+
+ $stagesPerCase = $this->getStagesSummary($parentRequest);
+
+ return [
+ 'parentRequest' => [
+ 'id' => $parentRequest->id,
+ 'case_number' => $parentRequest->case_number,
+ 'status' => $parentRequest->status,
+ 'completed_at' => $parentRequest->completed_at,
+ ],
+ 'requestCount' => $requestCount,
+ 'all_stages' => [],
+ 'current_stage' => [],
+ 'stages_per_case' => $stagesPerCase,
+ ];
+ }
+
+ /**
+ * Get default case stages with status handling
+ *
+ * @param string|null $status The status to set for the stages
+ * @return array
+ */
+ private function getDefaultCaseStages($status = null)
+ {
+ return [
+ [
+ 'id' => 0,
+ 'name' => 'In Progress',
+ 'status' => $this->mapStatus($status, 'In Progress'),
+ 'completed_at' => '',
+ ],
+ [
+ 'id' => 0,
+ 'name' => 'Completed',
+ 'status' => $this->mapStatus($status, 'Completed'),
+ 'completed_at' => '',
+ ],
+ ];
+ }
+
+ /**
+ * Map the status for each stage based on the input status
+ *
+ * @param string|null $status The input status to map
+ * @param string $stageName The name of the stage ('In Progress' or 'Completed')
+ * @return string The mapped status
+ */
+ private function mapStatus($status, $stageName)
+ {
+ if ($status === 'COMPLETED') {
+ return 'Done';
+ }
+
+ if ($status === 'ACTIVE') {
+ return match ($stageName) {
+ 'In Progress' => 'In Progress',
+ 'Completed' => 'Pending',
+ default => 'Pending'
+ };
+ }
+
+ return 'Pending';
+ }
+
+ /**
+ * Get the stages summary based on the provided request.
+ *
+ * @param $requestId
+ * @return array An array of stage results, each containing the stage ID, name, status,
+ * and completion date.
+ */
+ private function getStagesSummary(ProcessRequest $request)
+ {
+ $requestId = $request->id;
+ $processId = $request->process_id;
+ $process = Process::where('id', $processId)->first();
+ if ($process && !empty($process->stages)) {
+ $allStages = $process->stages;
+ } else {
+ // Return the default stages if the process does not have
+ return $this->getDefaultCaseStages($request->status);
+ }
+
+ $allCurrentStages = ProcessRequestToken::where('process_request_id', $requestId)
+ ->select('stage_id', 'stage_name', 'status', 'completed_at')
+ ->get()
+ ->toArray();
+ if (empty($allCurrentStages)) {
+ // TO_DO: define what happen if the process does not have task, is a valid use case
+ }
+
+ // Helper to map status
+ $mapStatus = function ($status) {
+ if ($status === 'CLOSED') {
+ return 'Done';
+ } elseif ($status === 'ACTIVE') {
+ return 'In Progress';
+ } else {
+ return 'Pending';
+ }
+ };
+
+ $stageResult = [];
+ // Initialize stage counts with zero for all stages
+ foreach ($allStages as $stage) {
+ $stageData = [
+ 'id' => $stage['id'],
+ 'name' => $stage['name'],
+ 'status' => 'Pending',
+ 'completed_at' => '',
+ ];
+
+ foreach ($allCurrentStages as $task) {
+ if ($task['stage_id'] === $stage['id']) {
+ $stageData['status'] = $mapStatus($task['status']);
+ $stageData['completed_at'] = $task['completed_at'] ?? '';
+ break;
+ }
+ }
+
+ $stageResult[] = $stageData;
+ }
+
+ return $stageResult;
+ }
+}
diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php
index 0c223f70da..b9b14d3f11 100644
--- a/ProcessMaker/Http/Controllers/Api/ProcessController.php
+++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php
@@ -33,6 +33,7 @@
use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessLaunchpad;
use ProcessMaker\Models\ProcessPermission;
+use ProcessMaker\Models\ProcessVersion;
use ProcessMaker\Models\Screen;
use ProcessMaker\Models\Script;
use ProcessMaker\Models\Template;
@@ -49,11 +50,17 @@ class ProcessController extends Controller
{
use ProjectAssetTrait;
- const CAROUSEL_TYPES = [
+ public const CAROUSEL_TYPES = [
'IMAGE' => 'image',
'EMBED' => 'embed',
];
+ public const STAGES_STRUCTURE = [
+ 'id',
+ 'name',
+ 'order',
+ ];
+
/**
* A whitelist of attributes that should not be
* sanitized by our SanitizeInput middleware.
@@ -143,7 +150,7 @@ public function index(Request $request)
$modifiedCollection = $processes->map(function ($item) {
return [
'id' => $item['id'],
- 'events'=> $item['start_events']];
+ 'events' => $item['start_events']];
});
return new ApiCollection($modifiedCollection);
@@ -508,6 +515,14 @@ public function update(Request $request, Process $process)
$process->warnings = null;
}
+ // Validate stages
+ $stages = $request->input('stages', null);
+ if (!empty($stages)) {
+ if (!is_array($stages) && !$this->validateStagesStructure($stages)) {
+ return ['error' => 'Invalid stages structure. Each stage must have id, name, and order.'];
+ }
+ }
+
$process->fill($request->except('notifications', 'task_notifications', 'notification_settings', 'cancel_request', 'cancel_request_id', 'start_request_id', 'edit_data', 'edit_data_id', 'projects'));
if ($request->has('manager_id')) {
$process->manager_id = $request->input('manager_id', null);
@@ -585,6 +600,31 @@ public function update(Request $request, Process $process)
return new Resource($process->refresh());
}
+ /**
+ * Validate the structure of stages.
+ *
+ * @param array $stages
+ * @return bool
+ */
+ private function validateStagesStructure(array $stages): bool
+ {
+ foreach ($stages as $stage) {
+ // Check if all required keys are present
+ foreach (self::STAGES_STRUCTURE as $key) {
+ if (!array_key_exists($key, $stage)) {
+ return false; // Missing required key
+ }
+ }
+
+ // Additional validation for data types
+ if (!is_int($stage['id']) || !is_string($stage['name']) || !is_int($stage['order'])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public function updateBpmn(Request $request, Process $process)
{
$request->validate(Process::rules($process));
@@ -1911,4 +1951,389 @@ public function deleteEmbed(Request $request, Process $process)
$embedUrl->delete();
}
}
+
+ /**
+ * Get stages of a process
+ *
+ * @OA\Get(
+ * path="/processes/{process}/stages",
+ * summary="Get the list of stages for a process",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * in="path",
+ * required=true,
+ * description="ID of the process",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\Parameter(
+ * name="alternative",
+ * in="query",
+ * required=false,
+ * description="Alternative version (A or B)",
+ * @OA\Schema(type="string")
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="List of stages",
+ * @OA\JsonContent(
+ * type="array",
+ * @OA\Items(
+ * type="object",
+ * @OA\Property(property="id", type="integer"),
+ * @OA\Property(property="order", type="integer"),
+ * @OA\Property(property="name", type="string"),
+ * @OA\Property(property="selected", type="boolean")
+ * )
+ * )
+ * )
+ * )
+ */
+ public function getStages(Process $process, Request $request)
+ {
+ $alternative = $request->input('alternative', 'A');
+ $stages = [];
+ if ($alternative === 'B') {
+ // Get the alternative B version
+ $alternativeVersion = ProcessVersion::where('process_id', $process->id)
+ ->where('alternative', 'B')
+ ->first();
+
+ if ($alternativeVersion) {
+ $stages = $alternativeVersion->stages ?? [];
+ }
+ } else {
+ // Default to alternative A or get from main process
+ $stages = $process->stages ?? [];
+ }
+
+ return new ApiCollection($stages);
+ }
+
+ /**
+ * Save stages for a process
+ *
+ * @OA\Post(
+ * path="/processes/{process}/stages",
+ * summary="Save or update the list of stages for a process",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * in="path",
+ * required=true,
+ * description="ID of the process",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\Parameter(
+ * name="alternative",
+ * in="query",
+ * required=false,
+ * description="Alternative version (A or B)",
+ * @OA\Schema(type="string")
+ * ),
+ * @OA\RequestBody(
+ * required=true,
+ * @OA\JsonContent(
+ * type="array",
+ * @OA\Items(
+ * type="object",
+ * @OA\Property(property="id", type="integer"),
+ * @OA\Property(property="order", type="integer"),
+ * @OA\Property(property="label", type="string"),
+ * @OA\Property(property="selected", type="boolean")
+ * )
+ * )
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Updated stages",
+ * @OA\JsonContent(
+ * type="array",
+ * @OA\Items(
+ * type="object",
+ * @OA\Property(property="id", type="integer"),
+ * @OA\Property(property="order", type="integer"),
+ * @OA\Property(property="name", type="string"),
+ * @OA\Property(property="selected", type="boolean")
+ * )
+ * )
+ * )
+ * )
+ */
+ public function saveStages(Request $request, Process $process)
+ {
+ $alternative = $request->input('alternative', 'A');
+ $stages = $request->input('stages');
+
+ if ($alternative === 'B') {
+
+ // Get or create alternative B version
+ $alternativeVersion = ProcessVersion::where('process_id', $process->id)
+ ->where('alternative', 'B')
+ ->first();
+
+ // Save stages to alternative B version
+ $alternativeVersion->stages = $stages;
+ $alternativeVersion->save();
+
+ return new ApiCollection($alternativeVersion->stages);
+ } else {
+ // Save stages to main process (alternative A)
+ $process->stages = $stages;
+ $process->save();
+
+ return new ApiCollection($process->stages);
+ }
+ }
+
+ /**
+ * Get aggregation for a process
+ *
+ * @OA\Get(
+ * path="/processes/{process}/aggregation",
+ * summary="Get the aggregation configuration for a process",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * in="path",
+ * required=true,
+ * description="ID of the process",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Aggregation configuration",
+ * @OA\JsonContent(
+ * type="object",
+ * @OA\Property(property="aggregation", type="string", description="string containing var aggregation")
+ * )
+ * )
+ * )
+ */
+ public function getAggregation(Process $process)
+ {
+ return new ApiResource([
+ 'data' => $process->aggregation,
+ ]);
+ }
+
+ /**
+ * Save aggregation for a process
+ *
+ * @OA\Post(
+ * path="/processes/{process}/aggregation",
+ * summary="Save or update the aggregation field for a process",
+ * description="Updates the aggregation field of a process. If no aggregation is provided, defaults to 'amount'.",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * in="path",
+ * required=true,
+ * description="ID of the process",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\RequestBody(
+ * required=false,
+ * @OA\JsonContent(
+ * type="object",
+ * @OA\Property(
+ * property="aggregation",
+ * type="string",
+ * description="Field name to use for aggregation (defaults to 'amount' if not provided)",
+ * example="amount"
+ * )
+ * )
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Updated aggregation field",
+ * @OA\JsonContent(
+ * type="object",
+ * @OA\Property(
+ * property="data",
+ * type="string",
+ * description="The saved aggregation field value",
+ * example="amount"
+ * )
+ * )
+ * ),
+ * @OA\Response(
+ * response=404,
+ * description="Process not found"
+ * )
+ * )
+ */
+ public function saveAggregation(Request $request, Process $process)
+ {
+ $process->aggregation = $request->input('aggregation', 'amount');
+ $process->save();
+
+ return new ApiResource([
+ 'data' => $process->aggregation,
+ ]);
+ }
+
+ /**
+ * Get the stages configuration for a specific process.
+ *
+ * @OA\Get(
+ * path="/processes/{process}/stage-mapping",
+ * summary="Get process stages configuration",
+ * description="Retrieves and formats the stages configuration for a specific process, including total counts and individual stages.",
+ * operationId="getStageMapping",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * description="ID of the process",
+ * required=true,
+ * in="path",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Successful operation",
+ * @OA\JsonContent(
+ * type="object",
+ * @OA\Property(
+ * property="data",
+ * type="object",
+ * @OA\Property(
+ * property="total",
+ * type="object",
+ * @OA\Property(property="stage_id", type="number", example="0"),
+ * @OA\Property(property="stage_name", type="string", example="Total Cases"),
+ * @OA\Property(property="percentage", type="number", example=100),
+ * @OA\Property(property="percentage_format", type="string", example="100%"),
+ * @OA\Property(property="agregation_sum", type="number", example=50000),
+ * @OA\Property(property="agregation_count", type="number", example=150),
+ * ),
+ * @OA\Property(
+ * property="stages",
+ * type="array",
+ * @OA\Items(
+ * type="object",
+ * @OA\Property(property="stage_id", type="number", example="1"),
+ * @OA\Property(property="stage_name", type="string", example="In progress"),
+ * @OA\Property(property="percentage", type="number", nullable=true, example=60),
+ * @OA\Property(property="percentage_format", type="string", example="60%"),
+ * @OA\Property(property="agregation_sum", type="number", nullable=true, example=28678),
+ * @OA\Property(property="agregation_count", type="number", nullable=true, example=100),
+ * )
+ * )
+ * )
+ * )
+ * )
+ * )
+ */
+ public function getStageMapping(Process $process)
+ {
+ $formattedStages = Process::formatStages($process->id, $process->stages, $process->aggregation);
+
+ return response()->json(['data' => $formattedStages]);
+ }
+
+ /**
+ * Get the stages configuration for a specific process.
+ *
+ * @OA\Get(
+ * path="/api/processes/{process}/default-stages",
+ * summary="Get default process stages configuration",
+ * description="Retrieves and formats the default stages configuration for a process.",
+ * operationId="getDefaultStagesPerProcess",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * description="ID of the process",
+ * required=true,
+ * in="path",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Successful operation",
+ * @OA\JsonContent(
+ * type="array",
+ * @OA\Items(
+ * type="object",
+ * @OA\Property(property="stage_id", type="number", example="1"),
+ * @OA\Property(property="stage_name", type="string", example="In progress"),
+ * @OA\Property(property="percentage", type="number", nullable=true, example=60),
+ * @OA\Property(property="percentage_format", type="string", example="60%"),
+ * @OA\Property(property="agregation_sum", type="number", nullable=true, example=28678),
+ * @OA\Property(property="agregation_count", type="number", nullable=true, example=100),
+ * )
+ * )
+ * )
+ * )
+ */
+ public function getDefaultStagesPerProcess(Process $process)
+ {
+ $formattedStages = Process::formatStages($process->id, '', '');
+
+ return response()->json(['data' => $formattedStages]);
+ }
+
+ /**
+ * Get the metrics configuration for a specific process.
+ *
+ * @OA\Get(
+ * path="/api/processes/{process}/metrics",
+ * summary="Get process metrics configuration",
+ * description="Retrieves and formats the metrics configuration for a specific process. Supports different formats like 'student' or default metrics.",
+ * operationId="getMetricsPerProcess",
+ * tags={"Processes"},
+ * @OA\Parameter(
+ * name="process",
+ * description="ID of the process",
+ * required=true,
+ * in="path",
+ * @OA\Schema(type="integer")
+ * ),
+ * @OA\Parameter(
+ * name="format",
+ * description="Format type of the metrics (e.g., 'student')",
+ * required=false,
+ * in="query",
+ * @OA\Schema(
+ * type="string",
+ * enum={"student", "default"},
+ * default="student"
+ * )
+ * ),
+ * @OA\Response(
+ * response=200,
+ * description="Successful operation",
+ * @OA\JsonContent(
+ * type="array",
+ * @OA\Items(
+ * type="object",
+ * @OA\Property(property="id", type="integer", example=1),
+ * @OA\Property(property="metric_description", type="string", example="Max amount available"),
+ * @OA\Property(property="metric_count", type="integer", nullable=true, example=10),
+ * @OA\Property(property="metric_count_description", type="string", example="Across 10 applicants"),
+ * @OA\Property(property="metric_value", type="number", example=84000),
+ * @OA\Property(property="metric_value_unit", type="string", example="k")
+ * )
+ * )
+ * ),
+ * @OA\Response(
+ * response=400,
+ * description="Invalid format parameter",
+ * @OA\JsonContent(
+ * @OA\Property(property="error", type="string", example="Invalid format parameter")
+ * )
+ * )
+ * )
+ */
+ public function getMetricsPerProcess(Process $process, Request $request)
+ {
+ try {
+ $format = $request->query('format', 'student');
+ $formattedMetrics = Process::formatMetrics($process, $format);
+
+ return response()->json(['data' => $formattedMetrics]);
+ } catch (\InvalidArgumentException $e) {
+ return response()->json(['error' => $e->getMessage()], 400);
+ }
+ }
}
diff --git a/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php b/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php
index cf6ac094f9..72d62ccc95 100644
--- a/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php
+++ b/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php
@@ -56,6 +56,8 @@ public function getProcesses(Request $request)
$process->bookmark_id = Bookmark::getBookmarked($bookmark, $process->id, $user->id);
// Get the launchpad configuration
$process->launchpad = ProcessLaunchpad::getLaunchpad($launchpad, $process->id);
+ // Load Stages
+ $process->stagesSummary = $process->getStagesSummary($process->stages);
}
$process = $processes->map(function ($process) {
@@ -94,6 +96,7 @@ public function index(Request $request, Process $process)
->get()
->map(function ($process) use ($request) {
$process->counts = $process->getCounts();
+ $process->stagesSummary = $process->getStagesSummary($process->stages);
$process->bookmark_id = Bookmark::getBookmarked(true, $process->id, $request->user()->id);
return $process;
@@ -201,14 +204,14 @@ public function getProcessesMenu(Request $request)
$processes = Process::select('processes.*')
->notArchived()
->distinct()
- ->whereHas('requests', function($query) use ($userId) {
+ ->whereHas('requests', function ($query) use ($userId) {
$query->where('user_id', $userId)
- ->orWhereHas('tokens', function($query) use ($userId) {
+ ->orWhereHas('tokens', function ($query) use ($userId) {
$query->where('user_id', $userId);
});
})
- ->where('asset_type', NULL)
- ->where('package_key', '=', NULL)
+ ->where('asset_type', null)
+ ->where('package_key', '=', null)
->orderBy('processes.name', 'asc')
->get();
diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
index a183a22f9e..98a9293746 100644
--- a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
+++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php
@@ -4,6 +4,7 @@
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Routing\ResponseFactory;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
@@ -42,7 +43,6 @@
class ProcessRequestController extends Controller
{
use ProcessMapTrait;
-
const DOMAIN_CACHE_TIME = 86400;
/**
diff --git a/ProcessMaker/Http/Controllers/Api/UserTokenController.php b/ProcessMaker/Http/Controllers/Api/UserTokenController.php
index f3a853c5d7..70215fa19e 100644
--- a/ProcessMaker/Http/Controllers/Api/UserTokenController.php
+++ b/ProcessMaker/Http/Controllers/Api/UserTokenController.php
@@ -24,22 +24,22 @@ class UserTokenController extends Controller
/**
* The token repository implementation.
*
- * @var \Laravel\Passport\TokenRepository
+ * @var TokenRepository
*/
protected $tokenRepository;
/**
* The validation factory implementation.
*
- * @var \Illuminate\Contracts\Validation\Factory
+ * @var ValidationFactory
*/
protected $validation;
/**
* Create a controller instance.
*
- * @param \Laravel\Passport\TokenRepository $tokenRepository
- * @param \Illuminate\Contracts\Validation\Factory $validation
+ * @param TokenRepository $tokenRepository
+ * @param ValidationFactory $validation
* @return void
*/
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
@@ -109,7 +109,7 @@ public function index(Request $request, User $user)
/**
* Create a new personal access token for the user.
*
- * @param \Illuminate\Http\Request $request
+ * @param Request $request
* @return \Laravel\Passport\PersonalAccessTokenResult
*
* @OA\Post(
@@ -215,7 +215,7 @@ public function show(Request $request, User $user, $tokenId)
/**
* Delete the given token for a user
*
- * @param \Illuminate\Http\Request $request
+ * @param Request $request
* @param string $tokenId
* @return \Illuminate\Http\Response
*
diff --git a/ProcessMaker/Http/Controllers/CasesController.php b/ProcessMaker/Http/Controllers/CasesController.php
index a57d90f680..c6a0a94e44 100644
--- a/ProcessMaker/Http/Controllers/CasesController.php
+++ b/ProcessMaker/Http/Controllers/CasesController.php
@@ -84,6 +84,17 @@ public function show($case_number)
} else {
$request->summary_screen = $request->getSummaryScreen();
}
+ // Stage information
+ if ($request->status === 'COMPLETED') {
+ $currentStages = [
+ 'stage_name' => __('Completed'),
+ ];
+ $progressStage = 100.0; // 100% completed
+ } else {
+ $currentStages = $this->formatCurrentStage($request->last_stage_id, $request->last_stage_name);
+ $allStages = $this->getStagesByProcessId($request->process_id);
+ $progressStage = calculateProgressById($request->last_stage_id, $allStages);
+ }
// Load the screen configured in "Request Detail Screen"
$request->request_detail_screen = Screen::find($request->process->request_detail_screen_id);
// The user canCancel if has the processPermission and the case has only one request
@@ -109,6 +120,9 @@ public function show($case_number)
$modelerController = new ModelerController();
$pmBlockList = $modelerController->getPmBlockList();
+ // Is TCE Customization
+ $isTceCustomization = config('app.tce_customization_enable');
+
// Return the view
return view('cases.edit', compact(
'request',
@@ -122,7 +136,10 @@ public function show($case_number)
'managerModelerScripts',
'bpmn',
'inflightData',
- 'pmBlockList'
+ 'pmBlockList',
+ 'progressStage',
+ 'currentStages',
+ 'isTceCustomization',
));
}
@@ -171,4 +188,31 @@ public function summaryScreenTranslation(ProcessRequest $request): void
}
}
}
+
+ /**
+ * Get the current stages from a JSON string.
+ *
+ * This method decodes a JSON string representing stages into an associative array.
+ * If the input is null or not a valid JSON string, it returns an empty array.
+ *
+ * @param int|null $id The ID of the stage.
+ * @param string|null $name The name of the stage.
+ * @return array An associative array of stages if the JSON is valid;
+ * otherwise, an empty array.
+ */
+ public static function formatCurrentStage(?int $id, ?string $name): array
+ {
+ // Initialize currentStages as an empty array
+ $currentStages = [];
+
+ // Check if $id is not null and $name is a valid string
+ if (!is_null($id) && is_string($name)) {
+ $currentStages = [
+ 'stage_id' => $id,
+ 'stage_name' => $name,
+ ];
+ }
+
+ return $currentStages;
+ }
}
diff --git a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php
index b55c89698b..596c5821ed 100644
--- a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php
+++ b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php
@@ -10,6 +10,7 @@
use ProcessMaker\Http\Controllers\Controller;
use ProcessMaker\Managers\ScreenBuilderManager;
use ProcessMaker\Models\Bookmark;
+use ProcessMaker\Models\EnvironmentVariable;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessCategory;
use ProcessMaker\Models\ProcessLaunchpad;
@@ -53,7 +54,22 @@ public function index(Request $request, Process $process = null)
$userConfiguration = (new UserConfigurationController())->index()['ui_configuration'];
$defaultSavedSearch = $this->getDefaultSavedSearchId();
+ $defaultColumns = [];
- return view('processes-catalogue.index', compact('process', 'currentUser', 'manager', 'userConfiguration', 'defaultSavedSearch'));
+ $metricsApiEndpoint = '';
+ // If TCE customization is enabled, get the metrics API endpoint
+ if (config('app.tce_customization_enable') == 'true') {
+ $metricsApiEndpoint = EnvironmentVariable::getMetricsApiEndpoint();
+ }
+
+ return view('processes-catalogue.index', compact(
+ 'process',
+ 'currentUser',
+ 'manager',
+ 'userConfiguration',
+ 'defaultSavedSearch',
+ 'defaultColumns',
+ 'metricsApiEndpoint',
+ ));
}
}
diff --git a/ProcessMaker/Http/Controllers/TaskController.php b/ProcessMaker/Http/Controllers/TaskController.php
index 374a2201c7..d920c27dbc 100755
--- a/ProcessMaker/Http/Controllers/TaskController.php
+++ b/ProcessMaker/Http/Controllers/TaskController.php
@@ -19,6 +19,7 @@
use ProcessMaker\Managers\ScreenBuilderManager;
use ProcessMaker\Models\AnonymousUser;
use ProcessMaker\Models\Comment;
+use ProcessMaker\Models\EnvironmentVariable;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessAbeRequestToken;
use ProcessMaker\Models\ProcessRequestToken;
@@ -76,7 +77,31 @@ public function index()
$defaultSavedSearchId = $this->getDefaultSavedSearchId();
- return view('tasks.index', compact('title', 'userFilter', 'defaultColumns', 'isDefaultColumns', 'taskDraftsEnabled', 'userConfiguration', 'showOldTaskScreen', 'currentUser', 'selectedProcess', 'defaultSavedSearchId', 'manager'));
+ $metricsApiEndpoint = '';
+ // If TCE customization is enabled, get the metrics API endpoint
+ if (config('app.tce_customization_enable') == 'true') {
+ $metricsApiEndpoint = EnvironmentVariable::getMetricsApiEndpoint();
+ }
+
+ return view('tasks.index', compact(
+ // User and Authentication related
+ 'currentUser',
+ 'userFilter',
+ 'userConfiguration',
+ // Process and Task related
+ 'selectedProcess',
+ 'defaultColumns',
+ 'isDefaultColumns',
+ 'taskDraftsEnabled',
+ 'showOldTaskScreen',
+ // Search and Display
+ 'defaultSavedSearchId',
+ 'manager',
+ // General
+ 'title',
+ // Metrics API Endpoint
+ 'metricsApiEndpoint',
+ ));
}
public function edit(ProcessRequestToken $task, string $preview = '')
diff --git a/ProcessMaker/Http/Resources/V1_1/CaseResource.php b/ProcessMaker/Http/Resources/V1_1/CaseResource.php
index ba4d25dc14..fe775ae774 100644
--- a/ProcessMaker/Http/Resources/V1_1/CaseResource.php
+++ b/ProcessMaker/Http/Resources/V1_1/CaseResource.php
@@ -29,6 +29,9 @@ class CaseResource extends ApiResource
'participants',
'initiated_at',
'completed_at',
+ 'last_stage_id',
+ 'last_stage_name',
+ 'progress',
];
public function toArray($request): array
diff --git a/ProcessMaker/Models/CaseParticipated.php b/ProcessMaker/Models/CaseParticipated.php
index 6f1c6b6e8d..88304a6a68 100644
--- a/ProcessMaker/Models/CaseParticipated.php
+++ b/ProcessMaker/Models/CaseParticipated.php
@@ -30,6 +30,9 @@ class CaseParticipated extends ProcessMakerModel
'initiated_at',
'completed_at',
'keywords',
+ 'last_stage_id',
+ 'last_stage_name',
+ 'progress',
];
protected $casts = [
diff --git a/ProcessMaker/Models/CaseStarted.php b/ProcessMaker/Models/CaseStarted.php
index 5141599ab7..060e33f591 100644
--- a/ProcessMaker/Models/CaseStarted.php
+++ b/ProcessMaker/Models/CaseStarted.php
@@ -30,6 +30,9 @@ class CaseStarted extends ProcessMakerModel
'initiated_at',
'completed_at',
'keywords',
+ 'last_stage_id',
+ 'last_stage_name',
+ 'progress',
];
protected $casts = [
diff --git a/ProcessMaker/Models/EnvironmentVariable.php b/ProcessMaker/Models/EnvironmentVariable.php
index 2d872a11f9..97554fce2a 100644
--- a/ProcessMaker/Models/EnvironmentVariable.php
+++ b/ProcessMaker/Models/EnvironmentVariable.php
@@ -87,4 +87,15 @@ public static function messages()
'name.regex' => trans('environmentVariables.validation.name.invalid_variable_name'),
];
}
+
+ public static function getMetricsApiEndpoint()
+ {
+ $variable = self::where('name', 'METRICS_API_ENDPOINT')->first();
+
+ if ($variable) {
+ return $variable->value;
+ }
+
+ return '/api/1.0/processes/{process}/metrics';
+ }
}
diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php
index b1cae9afc3..1ddfe81e61 100644
--- a/ProcessMaker/Models/Process.php
+++ b/ProcessMaker/Models/Process.php
@@ -226,7 +226,7 @@ class Process extends ProcessMakerModel implements HasMedia, ProcessModelInterfa
'assigned',
'completed',
'due',
- 'default'
+ 'default',
];
protected $appends = [
@@ -241,6 +241,7 @@ class Process extends ProcessMakerModel implements HasMedia, ProcessModelInterfa
'signal_events' => 'array',
'conditional_events' => 'array',
'properties' => 'array',
+ 'stages' => 'array',
];
public static function boot()
@@ -1854,6 +1855,234 @@ public function hasAlternative()
return true;
}
+ /**
+ * Provides default stage data when no configuration exists.
+ *
+ * @param int $processId The process_id.
+ * @return array The default formatted array of stages.
+ */
+ protected static function getDefaultStagesData(int $processId): array
+ {
+ $totalCount = 0;
+ $activeCount = 0;
+ $completedCount = 0;
+ $activePercentage = 0;
+ $completedPercentage = 0;
+ $defaultCounts = self::getProcessDefaultStageCounts($processId);
+ if ($defaultCounts) {
+ $totalCount = $defaultCounts['total_count'] ?? 0;
+ $activeCount = $defaultCounts['active_count'] ?? 0;
+ $completedCount = $defaultCounts['completed_count'] ?? 0;
+ $activePercentage = ($totalCount > 0) ? (($activeCount / $totalCount) * 100) : 0;
+ $completedPercentage = ($totalCount > 0) ? (($completedCount / $totalCount) * 100) : 0;
+ }
+
+ return [
+ 'total' => [
+ 'stage_id' => 0,
+ 'stage_name' => 'Total Cases',
+ 'percentage' => 100,
+ 'percentage_format' => '100%',
+ 'agregation_sum' => 0,
+ 'agregation_count' => $totalCount,
+ ],
+ 'stages' => [
+ [
+ 'stage_id' => 'in_progress',
+ 'stage_name' => 'In progress',
+ 'percentage' => $activePercentage,
+ 'percentage_format' => $activePercentage . '%',
+ 'agregation_sum' => 0,
+ 'agregation_count' => $activeCount,
+ ],
+ [
+ 'stage_id' => 'completed',
+ 'stage_name' => 'Completed',
+ 'percentage' => $completedPercentage,
+ 'percentage_format' => $completedPercentage . '%',
+ 'agregation_sum' => 0,
+ 'agregation_count' => $completedCount,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Maps the stages configuration with the counts from the database.
+ *
+ * @param array $stagesConf The decoded stages configuration.
+ * @param array $stageCounts The counts of process requests by last_stage_id.
+ * @return array The formatted array of stages with counts.
+ */
+ protected static function mapStagesWithCounts(array $stagesConfig, array $stageCounts): array
+ {
+ $stages = collect($stagesConfig)->map(function ($stage, $index) use ($stageCounts) {
+ $stageId = $stage['id'] ?? 0;
+ $totalCount = $stageCounts['total_count'] ?? 0;
+ $stageCount = $stageCounts[$stageId]['count'] ?? 0;
+ $stageSum = $stageCounts[$stageId]['total_aggregation_sum'] ?? 0;
+ $stagePercentage = ($totalCount > 0) ? ($stageCount / $totalCount * 100) : 0;
+
+ return [
+ 'stage_id' => $stage['id'] ?? 0,
+ 'stage_name' => $stage['name'] ?? 'Unknown Stage ' . ($index + 1),
+ 'percentage' => $stagePercentage,
+ 'percentage_format' => $stagePercentage . '%',
+ 'agregation_sum' => $stageSum,
+ 'agregation_count' => $stageCount,
+ ];
+ })->toArray();
+
+ $totalCount = $stageCounts['total_count'] ?? 0;
+ $totalSum = collect($stages)->sum('agregation_sum') ?? 0;
+
+ return [
+ 'total' => [
+ 'stage_id' => 0,
+ 'stage_name' => 'Total Cases',
+ 'percentage' => 100,
+ 'percentage_format' => '100%',
+ 'agregation_sum' => $totalCount,
+ 'agregation_count' => $totalSum,
+ ],
+ 'stages' => $stages,
+ ];
+ }
+
+ protected static function getProcessDefaultStageCounts(int $processId): array
+ {
+ return ProcessRequest::where('process_id', $processId)
+ ->whereIn('status', ['ACTIVE', 'COMPLETED', 'ERROR', 'CANCELED'])
+ ->selectRaw("
+ COUNT(CASE WHEN status = 'ACTIVE' THEN 1 END) AS active_count,
+ COUNT(CASE WHEN status = 'ERROR' THEN 1 END) AS error_count,
+ COUNT(CASE WHEN status = 'CANCELED' THEN 1 END) AS cancel_count,
+ COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) AS completed_count,
+ COUNT(*) AS total_count
+ ")
+ ->first()
+ ->toArray();
+ }
+
+ protected static function getProcessStageCounts(int $processId, string $amountKey = 'amount'): array
+ {
+ $rawSql = sprintf('last_stage_id, COUNT(*) AS count, SUM(CAST(JSON_EXTRACT(data, \'$.%s\') AS DECIMAL(10, 2))) AS var_aggregation_sum', $amountKey);
+
+ $results = ProcessRequest::where('process_id', $processId)
+ ->groupBy('last_stage_id')
+ ->selectRaw($rawSql)
+ ->get()
+ ->toArray();
+
+ $stageData = [];
+ $totalCount = 0;
+ foreach ($results as $result) {
+ $stageData[$result['last_stage_id']] = [
+ 'count' => $result['count'],
+ 'total_aggregation_sum' => $result['var_aggregation_sum'],
+ ];
+ // We will consider only the request with stages related
+ if (!empty($result['last_stage_id'])) {
+ $totalCount += $result['count'];
+ }
+ }
+ $stageData['total_count'] = $totalCount;
+
+ return $stageData;
+ }
+
+ /**
+ * Formats the stages configuration for the API response.
+ *
+ * @param int $processId The process_id
+ * @param array $stages The stages configuration.
+ * @param string $varAggregation The aggregation var configuration.
+ * @return array The formatted array of stages.
+ */
+ public static function formatStages($processId, $stages = [], ?string $varAggregation = ''): array
+ {
+ if (empty($stages)) {
+ // If not exist stages we will return the default stages
+ return self::getDefaultStagesData($processId);
+ } else {
+ // Get the stages
+ $stageCounts = self::getProcessStageCounts($processId, $varAggregation);
+
+ return self::mapStagesWithCounts($stages, $stageCounts);
+ }
+ }
+
+ /**
+ * Formats the stages configuration for the API response.
+ *
+ * @param Process $process The process to get metrics for
+ * @param string|null $stagesJson The JSON string of the stages configuration.
+ * @return array The formatted array of stages.
+ */
+ public static function formatMetrics(self $process, $format = 'student'): array
+ {
+ // Get user authenticated
+ $user = Auth::user();
+ // Create a base query for process requests
+ $baseQuery = ProcessRequest::query()
+ ->where('process_id', $process->id)
+ ->forUser($user);
+ // Count the number of process requests in different states
+ $counts = [
+ 'started_me' => (clone $baseQuery)
+ ->startedMe($user->id)
+ ->notCompleted()
+ ->count(),
+ 'in_progress' => (clone $baseQuery)
+ ->inProgress()
+ ->count(),
+ 'completed' => (clone $baseQuery)
+ ->completed()
+ ->count(),
+ ];
+
+ $totalRequests = array_sum($counts);
+ $startedPercentage = $totalRequests > 0 ? round(($counts['started_me'] / $totalRequests) * 100) : 0;
+ $inProgressPercentage = $totalRequests > 0 ? round(($counts['in_progress'] / $totalRequests) * 100) : 0;
+ $completedPercentage = $totalRequests > 0 ? round(($counts['completed'] / $totalRequests) * 100) : 0;
+
+ switch ($format) {
+ case 'student':
+ case 'college':
+ $metrics = [
+ [
+ 'id' => 1,
+ 'metric_description' => sprintf('Cases Started by me (%d%%)', $startedPercentage),
+ 'metric_count' => 40,
+ 'metric_count_description' => 'Applications started by me',
+ 'metric_value' => $counts['started_me'],
+ 'metric_value_unit' => '',
+ ],
+ [
+ 'id' => 2,
+ 'metric_description' => sprintf('Cases In progress (%d%%)', $inProgressPercentage),
+ 'metric_count' => 0,
+ 'metric_count_description' => 'Applications are currently being reviewed',
+ 'metric_value' => $counts['in_progress'],
+ 'metric_value_unit' => '',
+ ],
+ [
+ 'id' => 3,
+ 'metric_description' => sprintf('Completed Cases (%d%%)', $completedPercentage),
+ 'metric_count' => 0,
+ 'metric_count_description' => 'Applications have been processed',
+ 'metric_value' => $counts['completed'],
+ 'metric_value_unit' => '',
+ ],
+ ];
+ break;
+ default:
+ // Default format for metrics
+ }
+
+ return $metrics;
+ }
+
public function scopeOrderByRecentRequests($query)
{
return $query->orderByDesc(
diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php
index dbfb962449..0f052e1819 100644
--- a/ProcessMaker/Models/ProcessRequestToken.php
+++ b/ProcessMaker/Models/ProcessRequestToken.php
@@ -4,6 +4,7 @@
use Carbon\Carbon;
use DB;
+use DOMXPath;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Arr;
@@ -21,6 +22,7 @@
use ProcessMaker\Nayra\Contracts\Bpmn\MultiInstanceLoopCharacteristicsInterface;
use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
use ProcessMaker\Nayra\Managers\WorkflowManagerDefault;
+use ProcessMaker\Nayra\Storage\BpmnDocument;
use ProcessMaker\Notifications\ActivityActivatedNotification;
use ProcessMaker\Notifications\TaskReassignmentNotification;
use ProcessMaker\Query\Expression;
@@ -1045,6 +1047,53 @@ function ($key) use ($allowed) {
},
ARRAY_FILTER_USE_KEY
);
+
+ $this->setStagePropertiesInRecord();
+ }
+
+ /**
+ * Extracts and assigns stage properties from the BPMN sequence flow configuration.
+ *
+ * This method loads the BPMN XML from the process associated with the current instance
+ * and uses XPath to locate any sequenceFlow elements that target the current element ID.
+ * It iterates over the matching flows and attempts to extract the `pm:config` attribute
+ * from each, which is expected to be a JSON-encoded string.
+ *
+ * If a configuration is found with a defined `stage.id`, the method assigns the `stage_id`
+ * and `stage_name` properties of the current object accordingly.
+ *
+ * Typical use case: setting the stage metadata (ID and name) during execution flow
+ * resolution in a BPMN process.
+ *
+ * @return void
+ */
+ public function setStagePropertiesInRecord()
+ {
+ try {
+ $instance = $this->getInstance();
+ $bpmnDocument = new BpmnDocument();
+ $bpmnDocument->loadXml($instance->process->bpmn);
+ $xpath = new DOMXPath($bpmnDocument);
+ $xpath->registerNamespace('bpmn', BpmnDocument::BPMN_MODEL);
+ $sequenceFlows = $xpath->query("//bpmn:sequenceFlow[@targetRef='$this->element_id']");
+ $config = null;
+ foreach ($sequenceFlows as $flow) {
+ $id = $flow->getAttribute('id');
+ $incomingFlow = $bpmnDocument->findElementById($id);
+ if (!$incomingFlow) {
+ continue;
+ }
+ $pmConfig = $incomingFlow->getAttribute('pm:config');
+ $config = json_decode($pmConfig ?? '');
+ if (isset($config->stage) && isset($config->stage->id)) {
+ break;
+ }
+ }
+ $this->stage_id = isset($config->stage) && isset($config->stage->id) ? $config->stage->id : null;
+ $this->stage_name = isset($config->stage) && isset($config->stage->name) ? $config->stage->name : null;
+ } catch (Exception $e) {
+ Log::debug($e->getMessage());
+ }
}
public function loadTokenProperties()
diff --git a/ProcessMaker/Models/ProcessVersion.php b/ProcessMaker/Models/ProcessVersion.php
index eafa4531d4..1d5265669d 100644
--- a/ProcessMaker/Models/ProcessVersion.php
+++ b/ProcessMaker/Models/ProcessVersion.php
@@ -49,6 +49,7 @@ class ProcessVersion extends ProcessMakerModel implements ProcessModelInterface
'signal_events' => 'array',
'conditional_events' => 'array',
'properties' => 'array',
+ 'stages' => 'array',
];
/**
diff --git a/ProcessMaker/Repositories/CaseApiRepository.php b/ProcessMaker/Repositories/CaseApiRepository.php
index 2440e77505..4d4918217f 100644
--- a/ProcessMaker/Repositories/CaseApiRepository.php
+++ b/ProcessMaker/Repositories/CaseApiRepository.php
@@ -32,6 +32,9 @@ class CaseApiRepository implements CaseApiRepositoryInterface
'participants',
'initiated_at',
'completed_at',
+ 'last_stage_id',
+ 'last_stage_name',
+ 'progress'
];
protected $sortableFields = [
@@ -66,7 +69,7 @@ class CaseApiRepository implements CaseApiRepositoryInterface
'updated_at',
];
- const DEFAULT_SORT_DIRECTION = 'asc';
+ public const DEFAULT_SORT_DIRECTION = 'asc';
/**
* Get all cases
diff --git a/ProcessMaker/Repositories/CaseParticipatedRepository.php b/ProcessMaker/Repositories/CaseParticipatedRepository.php
index 48a3b19f77..7a27637989 100644
--- a/ProcessMaker/Repositories/CaseParticipatedRepository.php
+++ b/ProcessMaker/Repositories/CaseParticipatedRepository.php
@@ -4,6 +4,7 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
+use ProcessMaker\Constants\CaseStatusConstants;
use ProcessMaker\Models\CaseParticipated;
use ProcessMaker\Models\CaseStarted;
@@ -66,6 +67,11 @@ public function update(CaseStarted $case): void
*/
private function mapCaseToArray(CaseStarted $case, int $userId = null): array
{
+ // Define the case status if is not set the stage
+ if (is_null($case->last_stage_id)) {
+ $case->last_stage_name = $case->case_status;
+ $case->progress = 50;
+ }
$data = [
'case_number' => $case->case_number,
'case_title' => $case->case_title,
@@ -79,6 +85,9 @@ private function mapCaseToArray(CaseStarted $case, int $userId = null): array
'initiated_at' => $case->initiated_at,
'completed_at' => $case->completed_at,
'keywords' => $case->keywords,
+ 'last_stage_id' => $case->last_stage_id,
+ 'last_stage_name' => $case->last_stage_name,
+ 'progress' => $case->progress ?? 0,
];
if ($userId !== null) {
diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseRepository.php
index 87755b6aba..ce21d63723 100644
--- a/ProcessMaker/Repositories/CaseRepository.php
+++ b/ProcessMaker/Repositories/CaseRepository.php
@@ -53,6 +53,11 @@ public function create(ExecutionInstanceInterface $instance): void
$processData = CaseUtils::extractData($instance->process, 'PROCESS');
$requestData = CaseUtils::extractData($instance, 'REQUEST');
$dataKeywords = CaseUtils::extractData($instance, 'KEYWORD');
+ // Check the case status
+ if (is_null($instance->last_stage_id)) {
+ $instance->last_stage_name = $instance->case_status;
+ $instance->progress = 50;
+ }
CaseStarted::create([
'case_number' => $instance->case_number,
@@ -68,6 +73,9 @@ public function create(ExecutionInstanceInterface $instance): void
'initiated_at' => $instance->initiated_at,
'completed_at' => null,
'keywords' => CaseUtils::getKeywords($dataKeywords),
+ 'last_stage_id' => $instance->last_stage_id,
+ 'last_stage_name' => $instance->last_stage_name,
+ 'progress' => 0,
]);
} catch (\Exception $e) {
Log::error('CaseException: ' . $e->getMessage());
@@ -98,6 +106,9 @@ public function update(ExecutionInstanceInterface $instance, TokenInterface $tok
$this->case->request_tokens = CaseUtils::storeRequestTokens($this->case->request_tokens, $token->getKey());
$this->case->tasks = CaseUtils::storeTasks($this->case->tasks, $taskData);
$this->case->keywords = CaseUtils::getKeywords($dataKeywords);
+ $this->case->last_stage_id = $token->stage_id;
+ $this->case->last_stage_name = $token->stage_name;
+ $this->case->progress = calculateProgressById($token->stage_id, $instance?->process?->stages);
$this->updateParticipants($token);
@@ -134,6 +145,8 @@ public function updateStatus(ExecutionInstanceInterface $instance): void
if (in_array($caseStatus, [CaseStatusConstants::COMPLETED, CaseStatusConstants::CANCELED])) {
$data['completed_at'] = $instance->completed_at;
+ $data['last_stage_name'] = $caseStatus;
+ $data['progress'] = 100;
}
// Update the case started and case participated
diff --git a/ProcessMaker/Repositories/TokenRepository.php b/ProcessMaker/Repositories/TokenRepository.php
index e98a967622..a5880642e7 100644
--- a/ProcessMaker/Repositories/TokenRepository.php
+++ b/ProcessMaker/Repositories/TokenRepository.php
@@ -169,6 +169,9 @@ public function persistActivityActivated(ActivityInterface $activity, TokenInter
$token->saveOrFail();
$token->setId($token->getKey());
$request = $token->getInstance();
+ $request->last_stage_id = $token->stage_id;
+ $request->last_stage_name = $token->stage_name;
+ $request->progress = calculateProgressById($token->stage_id, $request?->process?->stages);
$request->notifyProcessUpdated('ACTIVITY_ACTIVATED', $token);
CaseUpdate::dispatchSync($request, $token);
@@ -287,10 +290,6 @@ public function persistStartEventTriggered(StartEventInterface $startEvent, Coll
$request->notifyProcessUpdated('START_EVENT_TRIGGERED', $token);
}
- private function assignTaskUser(ActivityInterface $activity, TokenInterface $token, Instance $instance)
- {
- }
-
/**
* Persists instance and token data when a token within an activity change to error state
*
diff --git a/ProcessMaker/Traits/ProcessMapTrait.php b/ProcessMaker/Traits/ProcessMapTrait.php
index 2b9d0c3a42..8bdfaa10c2 100644
--- a/ProcessMaker/Traits/ProcessMapTrait.php
+++ b/ProcessMaker/Traits/ProcessMapTrait.php
@@ -4,6 +4,7 @@
use Illuminate\Support\Collection;
use ProcessMaker\Bpmn\Process;
+use ProcessMaker\Models\Process as ModelsProcess;
use ProcessMaker\Models\ProcessRequest;
use SimpleXMLElement;
@@ -158,4 +159,28 @@ private function loadProcessMap(ProcessRequest $request): array
'requestId' => $request->id,
];
}
+
+ /**
+ * Get the stages for a specific process ID.
+ *
+ * @param int $processId
+ * @return array|null
+ */
+ public static function getStagesByProcessId(int $processId): ?array
+ {
+ // Retrieve the process by ID
+ $process = ModelsProcess::find($processId);
+
+ // If the process exists, return the stages
+ if ($process) {
+ $stages = $process->stages;
+
+ if (!is_null($stages)) {
+ return $stages;
+ }
+ }
+
+ // Return empty array if the process does not exist or has no stages
+ return [];
+ }
}
diff --git a/ProcessMaker/Traits/ProcessTrait.php b/ProcessMaker/Traits/ProcessTrait.php
index fd5cf1f9db..0bd2f4b7f4 100644
--- a/ProcessMaker/Traits/ProcessTrait.php
+++ b/ProcessMaker/Traits/ProcessTrait.php
@@ -218,4 +218,65 @@ public function getCounts()
'total' => $completed + $in_progress,
];
}
+
+ /**
+ * Get the count of stages per request.
+ *
+ * This method retrieves the count of stages based on the last_stage_id for each request.
+ * If a JSON string of stages is provided, it decodes it; otherwise, it fetches all stages
+ * from the database. The method returns an array containing each stage's ID, name, and count.
+ *
+ * @param string|null $stages A JSON string representing stages. If provided, it will be decoded;
+ * if null, all stages will be fetched from the database.
+ * @return array An array of associative arrays, each containing:
+ * - 'id': The ID of the stage.
+ * - 'name': The name of the stage.
+ * - 'count': The count of occurrences of the stage based on last_stage_id.
+ */
+ public function getStagesSummary($stages = null)
+ {
+ if (!empty($stages)) {
+ $allStages = $stages;
+ } else {
+ return [];
+ }
+
+ // Assuming 'stages' is a relationship defined in the model
+ $result = $this->requests()
+ ->selectRaw('last_stage_id, count(*) as count')
+ ->groupBy('last_stage_id')
+ ->get();
+
+ // Prepare an array to hold the counts for each stage
+ $stageCounts = [];
+ // Initialize stage counts with zero for all stages
+ foreach ($allStages as $stage) {
+ $stageCounts[] = [
+ 'id' => $stage['id'] ?? 0,
+ 'name' => $stage['name'] ?? 'Unknow Stage',
+ 'count' => 0, // Initialize count to 0 for each stage
+ 'percentage' => 0, // Initialize percentaje to 0 for each stage
+ ];
+ }
+
+ foreach ($result as $stage) {
+ foreach ($stageCounts as $key => &$countData) {
+ if ($countData['id'] == $stage->last_stage_id) {
+ $countData['count'] = $stage->count; // Update the count
+ }
+ }
+ }
+
+ // Calculate the total count of all stages
+ $totalCount = array_sum(array_column($stageCounts, 'count'));
+
+ // Calculate the percentage for each stage
+ foreach ($stageCounts as &$countData) {
+ if ($totalCount > 0) {
+ $countData['percentage'] = ($countData['count'] / $totalCount) * 100;
+ }
+ }
+
+ return $stageCounts;
+ }
}
diff --git a/config/app.php b/config/app.php
index dba8da36cd..f94062de38 100644
--- a/config/app.php
+++ b/config/app.php
@@ -280,6 +280,9 @@
'custom_executors' => env('CUSTOM_EXECUTORS', false),
+ // Enable or disable TCE customization feature
+ 'tce_customization_enable' => env('TCE_CUSTOMIZATION_ENABLED', false),
+
'prometheus_namespace' => env('PROMETHEUS_NAMESPACE', strtolower(preg_replace('/[^a-zA-Z0-9_]+/', '_', env('APP_NAME', 'processmaker')))),
'server_timing' => [
diff --git a/database/migrations/2025_04_23_162421_add_stages_to_processes_and_process_versions.php b/database/migrations/2025_04_23_162421_add_stages_to_processes_and_process_versions.php
new file mode 100644
index 0000000000..84cf210cfb
--- /dev/null
+++ b/database/migrations/2025_04_23_162421_add_stages_to_processes_and_process_versions.php
@@ -0,0 +1,35 @@
+json('stages')->nullable();
+ });
+ Schema::table('process_versions', function (Blueprint $table) {
+ $table->json('stages')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('processes', function (Blueprint $table) {
+ $table->dropColumn('stages');
+ });
+
+ Schema::table('process_versions', function (Blueprint $table) {
+ $table->dropColumn('stages');
+ });
+ }
+};
diff --git a/database/migrations/2025_04_23_183734_add_stage_id_and_stage_name_to_process_request_tokens.php b/database/migrations/2025_04_23_183734_add_stage_id_and_stage_name_to_process_request_tokens.php
new file mode 100644
index 0000000000..91dcfdf4e6
--- /dev/null
+++ b/database/migrations/2025_04_23_183734_add_stage_id_and_stage_name_to_process_request_tokens.php
@@ -0,0 +1,38 @@
+integer('stage_id')->nullable();
+ // Add a 'stage_name' column to store the name of the current stage
+ // This will facilitate the identification of the stage
+ $table->string('stage_name')->nullable();
+ // This column will be used to display the percentage of advancement of the token through the stages.
+ $table->float('progress')->default(0)->nullable();
+ $table->decimal('amount', 10, 2)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('process_request_tokens', function (Blueprint $table) {
+ $table->dropColumn('stage_id');
+ $table->dropColumn('stage_name');
+ $table->dropColumn('progress');
+ $table->dropColumn('amount');
+ });
+ }
+};
diff --git a/database/migrations/2025_04_23_183839_add_stage_id_and_stage_name_to_process_request.php b/database/migrations/2025_04_23_183839_add_stage_id_and_stage_name_to_process_request.php
new file mode 100644
index 0000000000..abc2092d4f
--- /dev/null
+++ b/database/migrations/2025_04_23_183839_add_stage_id_and_stage_name_to_process_request.php
@@ -0,0 +1,33 @@
+integer('last_stage_id')->nullable();
+ $table->string('last_stage_name')->nullable();
+ // This column will be used to display the percentage of advancement of the request through the stages.
+ $table->float('progress')->default(0)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('process_requests', function (Blueprint $table) {
+ $table->dropColumn('last_stage_id');
+ $table->dropColumn('last_stage_name');
+ $table->dropColumn('progress');
+ });
+ }
+};
diff --git a/database/migrations/2025_04_23_184251_add_stage_id_and_stage_name_to_cases_participated.php b/database/migrations/2025_04_23_184251_add_stage_id_and_stage_name_to_cases_participated.php
new file mode 100644
index 0000000000..a054719cfd
--- /dev/null
+++ b/database/migrations/2025_04_23_184251_add_stage_id_and_stage_name_to_cases_participated.php
@@ -0,0 +1,33 @@
+integer('last_stage_id')->nullable();
+ $table->string('last_stage_name')->nullable();
+ // This column will be used to display the percentage of advancement of the case through the stages.
+ $table->float('progress')->default(0)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('cases_participated', function (Blueprint $table) {
+ $table->dropColumn('last_stage_id');
+ $table->dropColumn('last_stage_name');
+ $table->dropColumn('progress');
+ });
+ }
+};
diff --git a/database/migrations/2025_04_23_184327_add_stage_id_and_stage_name_to_cases_started.php b/database/migrations/2025_04_23_184327_add_stage_id_and_stage_name_to_cases_started.php
new file mode 100644
index 0000000000..1788c9d0b4
--- /dev/null
+++ b/database/migrations/2025_04_23_184327_add_stage_id_and_stage_name_to_cases_started.php
@@ -0,0 +1,33 @@
+integer('last_stage_id')->nullable();
+ $table->string('last_stage_name')->nullable();
+ // This column will be used to display the percentage of advancement of the case through the stages.
+ $table->float('progress')->default(0)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('cases_started', function (Blueprint $table) {
+ $table->dropColumn('last_stage_id');
+ $table->dropColumn('last_stage_name');
+ $table->dropColumn('progress');
+ });
+ }
+};
diff --git a/database/migrations/2025_04_25_222214_add_stages_table.php b/database/migrations/2025_04_25_222214_add_stages_table.php
new file mode 100644
index 0000000000..66f972cbfb
--- /dev/null
+++ b/database/migrations/2025_04_25_222214_add_stages_table.php
@@ -0,0 +1,33 @@
+id(); // Auto-incrementing ID for the stage
+ $table->unsignedInteger('process_id')->nullable(); // Foreign key for the process
+ $table->integer('process_version_id')->nullable();
+ $table->string('stage_name'); // Name of the stage
+ $table->decimal('total_amount', 10, 2)->default(0); // Total amount accumulated for the stage
+ $table->timestamps(); // Created at and updated at timestamps
+
+ // Foreign key constraint (assuming you have a processes table)
+ $table->foreign('process_id')->references('id')->on('processes')->onDelete('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('stages');
+ }
+};
diff --git a/database/migrations/2025_05_19_205653_add_index_stage_id_for_requests_cases_and_tokens.php b/database/migrations/2025_05_19_205653_add_index_stage_id_for_requests_cases_and_tokens.php
new file mode 100644
index 0000000000..ac6757ab55
--- /dev/null
+++ b/database/migrations/2025_05_19_205653_add_index_stage_id_for_requests_cases_and_tokens.php
@@ -0,0 +1,45 @@
+index('stage_id');
+ });
+ Schema::table('process_requests', function (Blueprint $table) {
+ $table->index('last_stage_id');
+ });
+ Schema::table('cases_participated', function (Blueprint $table) {
+ $table->index('last_stage_id');
+ });
+ Schema::table('cases_started', function (Blueprint $table) {
+ $table->index('last_stage_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('process_request_tokens', function (Blueprint $table) {
+ $table->dropIndex(['stage_id']);
+ });
+ Schema::table('process_requests', function (Blueprint $table) {
+ $table->dropIndex(['last_stage_id']);
+ });
+ Schema::table('cases_participated', function (Blueprint $table) {
+ $table->dropIndex(['last_stage_id']);
+ });
+ Schema::table('cases_started', function (Blueprint $table) {
+ $table->dropIndex(['last_stage_id']);
+ });
+ }
+};
diff --git a/database/migrations/2025_05_20_145423_add_aggregation_var_to_process_and_process_versions.php b/database/migrations/2025_05_20_145423_add_aggregation_var_to_process_and_process_versions.php
new file mode 100644
index 0000000000..70c91266bc
--- /dev/null
+++ b/database/migrations/2025_05_20_145423_add_aggregation_var_to_process_and_process_versions.php
@@ -0,0 +1,33 @@
+string('aggregation')->default('amount')->nullable();
+ });
+ Schema::table('process_versions', function (Blueprint $table) {
+ $table->string('aggregation')->default('amount')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('processes', function (Blueprint $table) {
+ $table->dropColumn('aggregation');
+ });
+ Schema::table('process_versions', function (Blueprint $table) {
+ $table->dropColumn('aggregation');
+ });
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index 4109049cd6..d99cbf8465 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -30,6 +30,7 @@ public function run()
SignalSeeder::class,
SettingsMenusSeeder::class,
ScreenEmailSeeder::class,
+ MetricsApiEnvironmentVariableSeeder::class,
]);
$this->callPluginSeeders();
}
diff --git a/database/seeders/MetricsApiEnvironmentVariableSeeder.php b/database/seeders/MetricsApiEnvironmentVariableSeeder.php
new file mode 100644
index 0000000000..fbc7cf44be
--- /dev/null
+++ b/database/seeders/MetricsApiEnvironmentVariableSeeder.php
@@ -0,0 +1,27 @@
+ 'METRICS_API_ENDPOINT',
+ ],
+ [
+ 'description' => 'API endpoint for retrieving process metrics example: /api/1.0/package-plg/processes/{process}/metrics',
+ 'value' => '/api/1.0/processes/{process}/metrics',
+ ]
+ );
+ }
+}
diff --git a/devhub/pm-font/svg/check-circle-blue.svg b/devhub/pm-font/svg/check-circle-blue.svg
new file mode 100644
index 0000000000..9bb8c343c3
--- /dev/null
+++ b/devhub/pm-font/svg/check-circle-blue.svg
@@ -0,0 +1,3 @@
+
diff --git a/devhub/pm-font/svg/pen-edit.svg b/devhub/pm-font/svg/pen-edit.svg
new file mode 100644
index 0000000000..1d8680feb6
--- /dev/null
+++ b/devhub/pm-font/svg/pen-edit.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/devhub/pm-font/svg/trash-blue.svg b/devhub/pm-font/svg/trash-blue.svg
new file mode 100644
index 0000000000..9a078b0905
--- /dev/null
+++ b/devhub/pm-font/svg/trash-blue.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/helpers.php b/helpers.php
index 6a284aa690..203e2322c8 100644
--- a/helpers.php
+++ b/helpers.php
@@ -279,6 +279,52 @@ function shouldShow($element)
}
}
+if (!function_exists('calculateProgressById')) {
+ /**
+ * Calculates the progress percentage of a stage based on its order
+ * within the total number of stages.
+ *
+ * This function searches for the stage with the given ID in the provided list
+ * of stages. Each stage must contain at least 'id' and 'order' keys.
+ * If the stage is found, it calculates the progress as:
+ * (stage_order / total_stages) * 100
+ *
+ * The result is rounded to two decimal places.
+ *
+ * If the list of stages is `null` or empty, or if the stage with the given ID
+ * is not found, the function returns 0.0.
+ *
+ * @param int|null $id The ID of the stage to calculate progress for.
+ * @param array|null $stages An array of stages, where each stage is an associative array
+ * containing at least the keys 'id' and 'order'.
+ * This parameter can be null.
+ *
+ * @return float The progress percentage (0.0 to 100.0), rounded to two decimal places.
+ */
+ function calculateProgressById(int|null $id, array|null $stages = [])
+ {
+ if (empty($stages)) {
+ return 0.0;
+ }
+ $totalStages = count($stages);
+ // Consider the completed stage as the last one
+ $totalStages++;
+ $currentStageOrder = 0;
+ foreach ($stages as $stage) {
+ if ($stage['id'] === $id) {
+ $currentStageOrder = $stage['order'];
+ break;
+ }
+ }
+ if ($currentStageOrder === 0) {
+ return 0.0;
+ }
+ $progress = ($currentStageOrder / $totalStages) * 100;
+
+ return round($progress, 2);
+ }
+}
+
if (!function_exists('validateURL')) {
/**
* Validate the URL
diff --git a/resources/fonts/pm-font/index.html b/resources/fonts/pm-font/index.html
index c4e368bce7..90ee73e5e7 100644
--- a/resources/fonts/pm-font/index.html
+++ b/resources/fonts/pm-font/index.html
@@ -103,7 +103,7 @@
- add-outlined
arrow-left
box-arrow-up-right
bpmn-action-by-email
bpmn-data-connector
bpmn-data-object
bpmn-data-store
bpmn-docusign
bpmn-end-event
bpmn-flowgenie
bpmn-gateway
bpmn-generic-gateway
bpmn-idp
bpmn-intermediate-event
bpmn-pool
bpmn-send-email
bpmn-start-event
bpmn-task
bpmn-text-annotation
brush-icon
close
cloud-download-outline
connector-outline
copy-outline
copy
desktop
edit-outline
expand
eye
fields-icon
flowgenie-outline
folder-outline
fullscreen
github
inbox
layout-icon
link-icon
map
minimize
mobile
pdf
play-outline
plus-thin
plus
pm-block
remove-outlined
screen-outline
script-outline
slack-notification
slack
slideshow
table
tachometer-alt-average
trash
unlink
update-outline
+ add-outlined
arrow-left
box-arrow-up-right
bpmn-action-by-email
bpmn-data-connector
bpmn-data-object
bpmn-data-store
bpmn-docusign
bpmn-end-event
bpmn-flowgenie
bpmn-gateway
bpmn-generic-gateway
bpmn-idp
bpmn-intermediate-event
bpmn-pool
bpmn-send-email
bpmn-start-event
bpmn-task
bpmn-text-annotation
brush-icon
check-circle-blue
close
cloud-download-outline
connector-outline
copy-outline
copy
desktop
edit-outline
expand
eye
fields-icon
flowgenie-outline
folder-outline
fullscreen
github
inbox
layout-icon
link-icon
map
minimize
mobile
pdf
pen-edit
play-outline
plus-thin
plus
pm-block
remove-outlined
screen-outline
script-outline
slack-notification
slack
slideshow
table
tachometer-alt-average
trash-blue
trash
unlink
update-outline
diff --git a/resources/fonts/pm-font/processmaker-font.css b/resources/fonts/pm-font/processmaker-font.css
index 3e0fee6584..c716cce24f 100644
--- a/resources/fonts/pm-font/processmaker-font.css
+++ b/resources/fonts/pm-font/processmaker-font.css
@@ -1,11 +1,11 @@
@font-face {
font-family: "processmaker-font";
- src: url('processmaker-font.eot?t=1743024460935'); /* IE9*/
- src: url('processmaker-font.eot?t=1743024460935#iefix') format('embedded-opentype'), /* IE6-IE8 */
- url("processmaker-font.woff2?t=1743024460935") format("woff2"),
- url("processmaker-font.woff?t=1743024460935") format("woff"),
- url('processmaker-font.ttf?t=1743024460935') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
- url('processmaker-font.svg?t=1743024460935#processmaker-font') format('svg'); /* iOS 4.1- */
+ src: url('processmaker-font.eot?t=1747081178345'); /* IE9*/
+ src: url('processmaker-font.eot?t=1747081178345#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ url("processmaker-font.woff2?t=1747081178345") format("woff2"),
+ url("processmaker-font.woff?t=1747081178345") format("woff"),
+ url('processmaker-font.ttf?t=1747081178345') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
+ url('processmaker-font.svg?t=1747081178345#processmaker-font') format('svg'); /* iOS 4.1- */
}
[class^="fp-"], [class*=" fp-"] {
@@ -36,40 +36,43 @@
.fp-bpmn-task:before { content: "\ea12"; }
.fp-bpmn-text-annotation:before { content: "\ea13"; }
.fp-brush-icon:before { content: "\ea14"; }
-.fp-close:before { content: "\ea15"; }
-.fp-cloud-download-outline:before { content: "\ea16"; }
-.fp-connector-outline:before { content: "\ea17"; }
-.fp-copy-outline:before { content: "\ea18"; }
-.fp-copy:before { content: "\ea19"; }
-.fp-desktop:before { content: "\ea1a"; }
-.fp-edit-outline:before { content: "\ea1b"; }
-.fp-expand:before { content: "\ea1c"; }
-.fp-eye:before { content: "\ea1d"; }
-.fp-fields-icon:before { content: "\ea1e"; }
-.fp-flowgenie-outline:before { content: "\ea1f"; }
-.fp-folder-outline:before { content: "\ea20"; }
-.fp-fullscreen:before { content: "\ea21"; }
-.fp-github:before { content: "\ea22"; }
-.fp-inbox:before { content: "\ea23"; }
-.fp-layout-icon:before { content: "\ea24"; }
-.fp-link-icon:before { content: "\ea25"; }
-.fp-map:before { content: "\ea26"; }
-.fp-minimize:before { content: "\ea27"; }
-.fp-mobile:before { content: "\ea28"; }
-.fp-pdf:before { content: "\ea29"; }
-.fp-play-outline:before { content: "\ea2a"; }
-.fp-plus-thin:before { content: "\ea2b"; }
-.fp-plus:before { content: "\ea2c"; }
-.fp-pm-block:before { content: "\ea2d"; }
-.fp-remove-outlined:before { content: "\ea2e"; }
-.fp-screen-outline:before { content: "\ea2f"; }
-.fp-script-outline:before { content: "\ea30"; }
-.fp-slack-notification:before { content: "\ea31"; }
-.fp-slack:before { content: "\ea32"; }
-.fp-slideshow:before { content: "\ea33"; }
-.fp-table:before { content: "\ea34"; }
-.fp-tachometer-alt-average:before { content: "\ea35"; }
-.fp-trash:before { content: "\ea36"; }
-.fp-unlink:before { content: "\ea37"; }
-.fp-update-outline:before { content: "\ea38"; }
+.fp-check-circle-blue:before { content: "\ea15"; }
+.fp-close:before { content: "\ea16"; }
+.fp-cloud-download-outline:before { content: "\ea17"; }
+.fp-connector-outline:before { content: "\ea18"; }
+.fp-copy-outline:before { content: "\ea19"; }
+.fp-copy:before { content: "\ea1a"; }
+.fp-desktop:before { content: "\ea1b"; }
+.fp-edit-outline:before { content: "\ea1c"; }
+.fp-expand:before { content: "\ea1d"; }
+.fp-eye:before { content: "\ea1e"; }
+.fp-fields-icon:before { content: "\ea1f"; }
+.fp-flowgenie-outline:before { content: "\ea20"; }
+.fp-folder-outline:before { content: "\ea21"; }
+.fp-fullscreen:before { content: "\ea22"; }
+.fp-github:before { content: "\ea23"; }
+.fp-inbox:before { content: "\ea24"; }
+.fp-layout-icon:before { content: "\ea25"; }
+.fp-link-icon:before { content: "\ea26"; }
+.fp-map:before { content: "\ea27"; }
+.fp-minimize:before { content: "\ea28"; }
+.fp-mobile:before { content: "\ea29"; }
+.fp-pdf:before { content: "\ea2a"; }
+.fp-pen-edit:before { content: "\ea2b"; }
+.fp-play-outline:before { content: "\ea2c"; }
+.fp-plus-thin:before { content: "\ea2d"; }
+.fp-plus:before { content: "\ea2e"; }
+.fp-pm-block:before { content: "\ea2f"; }
+.fp-remove-outlined:before { content: "\ea30"; }
+.fp-screen-outline:before { content: "\ea31"; }
+.fp-script-outline:before { content: "\ea32"; }
+.fp-slack-notification:before { content: "\ea33"; }
+.fp-slack:before { content: "\ea34"; }
+.fp-slideshow:before { content: "\ea35"; }
+.fp-table:before { content: "\ea36"; }
+.fp-tachometer-alt-average:before { content: "\ea37"; }
+.fp-trash-blue:before { content: "\ea38"; }
+.fp-trash:before { content: "\ea39"; }
+.fp-unlink:before { content: "\ea3a"; }
+.fp-update-outline:before { content: "\ea3b"; }
diff --git a/resources/fonts/pm-font/processmaker-font.eot b/resources/fonts/pm-font/processmaker-font.eot
index 8ae8423c30..fc8398ccf1 100644
Binary files a/resources/fonts/pm-font/processmaker-font.eot and b/resources/fonts/pm-font/processmaker-font.eot differ
diff --git a/resources/fonts/pm-font/processmaker-font.less b/resources/fonts/pm-font/processmaker-font.less
index 4c912c777f..889600b9db 100644
--- a/resources/fonts/pm-font/processmaker-font.less
+++ b/resources/fonts/pm-font/processmaker-font.less
@@ -1,10 +1,10 @@
@font-face {font-family: "processmaker-font";
- src: url('processmaker-font.eot?t=1743024460935'); /* IE9*/
- src: url('processmaker-font.eot?t=1743024460935#iefix') format('embedded-opentype'), /* IE6-IE8 */
- url("processmaker-font.woff2?t=1743024460935") format("woff2"),
- url("processmaker-font.woff?t=1743024460935") format("woff"),
- url('processmaker-font.ttf?t=1743024460935') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
- url('processmaker-font.svg?t=1743024460935#processmaker-font') format('svg'); /* iOS 4.1- */
+ src: url('processmaker-font.eot?t=1747081178345'); /* IE9*/
+ src: url('processmaker-font.eot?t=1747081178345#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ url("processmaker-font.woff2?t=1747081178345") format("woff2"),
+ url("processmaker-font.woff?t=1747081178345") format("woff"),
+ url('processmaker-font.ttf?t=1747081178345') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
+ url('processmaker-font.svg?t=1747081178345#processmaker-font') format('svg'); /* iOS 4.1- */
}
[class^="fp-"], [class*=" fp-"] {
@@ -34,39 +34,42 @@
.fp-bpmn-task:before { content: "\ea12"; }
.fp-bpmn-text-annotation:before { content: "\ea13"; }
.fp-brush-icon:before { content: "\ea14"; }
-.fp-close:before { content: "\ea15"; }
-.fp-cloud-download-outline:before { content: "\ea16"; }
-.fp-connector-outline:before { content: "\ea17"; }
-.fp-copy-outline:before { content: "\ea18"; }
-.fp-copy:before { content: "\ea19"; }
-.fp-desktop:before { content: "\ea1a"; }
-.fp-edit-outline:before { content: "\ea1b"; }
-.fp-expand:before { content: "\ea1c"; }
-.fp-eye:before { content: "\ea1d"; }
-.fp-fields-icon:before { content: "\ea1e"; }
-.fp-flowgenie-outline:before { content: "\ea1f"; }
-.fp-folder-outline:before { content: "\ea20"; }
-.fp-fullscreen:before { content: "\ea21"; }
-.fp-github:before { content: "\ea22"; }
-.fp-inbox:before { content: "\ea23"; }
-.fp-layout-icon:before { content: "\ea24"; }
-.fp-link-icon:before { content: "\ea25"; }
-.fp-map:before { content: "\ea26"; }
-.fp-minimize:before { content: "\ea27"; }
-.fp-mobile:before { content: "\ea28"; }
-.fp-pdf:before { content: "\ea29"; }
-.fp-play-outline:before { content: "\ea2a"; }
-.fp-plus-thin:before { content: "\ea2b"; }
-.fp-plus:before { content: "\ea2c"; }
-.fp-pm-block:before { content: "\ea2d"; }
-.fp-remove-outlined:before { content: "\ea2e"; }
-.fp-screen-outline:before { content: "\ea2f"; }
-.fp-script-outline:before { content: "\ea30"; }
-.fp-slack-notification:before { content: "\ea31"; }
-.fp-slack:before { content: "\ea32"; }
-.fp-slideshow:before { content: "\ea33"; }
-.fp-table:before { content: "\ea34"; }
-.fp-tachometer-alt-average:before { content: "\ea35"; }
-.fp-trash:before { content: "\ea36"; }
-.fp-unlink:before { content: "\ea37"; }
-.fp-update-outline:before { content: "\ea38"; }
+.fp-check-circle-blue:before { content: "\ea15"; }
+.fp-close:before { content: "\ea16"; }
+.fp-cloud-download-outline:before { content: "\ea17"; }
+.fp-connector-outline:before { content: "\ea18"; }
+.fp-copy-outline:before { content: "\ea19"; }
+.fp-copy:before { content: "\ea1a"; }
+.fp-desktop:before { content: "\ea1b"; }
+.fp-edit-outline:before { content: "\ea1c"; }
+.fp-expand:before { content: "\ea1d"; }
+.fp-eye:before { content: "\ea1e"; }
+.fp-fields-icon:before { content: "\ea1f"; }
+.fp-flowgenie-outline:before { content: "\ea20"; }
+.fp-folder-outline:before { content: "\ea21"; }
+.fp-fullscreen:before { content: "\ea22"; }
+.fp-github:before { content: "\ea23"; }
+.fp-inbox:before { content: "\ea24"; }
+.fp-layout-icon:before { content: "\ea25"; }
+.fp-link-icon:before { content: "\ea26"; }
+.fp-map:before { content: "\ea27"; }
+.fp-minimize:before { content: "\ea28"; }
+.fp-mobile:before { content: "\ea29"; }
+.fp-pdf:before { content: "\ea2a"; }
+.fp-pen-edit:before { content: "\ea2b"; }
+.fp-play-outline:before { content: "\ea2c"; }
+.fp-plus-thin:before { content: "\ea2d"; }
+.fp-plus:before { content: "\ea2e"; }
+.fp-pm-block:before { content: "\ea2f"; }
+.fp-remove-outlined:before { content: "\ea30"; }
+.fp-screen-outline:before { content: "\ea31"; }
+.fp-script-outline:before { content: "\ea32"; }
+.fp-slack-notification:before { content: "\ea33"; }
+.fp-slack:before { content: "\ea34"; }
+.fp-slideshow:before { content: "\ea35"; }
+.fp-table:before { content: "\ea36"; }
+.fp-tachometer-alt-average:before { content: "\ea37"; }
+.fp-trash-blue:before { content: "\ea38"; }
+.fp-trash:before { content: "\ea39"; }
+.fp-unlink:before { content: "\ea3a"; }
+.fp-update-outline:before { content: "\ea3b"; }
diff --git a/resources/fonts/pm-font/processmaker-font.module.less b/resources/fonts/pm-font/processmaker-font.module.less
index aa75311cc6..d101bf60e6 100644
--- a/resources/fonts/pm-font/processmaker-font.module.less
+++ b/resources/fonts/pm-font/processmaker-font.module.less
@@ -1,10 +1,10 @@
@font-face {font-family: "processmaker-font";
- src: url('processmaker-font.eot?t=1743024460935'); /* IE9*/
- src: url('processmaker-font.eot?t=1743024460935#iefix') format('embedded-opentype'), /* IE6-IE8 */
- url("processmaker-font.woff2?t=1743024460935") format("woff2"),
- url("processmaker-font.woff?t=1743024460935") format("woff"),
- url('processmaker-font.ttf?t=1743024460935') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
- url('processmaker-font.svg?t=1743024460935#processmaker-font') format('svg'); /* iOS 4.1- */
+ src: url('processmaker-font.eot?t=1747081178345'); /* IE9*/
+ src: url('processmaker-font.eot?t=1747081178345#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ url("processmaker-font.woff2?t=1747081178345") format("woff2"),
+ url("processmaker-font.woff?t=1747081178345") format("woff"),
+ url('processmaker-font.ttf?t=1747081178345') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
+ url('processmaker-font.svg?t=1747081178345#processmaker-font') format('svg'); /* iOS 4.1- */
}
[class^="fp-"], [class*=" fp-"] {
@@ -35,41 +35,44 @@
.fp-bpmn-task:before { content: "\ea12"; }
.fp-bpmn-text-annotation:before { content: "\ea13"; }
.fp-brush-icon:before { content: "\ea14"; }
-.fp-close:before { content: "\ea15"; }
-.fp-cloud-download-outline:before { content: "\ea16"; }
-.fp-connector-outline:before { content: "\ea17"; }
-.fp-copy-outline:before { content: "\ea18"; }
-.fp-copy:before { content: "\ea19"; }
-.fp-desktop:before { content: "\ea1a"; }
-.fp-edit-outline:before { content: "\ea1b"; }
-.fp-expand:before { content: "\ea1c"; }
-.fp-eye:before { content: "\ea1d"; }
-.fp-fields-icon:before { content: "\ea1e"; }
-.fp-flowgenie-outline:before { content: "\ea1f"; }
-.fp-folder-outline:before { content: "\ea20"; }
-.fp-fullscreen:before { content: "\ea21"; }
-.fp-github:before { content: "\ea22"; }
-.fp-inbox:before { content: "\ea23"; }
-.fp-layout-icon:before { content: "\ea24"; }
-.fp-link-icon:before { content: "\ea25"; }
-.fp-map:before { content: "\ea26"; }
-.fp-minimize:before { content: "\ea27"; }
-.fp-mobile:before { content: "\ea28"; }
-.fp-pdf:before { content: "\ea29"; }
-.fp-play-outline:before { content: "\ea2a"; }
-.fp-plus-thin:before { content: "\ea2b"; }
-.fp-plus:before { content: "\ea2c"; }
-.fp-pm-block:before { content: "\ea2d"; }
-.fp-remove-outlined:before { content: "\ea2e"; }
-.fp-screen-outline:before { content: "\ea2f"; }
-.fp-script-outline:before { content: "\ea30"; }
-.fp-slack-notification:before { content: "\ea31"; }
-.fp-slack:before { content: "\ea32"; }
-.fp-slideshow:before { content: "\ea33"; }
-.fp-table:before { content: "\ea34"; }
-.fp-tachometer-alt-average:before { content: "\ea35"; }
-.fp-trash:before { content: "\ea36"; }
-.fp-unlink:before { content: "\ea37"; }
-.fp-update-outline:before { content: "\ea38"; }
+.fp-check-circle-blue:before { content: "\ea15"; }
+.fp-close:before { content: "\ea16"; }
+.fp-cloud-download-outline:before { content: "\ea17"; }
+.fp-connector-outline:before { content: "\ea18"; }
+.fp-copy-outline:before { content: "\ea19"; }
+.fp-copy:before { content: "\ea1a"; }
+.fp-desktop:before { content: "\ea1b"; }
+.fp-edit-outline:before { content: "\ea1c"; }
+.fp-expand:before { content: "\ea1d"; }
+.fp-eye:before { content: "\ea1e"; }
+.fp-fields-icon:before { content: "\ea1f"; }
+.fp-flowgenie-outline:before { content: "\ea20"; }
+.fp-folder-outline:before { content: "\ea21"; }
+.fp-fullscreen:before { content: "\ea22"; }
+.fp-github:before { content: "\ea23"; }
+.fp-inbox:before { content: "\ea24"; }
+.fp-layout-icon:before { content: "\ea25"; }
+.fp-link-icon:before { content: "\ea26"; }
+.fp-map:before { content: "\ea27"; }
+.fp-minimize:before { content: "\ea28"; }
+.fp-mobile:before { content: "\ea29"; }
+.fp-pdf:before { content: "\ea2a"; }
+.fp-pen-edit:before { content: "\ea2b"; }
+.fp-play-outline:before { content: "\ea2c"; }
+.fp-plus-thin:before { content: "\ea2d"; }
+.fp-plus:before { content: "\ea2e"; }
+.fp-pm-block:before { content: "\ea2f"; }
+.fp-remove-outlined:before { content: "\ea30"; }
+.fp-screen-outline:before { content: "\ea31"; }
+.fp-script-outline:before { content: "\ea32"; }
+.fp-slack-notification:before { content: "\ea33"; }
+.fp-slack:before { content: "\ea34"; }
+.fp-slideshow:before { content: "\ea35"; }
+.fp-table:before { content: "\ea36"; }
+.fp-tachometer-alt-average:before { content: "\ea37"; }
+.fp-trash-blue:before { content: "\ea38"; }
+.fp-trash:before { content: "\ea39"; }
+.fp-unlink:before { content: "\ea3a"; }
+.fp-update-outline:before { content: "\ea3b"; }
}
\ No newline at end of file
diff --git a/resources/fonts/pm-font/processmaker-font.scss b/resources/fonts/pm-font/processmaker-font.scss
index 0d3f1f785f..0eae6ae361 100644
--- a/resources/fonts/pm-font/processmaker-font.scss
+++ b/resources/fonts/pm-font/processmaker-font.scss
@@ -1,10 +1,10 @@
@font-face {font-family: "processmaker-font";
- src: url('processmaker-font.eot?t=1743024460935'); /* IE9*/
- src: url('processmaker-font.eot?t=1743024460935#iefix') format('embedded-opentype'), /* IE6-IE8 */
- url("processmaker-font.woff2?t=1743024460935") format("woff2"),
- url("processmaker-font.woff?t=1743024460935") format("woff"),
- url('processmaker-font.ttf?t=1743024460935') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
- url('processmaker-font.svg?t=1743024460935#processmaker-font') format('svg'); /* iOS 4.1- */
+ src: url('processmaker-font.eot?t=1747081178345'); /* IE9*/
+ src: url('processmaker-font.eot?t=1747081178345#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ url("processmaker-font.woff2?t=1747081178345") format("woff2"),
+ url("processmaker-font.woff?t=1747081178345") format("woff"),
+ url('processmaker-font.ttf?t=1747081178345') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
+ url('processmaker-font.svg?t=1747081178345#processmaker-font') format('svg'); /* iOS 4.1- */
}
[class^="fp-"], [class*=" fp-"] {
@@ -34,42 +34,45 @@
.fp-bpmn-task:before { content: "\ea12"; }
.fp-bpmn-text-annotation:before { content: "\ea13"; }
.fp-brush-icon:before { content: "\ea14"; }
-.fp-close:before { content: "\ea15"; }
-.fp-cloud-download-outline:before { content: "\ea16"; }
-.fp-connector-outline:before { content: "\ea17"; }
-.fp-copy-outline:before { content: "\ea18"; }
-.fp-copy:before { content: "\ea19"; }
-.fp-desktop:before { content: "\ea1a"; }
-.fp-edit-outline:before { content: "\ea1b"; }
-.fp-expand:before { content: "\ea1c"; }
-.fp-eye:before { content: "\ea1d"; }
-.fp-fields-icon:before { content: "\ea1e"; }
-.fp-flowgenie-outline:before { content: "\ea1f"; }
-.fp-folder-outline:before { content: "\ea20"; }
-.fp-fullscreen:before { content: "\ea21"; }
-.fp-github:before { content: "\ea22"; }
-.fp-inbox:before { content: "\ea23"; }
-.fp-layout-icon:before { content: "\ea24"; }
-.fp-link-icon:before { content: "\ea25"; }
-.fp-map:before { content: "\ea26"; }
-.fp-minimize:before { content: "\ea27"; }
-.fp-mobile:before { content: "\ea28"; }
-.fp-pdf:before { content: "\ea29"; }
-.fp-play-outline:before { content: "\ea2a"; }
-.fp-plus-thin:before { content: "\ea2b"; }
-.fp-plus:before { content: "\ea2c"; }
-.fp-pm-block:before { content: "\ea2d"; }
-.fp-remove-outlined:before { content: "\ea2e"; }
-.fp-screen-outline:before { content: "\ea2f"; }
-.fp-script-outline:before { content: "\ea30"; }
-.fp-slack-notification:before { content: "\ea31"; }
-.fp-slack:before { content: "\ea32"; }
-.fp-slideshow:before { content: "\ea33"; }
-.fp-table:before { content: "\ea34"; }
-.fp-tachometer-alt-average:before { content: "\ea35"; }
-.fp-trash:before { content: "\ea36"; }
-.fp-unlink:before { content: "\ea37"; }
-.fp-update-outline:before { content: "\ea38"; }
+.fp-check-circle-blue:before { content: "\ea15"; }
+.fp-close:before { content: "\ea16"; }
+.fp-cloud-download-outline:before { content: "\ea17"; }
+.fp-connector-outline:before { content: "\ea18"; }
+.fp-copy-outline:before { content: "\ea19"; }
+.fp-copy:before { content: "\ea1a"; }
+.fp-desktop:before { content: "\ea1b"; }
+.fp-edit-outline:before { content: "\ea1c"; }
+.fp-expand:before { content: "\ea1d"; }
+.fp-eye:before { content: "\ea1e"; }
+.fp-fields-icon:before { content: "\ea1f"; }
+.fp-flowgenie-outline:before { content: "\ea20"; }
+.fp-folder-outline:before { content: "\ea21"; }
+.fp-fullscreen:before { content: "\ea22"; }
+.fp-github:before { content: "\ea23"; }
+.fp-inbox:before { content: "\ea24"; }
+.fp-layout-icon:before { content: "\ea25"; }
+.fp-link-icon:before { content: "\ea26"; }
+.fp-map:before { content: "\ea27"; }
+.fp-minimize:before { content: "\ea28"; }
+.fp-mobile:before { content: "\ea29"; }
+.fp-pdf:before { content: "\ea2a"; }
+.fp-pen-edit:before { content: "\ea2b"; }
+.fp-play-outline:before { content: "\ea2c"; }
+.fp-plus-thin:before { content: "\ea2d"; }
+.fp-plus:before { content: "\ea2e"; }
+.fp-pm-block:before { content: "\ea2f"; }
+.fp-remove-outlined:before { content: "\ea30"; }
+.fp-screen-outline:before { content: "\ea31"; }
+.fp-script-outline:before { content: "\ea32"; }
+.fp-slack-notification:before { content: "\ea33"; }
+.fp-slack:before { content: "\ea34"; }
+.fp-slideshow:before { content: "\ea35"; }
+.fp-table:before { content: "\ea36"; }
+.fp-tachometer-alt-average:before { content: "\ea37"; }
+.fp-trash-blue:before { content: "\ea38"; }
+.fp-trash:before { content: "\ea39"; }
+.fp-unlink:before { content: "\ea3a"; }
+.fp-update-outline:before { content: "\ea3b"; }
$fp-add-outlined: "\ea01";
$fp-arrow-left: "\ea02";
@@ -91,40 +94,43 @@ $fp-bpmn-start-event: "\ea11";
$fp-bpmn-task: "\ea12";
$fp-bpmn-text-annotation: "\ea13";
$fp-brush-icon: "\ea14";
-$fp-close: "\ea15";
-$fp-cloud-download-outline: "\ea16";
-$fp-connector-outline: "\ea17";
-$fp-copy-outline: "\ea18";
-$fp-copy: "\ea19";
-$fp-desktop: "\ea1a";
-$fp-edit-outline: "\ea1b";
-$fp-expand: "\ea1c";
-$fp-eye: "\ea1d";
-$fp-fields-icon: "\ea1e";
-$fp-flowgenie-outline: "\ea1f";
-$fp-folder-outline: "\ea20";
-$fp-fullscreen: "\ea21";
-$fp-github: "\ea22";
-$fp-inbox: "\ea23";
-$fp-layout-icon: "\ea24";
-$fp-link-icon: "\ea25";
-$fp-map: "\ea26";
-$fp-minimize: "\ea27";
-$fp-mobile: "\ea28";
-$fp-pdf: "\ea29";
-$fp-play-outline: "\ea2a";
-$fp-plus-thin: "\ea2b";
-$fp-plus: "\ea2c";
-$fp-pm-block: "\ea2d";
-$fp-remove-outlined: "\ea2e";
-$fp-screen-outline: "\ea2f";
-$fp-script-outline: "\ea30";
-$fp-slack-notification: "\ea31";
-$fp-slack: "\ea32";
-$fp-slideshow: "\ea33";
-$fp-table: "\ea34";
-$fp-tachometer-alt-average: "\ea35";
-$fp-trash: "\ea36";
-$fp-unlink: "\ea37";
-$fp-update-outline: "\ea38";
+$fp-check-circle-blue: "\ea15";
+$fp-close: "\ea16";
+$fp-cloud-download-outline: "\ea17";
+$fp-connector-outline: "\ea18";
+$fp-copy-outline: "\ea19";
+$fp-copy: "\ea1a";
+$fp-desktop: "\ea1b";
+$fp-edit-outline: "\ea1c";
+$fp-expand: "\ea1d";
+$fp-eye: "\ea1e";
+$fp-fields-icon: "\ea1f";
+$fp-flowgenie-outline: "\ea20";
+$fp-folder-outline: "\ea21";
+$fp-fullscreen: "\ea22";
+$fp-github: "\ea23";
+$fp-inbox: "\ea24";
+$fp-layout-icon: "\ea25";
+$fp-link-icon: "\ea26";
+$fp-map: "\ea27";
+$fp-minimize: "\ea28";
+$fp-mobile: "\ea29";
+$fp-pdf: "\ea2a";
+$fp-pen-edit: "\ea2b";
+$fp-play-outline: "\ea2c";
+$fp-plus-thin: "\ea2d";
+$fp-plus: "\ea2e";
+$fp-pm-block: "\ea2f";
+$fp-remove-outlined: "\ea30";
+$fp-screen-outline: "\ea31";
+$fp-script-outline: "\ea32";
+$fp-slack-notification: "\ea33";
+$fp-slack: "\ea34";
+$fp-slideshow: "\ea35";
+$fp-table: "\ea36";
+$fp-tachometer-alt-average: "\ea37";
+$fp-trash-blue: "\ea38";
+$fp-trash: "\ea39";
+$fp-unlink: "\ea3a";
+$fp-update-outline: "\ea3b";
diff --git a/resources/fonts/pm-font/processmaker-font.styl b/resources/fonts/pm-font/processmaker-font.styl
index 4c912c777f..889600b9db 100644
--- a/resources/fonts/pm-font/processmaker-font.styl
+++ b/resources/fonts/pm-font/processmaker-font.styl
@@ -1,10 +1,10 @@
@font-face {font-family: "processmaker-font";
- src: url('processmaker-font.eot?t=1743024460935'); /* IE9*/
- src: url('processmaker-font.eot?t=1743024460935#iefix') format('embedded-opentype'), /* IE6-IE8 */
- url("processmaker-font.woff2?t=1743024460935") format("woff2"),
- url("processmaker-font.woff?t=1743024460935") format("woff"),
- url('processmaker-font.ttf?t=1743024460935') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
- url('processmaker-font.svg?t=1743024460935#processmaker-font') format('svg'); /* iOS 4.1- */
+ src: url('processmaker-font.eot?t=1747081178345'); /* IE9*/
+ src: url('processmaker-font.eot?t=1747081178345#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ url("processmaker-font.woff2?t=1747081178345") format("woff2"),
+ url("processmaker-font.woff?t=1747081178345") format("woff"),
+ url('processmaker-font.ttf?t=1747081178345') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
+ url('processmaker-font.svg?t=1747081178345#processmaker-font') format('svg'); /* iOS 4.1- */
}
[class^="fp-"], [class*=" fp-"] {
@@ -34,39 +34,42 @@
.fp-bpmn-task:before { content: "\ea12"; }
.fp-bpmn-text-annotation:before { content: "\ea13"; }
.fp-brush-icon:before { content: "\ea14"; }
-.fp-close:before { content: "\ea15"; }
-.fp-cloud-download-outline:before { content: "\ea16"; }
-.fp-connector-outline:before { content: "\ea17"; }
-.fp-copy-outline:before { content: "\ea18"; }
-.fp-copy:before { content: "\ea19"; }
-.fp-desktop:before { content: "\ea1a"; }
-.fp-edit-outline:before { content: "\ea1b"; }
-.fp-expand:before { content: "\ea1c"; }
-.fp-eye:before { content: "\ea1d"; }
-.fp-fields-icon:before { content: "\ea1e"; }
-.fp-flowgenie-outline:before { content: "\ea1f"; }
-.fp-folder-outline:before { content: "\ea20"; }
-.fp-fullscreen:before { content: "\ea21"; }
-.fp-github:before { content: "\ea22"; }
-.fp-inbox:before { content: "\ea23"; }
-.fp-layout-icon:before { content: "\ea24"; }
-.fp-link-icon:before { content: "\ea25"; }
-.fp-map:before { content: "\ea26"; }
-.fp-minimize:before { content: "\ea27"; }
-.fp-mobile:before { content: "\ea28"; }
-.fp-pdf:before { content: "\ea29"; }
-.fp-play-outline:before { content: "\ea2a"; }
-.fp-plus-thin:before { content: "\ea2b"; }
-.fp-plus:before { content: "\ea2c"; }
-.fp-pm-block:before { content: "\ea2d"; }
-.fp-remove-outlined:before { content: "\ea2e"; }
-.fp-screen-outline:before { content: "\ea2f"; }
-.fp-script-outline:before { content: "\ea30"; }
-.fp-slack-notification:before { content: "\ea31"; }
-.fp-slack:before { content: "\ea32"; }
-.fp-slideshow:before { content: "\ea33"; }
-.fp-table:before { content: "\ea34"; }
-.fp-tachometer-alt-average:before { content: "\ea35"; }
-.fp-trash:before { content: "\ea36"; }
-.fp-unlink:before { content: "\ea37"; }
-.fp-update-outline:before { content: "\ea38"; }
+.fp-check-circle-blue:before { content: "\ea15"; }
+.fp-close:before { content: "\ea16"; }
+.fp-cloud-download-outline:before { content: "\ea17"; }
+.fp-connector-outline:before { content: "\ea18"; }
+.fp-copy-outline:before { content: "\ea19"; }
+.fp-copy:before { content: "\ea1a"; }
+.fp-desktop:before { content: "\ea1b"; }
+.fp-edit-outline:before { content: "\ea1c"; }
+.fp-expand:before { content: "\ea1d"; }
+.fp-eye:before { content: "\ea1e"; }
+.fp-fields-icon:before { content: "\ea1f"; }
+.fp-flowgenie-outline:before { content: "\ea20"; }
+.fp-folder-outline:before { content: "\ea21"; }
+.fp-fullscreen:before { content: "\ea22"; }
+.fp-github:before { content: "\ea23"; }
+.fp-inbox:before { content: "\ea24"; }
+.fp-layout-icon:before { content: "\ea25"; }
+.fp-link-icon:before { content: "\ea26"; }
+.fp-map:before { content: "\ea27"; }
+.fp-minimize:before { content: "\ea28"; }
+.fp-mobile:before { content: "\ea29"; }
+.fp-pdf:before { content: "\ea2a"; }
+.fp-pen-edit:before { content: "\ea2b"; }
+.fp-play-outline:before { content: "\ea2c"; }
+.fp-plus-thin:before { content: "\ea2d"; }
+.fp-plus:before { content: "\ea2e"; }
+.fp-pm-block:before { content: "\ea2f"; }
+.fp-remove-outlined:before { content: "\ea30"; }
+.fp-screen-outline:before { content: "\ea31"; }
+.fp-script-outline:before { content: "\ea32"; }
+.fp-slack-notification:before { content: "\ea33"; }
+.fp-slack:before { content: "\ea34"; }
+.fp-slideshow:before { content: "\ea35"; }
+.fp-table:before { content: "\ea36"; }
+.fp-tachometer-alt-average:before { content: "\ea37"; }
+.fp-trash-blue:before { content: "\ea38"; }
+.fp-trash:before { content: "\ea39"; }
+.fp-unlink:before { content: "\ea3a"; }
+.fp-update-outline:before { content: "\ea3b"; }
diff --git a/resources/fonts/pm-font/processmaker-font.svg b/resources/fonts/pm-font/processmaker-font.svg
index 6c7ef8e7d8..ed7d5af29f 100644
--- a/resources/fonts/pm-font/processmaker-font.svg
+++ b/resources/fonts/pm-font/processmaker-font.svg
@@ -67,113 +67,122 @@
-
+
+
+
diff --git a/resources/fonts/pm-font/processmaker-font.symbol.svg b/resources/fonts/pm-font/processmaker-font.symbol.svg
index d83d7074c1..a04a9ee859 100644
--- a/resources/fonts/pm-font/processmaker-font.symbol.svg
+++ b/resources/fonts/pm-font/processmaker-font.symbol.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/fonts/pm-font/processmaker-font.ttf b/resources/fonts/pm-font/processmaker-font.ttf
index faacacaa61..8fab2dbe12 100644
Binary files a/resources/fonts/pm-font/processmaker-font.ttf and b/resources/fonts/pm-font/processmaker-font.ttf differ
diff --git a/resources/fonts/pm-font/processmaker-font.woff b/resources/fonts/pm-font/processmaker-font.woff
index 96e06598ff..2a6167ca6b 100644
Binary files a/resources/fonts/pm-font/processmaker-font.woff and b/resources/fonts/pm-font/processmaker-font.woff differ
diff --git a/resources/fonts/pm-font/processmaker-font.woff2 b/resources/fonts/pm-font/processmaker-font.woff2
index dc8b0f57ef..66821e03a1 100644
Binary files a/resources/fonts/pm-font/processmaker-font.woff2 and b/resources/fonts/pm-font/processmaker-font.woff2 differ
diff --git a/resources/fonts/pm-font/symbol.html b/resources/fonts/pm-font/symbol.html
index 2bee76e088..89daea26d0 100644
--- a/resources/fonts/pm-font/symbol.html
+++ b/resources/fonts/pm-font/symbol.html
@@ -101,7 +101,7 @@
@@ -167,17 +157,21 @@
import Modal from "./Modal.vue";
import IconDropdown from "./IconDropdown.vue";
import InputImageCarousel from "./InputImageCarousel.vue";
-import ColumnChooser from "./ColumnChooser.vue";
+import EditColumnModal from "./EditColumnModal.vue";
+
+const isTceCustomization = () => window.ProcessMaker?.isTceCustomization;
export default {
- components: { Modal, IconDropdown, InputImageCarousel, ColumnChooser },
+ components: {
+ Modal, IconDropdown, InputImageCarousel, EditColumnModal,
+ },
props: {
options: {
type: Object,
- default: {
+ default: () => ({
id: "",
type: "",
- },
+ }),
},
filter: {
type: String,
@@ -199,6 +193,10 @@ export default {
type: Array,
default: () => [],
},
+ myCasesColumns: {
+ type: Array,
+ default: () => [],
+ },
},
data() {
return {
@@ -208,6 +206,23 @@ export default {
description: "",
errors: "",
selectedSavedChart: null,
+ tceScreens: [
+ {
+ id: "tce-student",
+ uuid: "",
+ title: this.$t("Distribution Bar Fin Aid Student"),
+ },
+ {
+ id: "tce-college",
+ uuid: "",
+ title: this.$t("Distribution Bar Fin Aid College"),
+ },
+ {
+ id: "tce-grants",
+ uuid: "",
+ title: this.$t("Distribution Bar Grants"),
+ },
+ ],
defaultScreen: {
id: 0,
uuid: "",
@@ -248,14 +263,56 @@ export default {
},
],
tabs: [],
+ columnListing: {},
+ typeListing: "",
myTasks: {
currentColumns: [],
availableColumns: [],
defaultColumns: [],
dataColumns: [],
},
+ myCases: {
+ currentColumns: [],
+ availableColumns: [],
+ defaultColumns: [],
+ dataColumns: [],
+ },
+ ScreenDefaultId: [0, "tce-student", "tce-college", "tce-grants"],
};
},
+ computed: {
+ isEditColumns() {
+ if (this.ScreenDefaultId.includes(this.selectedScreen?.id)) {
+ return true;
+ }
+ return false;
+ },
+ isTCEScreen() {
+ if (this.selectedScreen?.id === 0) {
+ return false;
+ }
+ if (this.ScreenDefaultId.includes(this.selectedScreen?.id)) {
+ return true;
+ }
+ return false;
+ },
+ showTasks() {
+ if (this.isEditColumns && !this.isTCEScreen) {
+ return true;
+ }
+ return false;
+ },
+ showCases() {
+ if (this.isEditColumns && this.isTCEScreen) {
+ return true;
+ }
+ if (this.isEditColumns && !this.isTCEScreen) {
+ // This was not implemented now
+ return false;
+ }
+ return false;
+ },
+ },
mounted() {
this.retrieveSavedSearchCharts();
this.retrieveDisplayScreen();
@@ -275,44 +332,29 @@ export default {
.then((response) => {
const firstResponse = response.data.shift();
const unparseProperties = firstResponse?.launchpad?.properties;
- const launchpadProperties = unparseProperties
- ? JSON.parse(unparseProperties)
- : "";
+ const launchpadProperties = unparseProperties ? JSON.parse(unparseProperties) : "";
if (launchpadProperties !== "" && "tabs" in launchpadProperties) {
this.tabs = launchpadProperties.tabs;
}
- if (
- launchpadProperties !== "" &&
- "my_tasks_columns" in launchpadProperties
- ) {
+ if (launchpadProperties !== "" && "my_tasks_columns" in launchpadProperties) {
this.myTasks.currentColumns = launchpadProperties.my_tasks_columns;
}
- if (
- launchpadProperties &&
- Object.keys(launchpadProperties).length > 0
- ) {
- this.selectedSavedChart =
- this.getSelectedSavedChartJSONFromResult(launchpadProperties);
- this.selectedLaunchpadIcon = this.verifyProperty(
- launchpadProperties.icon
- )
+ if (launchpadProperties !== "" && "my_cases_columns" in launchpadProperties) {
+ this.myCases.currentColumns = launchpadProperties.my_cases_columns;
+ }
+ if (launchpadProperties && Object.keys(launchpadProperties).length > 0) {
+ this.selectedSavedChart = this.getSelectedSavedChartJSONFromResult(launchpadProperties);
+ this.selectedLaunchpadIcon = this.verifyProperty(launchpadProperties.icon)
? this.defaultIcon
: launchpadProperties.icon;
- this.selectedLaunchpadIconLabel = this.verifyProperty(
- launchpadProperties.icon_label
- )
+ this.selectedLaunchpadIconLabel = this.verifyProperty(launchpadProperties.icon_label)
? this.defaultIcon
: launchpadProperties.icon_label;
- this.selectedScreen =
- this.getSelectedScreenJSONFromResult(launchpadProperties);
+ this.selectedScreen = this.getSelectedScreenJSONFromResult(launchpadProperties);
this.$refs["icon-dropdown"].setIcon(this.selectedLaunchpadIcon);
} else {
- this.selectedSavedChart = this.getSelectedSavedChartJSON(
- this.defaultChart
- );
- this.selectedScreen = this.getSelectedScreenJSON(
- this.defaultScreen
- );
+ this.selectedSavedChart = this.getSelectedSavedChartJSON(this.defaultChart);
+ this.selectedScreen = this.getSelectedScreenJSON(this.defaultScreen);
}
this.oldScreen = this.selectedScreen.id;
// Load media into Carousel Container
@@ -414,8 +456,7 @@ export default {
*/
saveProcessDescription() {
if (!this.$refs["image-carousel"].checkImages()) return;
- this.dataProcess.imagesCarousel =
- this.$refs["image-carousel"].getImages();
+ this.dataProcess.imagesCarousel = this.$refs["image-carousel"].getImages();
this.dataProcess.properties = JSON.stringify(
{
saved_chart_id: this.selectedSavedChart.id,
@@ -427,9 +468,10 @@ export default {
icon_label: this.selectedLaunchpadIconLabel,
tabs: this.tabs,
my_tasks_columns: this.myTasks.currentColumns,
+ my_cases_columns: this.myCases.currentColumns,
},
null,
- 1
+ 1,
);
ProcessMaker.apiClient
@@ -443,7 +485,7 @@ export default {
this.$t("The launchpad settings were saved."),
"success",
5,
- true
+ true,
);
const params = {
indexImage: null,
@@ -452,13 +494,14 @@ export default {
if (this.oldScreen !== this.selectedScreen.id) {
ProcessMaker.EventBus.$emit(
"reloadByNewScreen",
- this.selectedScreenId
+ this.selectedScreenId,
);
}
ProcessMaker.EventBus.$emit("getLaunchpadImagesEvent", params);
ProcessMaker.EventBus.$emit("getChartId", this.selectedSavedChart.id);
this.customModalButtons[1].disabled = false;
this.$emit("updateMyTasksColumns", this.myTasks.currentColumns);
+ this.$emit("updateMyCasesColumns", this.myCases.currentColumns);
this.saveLaunchpadSettings(response.data);
this.hideModal();
})
@@ -484,9 +527,9 @@ export default {
const filter = query === "" || query === null ? "" : `&filter=${query}`;
ProcessMaker.apiClient
.get(
- "saved-searches?page=1&per_page=10&order_by=title&order_direction=asc" +
- "&has=charts&include=charts&get=id,title,charts.id,charts.title,charts.saved_search_id,type" +
- `${filter}`
+ "saved-searches?page=1&per_page=10&order_by=title&order_direction=asc"
+ + "&has=charts&include=charts&get=id,title,charts.id,charts.title,charts.saved_search_id,type"
+ + `${filter}`,
)
.then((response) => {
if (response.data.data[0].charts) {
@@ -514,7 +557,7 @@ export default {
const filter = query === "" || query === null ? "" : `&filter=${query}`;
ProcessMaker.apiClient
.get(
- `screens?page=1&per_page=10&order_by=title&order_direction=asc&include=categories,category&exclude=config&type=DISPLAY${filter}`
+ `screens?page=1&per_page=10&order_by=title&order_direction=asc&include=categories,category&exclude=config&type=DISPLAY${filter}`,
)
.then((response) => {
if (response.data.data) {
@@ -523,7 +566,12 @@ export default {
title: item.title,
uuid: item.uuid,
}));
- this.dropdownSavedScreen = [this.defaultScreen].concat(resultArray);
+
+ this.dropdownSavedScreen = [this.defaultScreen];
+ if (isTceCustomization()) {
+ this.dropdownSavedScreen = this.dropdownSavedScreen.concat(this.tceScreens);
+ }
+ this.dropdownSavedScreen = this.dropdownSavedScreen.concat(resultArray);
}
})
.catch((error) => {
@@ -536,8 +584,7 @@ export default {
getDescriptionInitial() {
if (this.origin !== "core") {
if (ProcessMaker.modeler?.process) {
- this.processDescriptionInitial =
- ProcessMaker.modeler.process.description;
+ this.processDescriptionInitial = ProcessMaker.modeler.process.description;
}
} else {
this.processDescriptionInitial = this.descriptionSettings;
@@ -571,49 +618,474 @@ export default {
* This method shows a modal window to edit the columns of the My Tasks list.
* If you don't use the nextTick method, the modal will not be displayed correctly.
*/
- showEditTaskColumn() {
- this.$refs["editTaskColumn"].show();
+ showEditColumn(type) {
+ this.columnListing = type === "tasks" ? this.myTasks : this.myCases;
+ this.typeListing = type;
+ this.$refs.editColumnModal.showModal();
this.$nextTick(() => {
- this.getMyTasksColumns();
+ this.getMyColumns(type);
});
},
- async getMyTasksColumns() {
- this.myTasks.currentColumns = this.myTasksColumns;
-
+ async getMyColumns(type) {
+ if (this.isTCEScreen) {
+ this.changeSelectedScreen();
+ }
await ProcessMaker.apiClient
- .get(`saved-searches/columns`)
- .then((response) => {
- if (response.data && response.data.default) {
- this.myTasks.defaultColumns = response.data.default;
- this.myTasks.defaultColumns.push({
+ .get("saved-searches/columns")
+ .then((response) => {
+ this.columnListing.currentColumns = type === "tasks" ? this.myTasksColumns : this.myCasesColumns;
+ if (this.isTCEScreen) {
+ if (response.data) {
+ if (response.data.default) {
+ this.columnListing.defaultColumns = response.data.default;
+ }
+ if (response.data.available) {
+ const current = this.columnListing.currentColumns;
+ const available = response.data.available;
+ const result = available.filter(b => !current.some(a => a.field === b.field));
+ this.columnListing.availableColumns = result;
+ }
+ if (response.data.data) {
+ this.columnListing.dataColumns = response.data.data;
+ }
+ }
+ } else {
+ if (response.data) {
+ if (response.data.default) {
+ this.columnListing.defaultColumns = response.data.default;
+ this.columnListing.defaultColumns.push({
field: "options",
label: "",
sortable: false,
- width: 180
- });
- }
- if (response.data) {
+ width: 180,
+ });
+ }
if (response.data.available) {
- this.myTasks.availableColumns = response.data.available;
+ this.columnListing.availableColumns = response.data.available;
- //Merge all available and default columns; we use map to avoid duplicates.
+ // Merge all available and default columns; we use map to avoid duplicates.
const allColumns = new Map([
- ...this.myTasks.defaultColumns.map(col => [col.field, col]),
- ...this.myTasks.availableColumns.map(col => [col.field, col])
+ ...this.columnListing.defaultColumns.map((col) => [col.field, col]),
+ ...this.columnListing.availableColumns.map((col) => [col.field, col]),
]);
- //Filter only those that are not in `currentColumns`.
- this.myTasks.availableColumns = [...allColumns.values()].filter(
- column => !this.myTasks.currentColumns.some(
- currentColumn => currentColumn.field === column.field
- )
+ // Filter only those that are not in `currentColumns`.
+ this.columnListing.availableColumns = [...allColumns.values()].filter(
+ (column) => !this.columnListing.currentColumns.some(
+ (currentColumn) => currentColumn.field === column.field,
+ ),
);
}
if (response.data.data) {
- this.myTasks.dataColumns = response.data.data;
+ this.columnListing.dataColumns = response.data.data;
}
}
- });
+ }
+ });
+ },
+ updateColumns(columns, type) {
+ if (type === "tasks") {
+ this.myTasks.currentColumns = columns;
+ } else {
+ this.myCases.currentColumns = columns;
+ }
+ },
+ changeSelectedScreen() {
+ if (this.selectedScreen.id === 'tce-student') {
+ this.myCases.currentColumns = this.getTceStudent();
+ return;
+ }
+ if (this.selectedScreen.id === 'tce-college') {
+ this.myCases.currentColumns = this.getTceCollege();
+ return;
+ }
+ if (this.selectedScreen.id === 'tce-grants') {
+ this.myCases.currentColumns = this.getTceGrants();
+ return;
+ }
+ this.myCases.currentColumns = this.getDefaultColumns();
+ },
+ getDefaultColumns() {
+ return [
+ {
+ label: "Case #",
+ field: "case_number",
+ sortable: true,
+ default: true,
+ width: 80,
+ },
+ {
+ label: "Case title",
+ field: "case_title",
+ sortable: true,
+ default: true,
+ truncate: true,
+ width: 220,
+ },
+ {
+ label: "Status",
+ field: "status",
+ sortable: true,
+ default: true,
+ width: 100,
+ filter_subject: { type: "Status" },
+ },
+ {
+ label: "Started",
+ field: "initiated_at",
+ format: "datetime",
+ sortable: true,
+ default: true,
+ width: 160,
+ },
+ {
+ label: "Completed",
+ field: "completed_at",
+ format: "datetime",
+ sortable: true,
+ default: true,
+ width: 160,
+ },
+ ];
+ },
+ /**
+ * column = [
+ * 'case_number'
+ * 'case_title'
+ * 'tasks'
+ * 'status'
+ * 'last_stage_name'
+ * 'progress'
+ * 'data.program.name'
+ * 'data.program.type'
+ * 'data.program.source'
+ * 'data.program.deadline'
+ * 'data.program.amount'
+ * 'data.program.status'
+ * ];
+ * @returns {Array}
+ */
+ getTceStudent() {
+ return [
+ {
+ label: "Case #",
+ field: "case_number",
+ sortable: true,
+ default: true,
+ width: 80,
+ },
+ {
+ label: "Case title",
+ field: "case_title",
+ sortable: true,
+ default: true,
+ truncate: true,
+ width: 220,
+ },
+ {
+ label: "Tasks",
+ field: "active_tasks",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Status",
+ field: "status",
+ sortable: true,
+ default: true,
+ width: 80,
+ filter_subject: { type: "Status" },
+ },
+ {
+ label: "Last Stage Name",
+ field: "last_stage_name",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 110,
+ },
+ {
+ label: "Progress",
+ field: "progress",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Name",
+ field: "data.name",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Type",
+ field: "data.type",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Source",
+ field: "data.source",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Deadline",
+ field: "data.deadline",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Amount",
+ field: "data.amount",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Status",
+ field: "data.status",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ ];
+ },
+ /**
+ * column = [
+ * 'case_number'
+ * 'case_title'
+ * 'tasks'
+ * 'status'
+ * 'last_stage_name'
+ * 'progress'
+ * 'data.program'
+ * 'data.type'
+ * 'data.source'
+ * 'data.deadline'
+ * 'data.amount'
+ * ];
+ * @returns {Array}
+ */
+ getTceCollege() {
+ return [
+ {
+ label: "Case #",
+ field: "case_number",
+ sortable: true,
+ default: true,
+ width: 80,
+ },
+ {
+ label: "Case title",
+ field: "case_title",
+ sortable: true,
+ default: true,
+ truncate: true,
+ width: 220,
+ },
+ {
+ label: "Tasks",
+ field: "active_tasks",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Status",
+ field: "status",
+ sortable: true,
+ default: true,
+ width: 80,
+ filter_subject: { type: "Status" },
+ },
+ {
+ label: "Last Stage Name",
+ field: "last_stage_name",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 110,
+ },
+ {
+ label: "Progress",
+ field: "progress",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Program",
+ field: "data.program",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Type",
+ field: "data.type",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Source",
+ field: "data.source",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Deadline",
+ field: "data.deadline",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Amount",
+ field: "data.amount",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ ];
+ },
+ /**
+ * column = [
+ * 'case_number'
+ * 'case_title'
+ * 'tasks'
+ * 'status'
+ * 'last_stage_name'
+ * 'progress'
+ * 'data.applicationId'
+ * 'data.title'
+ * 'data.department'
+ * 'data.primaryInvestigator'
+ * 'data.agency'
+ * 'data.dueDate'
+ * ];
+ * @returns {Array}
+ */
+ getTceGrants() {
+ return [
+ {
+ label: "Case #",
+ field: "case_number",
+ sortable: true,
+ default: true,
+ width: 80,
+ },
+ {
+ label: "Case title",
+ field: "case_title",
+ sortable: true,
+ default: true,
+ truncate: true,
+ width: 220,
+ },
+ {
+ label: "Tasks",
+ field: "active_tasks",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Status",
+ field: "status",
+ sortable: true,
+ default: true,
+ width: 80,
+ filter_subject: { type: "Status" },
+ },
+ {
+ label: "Last Stage Name",
+ field: "last_stage_name",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 110,
+ },
+ {
+ label: "Progress",
+ field: "progress",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Application Id",
+ field: "data.applicationId",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Title",
+ field: "data.title",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Department",
+ field: "data.department",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Primary Investigator",
+ field: "data.primaryInvestigator",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Agency",
+ field: "data.agency",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ {
+ label: "Due Date",
+ field: "data.dueDate",
+ sortable: false,
+ default: true,
+ truncate: true,
+ width: 100,
+ },
+ ];
},
},
};
diff --git a/resources/js/processes-catalogue/components/BaseChart.vue b/resources/js/processes-catalogue/components/BaseChart.vue
index e65130d731..f677604381 100644
--- a/resources/js/processes-catalogue/components/BaseChart.vue
+++ b/resources/js/processes-catalogue/components/BaseChart.vue
@@ -310,7 +310,6 @@ export default {
diff --git a/resources/js/processes-catalogue/components/Process.vue b/resources/js/processes-catalogue/components/Process.vue
index 1096491bcc..07b2caeeb8 100644
--- a/resources/js/processes-catalogue/components/Process.vue
+++ b/resources/js/processes-catalogue/components/Process.vue
@@ -1,39 +1,49 @@
-
+
+ :process="selectedProcess" />
-
+
+ @goBackCategory="goBackCategory" />
+ @goBackCategory="goBackCategory" />
+
@@ -44,12 +54,14 @@ import MiniPieChart from "./MiniPieChart.vue";
import Bookmark from "./Bookmark.vue";
import ProcessDescription from "./optionsMenu/ProcessDescription.vue";
import ProcessCounter from "./optionsMenu/ProcessCounter.vue";
+import CustomHomeScreen from "./home/CustomHomeScreen.vue";
+import { isTceCustomization } from "./variables";
export default {
- props: ["process", "processId", "ellipsisPermission"],
components: {
- ProcessInfo, ProcessScreen, MiniPieChart, Bookmark, ProcessDescription, ProcessCounter
+ ProcessInfo, ProcessScreen, MiniPieChart, Bookmark, ProcessDescription, ProcessCounter, CustomHomeScreen,
},
+ props: ["process", "processId", "ellipsisPermission"],
data() {
return {
loadedProcess: null,
@@ -59,7 +71,7 @@ export default {
computed: {
/**
* if we pass in a process, use that. Otherwise load the process by ID
- **/
+ * */
selectedProcess() {
if (this.process) {
return this.process;
@@ -74,14 +86,24 @@ export default {
/**
* Verify if the process open the info or Screen
*/
- verifyScreen() {
+ verifyScreen() {
let screenId = 0;
const unparseProperties = this.selectedProcess?.launchpad?.properties || null;
if (unparseProperties !== null) {
screenId = JSON.parse(unparseProperties)?.screen_id || 0;
}
- return screenId !== 0;
+ const validScreen = ["tce-student", "tce-college", "tce-grants"];
+
+ if (validScreen.includes(screenId) && isTceCustomization()) {
+ return "custom";
+ }
+
+ if (screenId !== 0) {
+ return "screen";
+ }
+
+ return "default";
},
processVersion() {
return moment(this.selectedProcess.updated_at).format();
@@ -90,11 +112,6 @@ export default {
return this.selectedProcess.counts?.total || 0;
},
},
- methods: {
- goBackCategory() {
- this.$emit("goBackCategory");
- },
- },
mounted() {
if (!this.process && this.processId) {
ProcessMaker.apiClient
@@ -113,14 +130,16 @@ export default {
window.ProcessMaker.navbarMobile.display = true;
}
},
+ methods: {
+ goBackCategory() {
+ this.$emit("goBackCategory");
+ },
+ },
};
\ No newline at end of file
+}
diff --git a/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue b/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue
index 36954a5443..26ece215b9 100644
--- a/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue
+++ b/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue
@@ -1,47 +1,65 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {{ process.name }}
+
+
+
+
+
+
+
+
+
-
-
+
+
+
-
@@ -83,13 +103,12 @@ import CreateTemplateModal from "../../components/templates/CreateTemplateModal.
import CreatePmBlockModal from "../../components/pm-blocks/CreatePmBlockModal.vue";
import AddToProjectModal from "../../components/shared/AddToProjectModal.vue";
import LaunchpadSettingsModal from "../../components/shared/LaunchpadSettingsModal.vue";
-import ProcessesCarousel from "./ProcessesCarousel.vue";
-import ProcessOptions from "./ProcessOptions.vue";
import ellipsisMenuMixin from "../../components/shared/ellipsisMenuActions";
import processNavigationMixin from "../../components/shared/processNavigation";
import ProcessesMixin from "./mixins/ProcessesMixin";
-import ProcessHeader from "./ProcessHeader.vue";
-import ProcessHeaderStart from "./ProcessHeaderStart.vue";
+import ButtonsStart from "./optionsMenu/ButtonsStart.vue";
+import EllipsisMenu from "../../components/shared/EllipsisMenu.vue";
+import Bookmark from "./Bookmark.vue";
export default {
components: {
@@ -97,19 +116,28 @@ export default {
CreatePmBlockModal,
AddToProjectModal,
LaunchpadSettingsModal,
- ProcessOptions,
- ProcessesCarousel,
- ProcessHeader,
- ProcessHeaderStart,
+ ButtonsStart,
+ EllipsisMenu,
+ Bookmark,
},
mixins: [ProcessesMixin, ellipsisMenuMixin, processNavigationMixin],
- props: ["process", "currentUserId", "ellipsisPermission", "myTasksColumns"],
+ props: ["process", "currentUserId", "ellipsisPermission", "myTasksColumns", "myCasesColumns"],
+ data() {
+ return {
+ mobileApp: window.ProcessMaker.mobileApp,
+ showProcessInfo: false,
+ collapsed: true,
+ infoCollapsed: true,
+ processEvents: [],
+ singleStartEvent: null,
+ };
+ },
computed: {
createdFromWizardTemplate() {
return !!this.process?.properties?.wizardTemplateUuid;
},
isArchived() {
- return this.process?.status === 'ARCHIVED';
+ return this.process?.status === "ARCHIVED";
},
wizardTemplateUuid() {
return this.process?.properties?.wizardTemplateUuid;
@@ -117,16 +145,10 @@ export default {
},
mounted() {
this.verifyDescription();
- ProcessMaker.EventBus.$on("reloadByNewScreen", (newScreen) => {
+ ProcessMaker.EventBus.$on("reloadByNewScreen", () => {
window.location.reload();
});
- },
- data() {
- return {
- mobileApp: window.ProcessMaker.mobileApp,
- showProcessInfo: false,
- collapsed: true,
- };
+ this.getStartEvents();
},
methods: {
/**
@@ -148,13 +170,46 @@ export default {
},
toggleInfo() {
this.showProcessInfo = !this.showProcessInfo;
+ this.$emit("toggle-info");
+ },
+ setShowProcessInfo(show) {
+ this.showProcessInfo = show;
},
updateMyTasksColumns(columns) {
- this.$emit('updateMyTasksColumns', columns);
+ this.$emit("updateMyTasksColumns", columns);
+ },
+ updateMyCasesColumns(columns) {
+ this.$emit("updateMyCasesColumns", columns);
},
onProcessInfoCollapsed(collapsed) {
this.collapsed = collapsed;
},
+ ellipsisNavigate(action, data) {
+ this.onProcessNavigate(action, data);
+ },
+ /**
+ * get start events for dropdown Menu
+ */
+ getStartEvents() {
+ this.processEvents = [];
+ ProcessMaker.apiClient
+ .get(`process_bookmarks/processes/${this.process.id}/start_events`)
+ .then((response) => {
+ this.processEvents = response.data.data;
+ if (this.processEvents.length === 0) {
+ ProcessMaker.alert(this.$t("The current user does not have permission to start this process"), "danger");
+ }
+ const nonWebEntryStartEvents = this.processEvents.filter(
+ (e) => !("webEntry" in e) || !e.webEntry,
+ );
+ if (nonWebEntryStartEvents.length === 1 && this.processEvents.length === 1) {
+ this.singleStartEvent = nonWebEntryStartEvents[0].id;
+ }
+ })
+ .catch((err) => {
+ ProcessMaker.alert(err, "danger");
+ });
+ },
},
};
@@ -250,4 +305,103 @@ export default {
align-items: center;
}
}
+
+/* Estilos integrados de ProcessHeaderStart */
+.header-mobile .title,
+.header .title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: left;
+}
+
+.flex-grow-1 {
+ flex-grow: 1;
+}
+
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+
+.d-flex.align-items-center {
+ min-width: 0;
+}
+
+.text-truncate {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+
+.card-bookmark {
+ float: right;
+ width: 20px;
+ height: 23px;
+}
+
+.card-bookmark:hover {
+ cursor: pointer;
+}
+
+.card-custom {
+ background-color: #F6F9FB;
+ border: 1px solid rgba(205, 221, 238, 0.125);
+}
+
+.title {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: left;
+ max-width: 100%;
+ font-size: 22px;
+ letter-spacing: -0.2;
+ color: #4C545C;
+ font-weight: 400;
+}
+
+.custom-color {
+ color: #4C545C;
+}
+
+.info-button {
+ width: 20px;
+ height: 20px;
+ background-color: #6A7887;
+ border: none;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-weight: 700;
+ position: relative;
+
+ span {
+ color: #ffffff;
+ font-size: 14px;
+ }
+}
+
+.info-button-active {
+ background-color: #2773F3 !important;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -8px;
+ left: -8px;
+ right: -8px;
+ bottom: -8px;
+ background-color: rgba(106, 120, 135, 0.1);
+ border-radius: 8px;
+ z-index: 0;
+ border: 1px solid #d3dbe2;
+ }
+}
+
+.ellipsis-border div button span {
+ font-size: 16px;
+}
diff --git a/resources/js/processes-catalogue/components/ProcessInfo.vue b/resources/js/processes-catalogue/components/ProcessInfo.vue
index d8d2a95179..689ed743b2 100644
--- a/resources/js/processes-catalogue/components/ProcessInfo.vue
+++ b/resources/js/processes-catalogue/components/ProcessInfo.vue
@@ -1,52 +1,75 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
-
{{ process.name }} {{ this.firstImage }} of {{ this.lastImage }}
-
-
-
+
+
diff --git a/resources/js/processes-catalogue/components/ProcessTab.vue b/resources/js/processes-catalogue/components/ProcessTab.vue
index 90c37ec8d8..65db6bb76d 100644
--- a/resources/js/processes-catalogue/components/ProcessTab.vue
+++ b/resources/js/processes-catalogue/components/ProcessTab.vue
@@ -1,437 +1,480 @@
-
-
+
+
-
-
-
-
+
+
-
-
-
-
+
+
-
-
+
-
+ />
-
+
-
-
-
+
+
-
-
+
-
-
+
-
-
+
-
-
+
+
+
-
-
-
+
+
-
-
-
+
+
-
+
{{ removalMessage }}
diff --git a/resources/js/processes-catalogue/components/api/index.js b/resources/js/processes-catalogue/components/api/index.js
new file mode 100644
index 0000000000..3e197fecea
--- /dev/null
+++ b/resources/js/processes-catalogue/components/api/index.js
@@ -0,0 +1,35 @@
+import { api, metricsApiEndpoint } from "../variables";
+
+export const getRequests = async ({
+ page, perPage, orderDirection, orderBy, nonSystem, processesIManage, allInbox, pmql, filter, advancedFilter, include, statusFilter,
+}) => {
+ const response = await api.get("/requests", {
+ params: {
+ page,
+ include,
+ per_page: perPage,
+ pmql,
+ order_direction: orderDirection,
+ order_by: orderBy,
+ filter,
+ advanced_filter: JSON.stringify(advancedFilter),
+ },
+ });
+
+ return response.data;
+};
+
+export const getStages = async ({ processId }) => {
+ const response = await api.get(`/processes/${processId}/stage-mapping`);
+ return response.data;
+};
+
+export const getMetrics = async ({ processId }) => {
+ try {
+ const apiDefault = metricsApiEndpoint ? `${metricsApiEndpoint.replace("{process}", processId)}` : `/processes/${processId}/metrics`;
+ const response = await api.get(apiDefault);
+ return response.data;
+ } catch (error) {
+ return [];
+ }
+};
diff --git a/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButton.vue b/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButton.vue
new file mode 100644
index 0000000000..44e6be9511
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButton.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ {{ header }}
+
+
+
+
+ {{ body }}
+
+
+
+
+ {{ helper }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButtonGroup.vue b/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButtonGroup.vue
new file mode 100644
index 0000000000..7e2dbb5901
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButtonGroup.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButtonHome.vue b/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButtonHome.vue
new file mode 100644
index 0000000000..4a1f5341d0
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/ArrowButtonGroup/ArrowButtonHome.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ {{ header }}
+
+
+
+
+ {{ body }}
+
+
+
+
+ {{ helper }}
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/ButtonGroup/BaseCardButton.vue b/resources/js/processes-catalogue/components/home/ButtonGroup/BaseCardButton.vue
new file mode 100644
index 0000000000..d9ebea8ffe
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/ButtonGroup/BaseCardButton.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+ {{ header }}
+
+
+
+
+ {{ body }}
+
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/ButtonGroup/BaseCardButtonGroup.vue b/resources/js/processes-catalogue/components/home/ButtonGroup/BaseCardButtonGroup.vue
new file mode 100644
index 0000000000..bd99fbbbd1
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/ButtonGroup/BaseCardButtonGroup.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/CustomHomeScreen.vue b/resources/js/processes-catalogue/components/home/CustomHomeScreen.vue
new file mode 100644
index 0000000000..265a741ab4
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/CustomHomeScreen.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeFilter.vue b/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeFilter.vue
new file mode 100644
index 0000000000..2c54c083a6
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeFilter.vue
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeTableSection.js b/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeTableSection.js
new file mode 100644
index 0000000000..c8e167c8ea
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeTableSection.js
@@ -0,0 +1,50 @@
+import { getRequests } from "../../api";
+
+export const buildPmql = (pmql, filter) => {
+ let pmqlBuilded = null;
+
+ if (pmql !== undefined) {
+ pmqlBuilded = pmql;
+ }
+
+ if (filter && filter.length && filter.isPMQL()) {
+ pmqlBuilded = `(${pmql}) and (${filter})`;
+ }
+
+ return pmqlBuilded;
+};
+
+export const prepareToGetRequests = async ({
+ page, perPage, orderDirection, orderBy, processesIManage,
+ allInbox, pmql, filter, advancedFilter, include, statusFilter,
+}) => {
+ const nPage = page;
+ const nPerPage = perPage;
+ const nOrderDirection = orderDirection;
+ const nOrderBy = orderBy;
+ const nPmql = buildPmql(pmql, filter);
+ const nFilter = buildPmql(pmql, filter) === pmql ? filter : "";
+ const nAdvancedFilter = advancedFilter;
+
+ const nInclude = `process,participants,activeTasks,data${include ? `,${include}` : ""}`;
+
+ const response = await getRequests({
+ page: nPage,
+ perPage: nPerPage,
+ orderDirection: nOrderDirection,
+ orderBy: nOrderBy,
+ pmql: nPmql,
+ filter: nFilter,
+ include: nInclude,
+ advancedFilter: nAdvancedFilter,
+ });
+ return response;
+};
+
+export const buildColumns = (defaultColumns) => {
+ const columns = [];
+ defaultColumns.forEach((column) => {
+ columns.push(column);
+ });
+ return columns;
+};
diff --git a/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeTableSection.vue b/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeTableSection.vue
new file mode 100644
index 0000000000..6d45de2fd3
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/CustomHomeTableSection/CustomHomeTableSection.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/Home2.vue b/resources/js/processes-catalogue/components/home/Home2.vue
new file mode 100644
index 0000000000..5b294acfb2
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/Home2.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/PercentageButtonGroup/PercentageCardButton.vue b/resources/js/processes-catalogue/components/home/PercentageButtonGroup/PercentageCardButton.vue
new file mode 100644
index 0000000000..6ab0d5d7df
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/PercentageButtonGroup/PercentageCardButton.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+ {{ header }}
+
+
+
+
+ {{ body }}
+
+
+
+
+
+ {{ content }}
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/PercentageButtonGroup/PercentageCardButtonGroup.vue b/resources/js/processes-catalogue/components/home/PercentageButtonGroup/PercentageCardButtonGroup.vue
new file mode 100644
index 0000000000..eb2e9e170a
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/PercentageButtonGroup/PercentageCardButtonGroup.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/ProcessInfo.vue b/resources/js/processes-catalogue/components/home/ProcessInfo.vue
new file mode 100644
index 0000000000..7cd747e4a1
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/ProcessInfo.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/TceDistributionCollege.vue b/resources/js/processes-catalogue/components/home/TceDistributionCollege.vue
new file mode 100644
index 0000000000..a5048cc187
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/TceDistributionCollege.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/TceDistributionGrants.vue b/resources/js/processes-catalogue/components/home/TceDistributionGrants.vue
new file mode 100644
index 0000000000..5caf092e81
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/TceDistributionGrants.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/TceDistributionStudent.vue b/resources/js/processes-catalogue/components/home/TceDistributionStudent.vue
new file mode 100644
index 0000000000..9664f40c25
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/TceDistributionStudent.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
diff --git a/resources/js/processes-catalogue/components/home/config/columns.js b/resources/js/processes-catalogue/components/home/config/columns.js
new file mode 100644
index 0000000000..623eca1809
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/config/columns.js
@@ -0,0 +1,377 @@
+import { t } from "i18next";
+import { get } from "lodash";
+import {
+ LinkCell,
+ TitleCell,
+ FlagCell,
+ StatusCell,
+ TruncatedOptionsCell,
+ ParticipantsCell,
+ ProgressBarCell,
+} from "../../../../../jscomposition/system/index";
+import { formatDate } from "../../../../../jscomposition/utils";
+// Columns in the table:
+
+export const requestNumberColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ formatter: (row, column, columns) => `# ${row.id}`,
+ filter: {
+ dataType: "string",
+ operators: ["=", ">", ">=", "in", "between"],
+ resetTable: true,
+ },
+ cellRenderer: () => ({
+ component: LinkCell,
+ params: {
+ href: (row) => `/requests/${get(row, field)}`,
+ },
+ }),
+});
+
+export const caseNumberColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ formatter: (row, column, columns) => `# ${row.id}`,
+ filter: {
+ dataType: "string",
+ operators: ["=", ">", ">=", "in", "between"],
+ resetTable: true,
+ },
+ cellRenderer: () => ({
+ component: LinkCell,
+ params: {
+ href: (row) => `/cases/${get(row, field)}`,
+ },
+ }),
+});
+
+export const caseTitleColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ cellRenderer: () => ({
+ component: TitleCell,
+ params: {
+ href: (row) => `/cases/${row.case_number}`,
+ },
+ }),
+ filter: {
+ dataType: "string",
+ operators: ["=", "in", "contains", "regex"],
+ resetTable: true,
+ },
+});
+
+export const textColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ filter: {
+ dataType: "string",
+ operators: ["=", "in", "contains", "regex"],
+ resetTable: true,
+ },
+});
+
+export const flagColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header: " ",
+ resizable,
+ width: 50,
+ cellRenderer: () => ({
+ component: FlagCell,
+ params: {
+ active: (row) => get(row, field),
+ click: (active, row, column, columns) => {
+ console.log(active, row, column, columns);
+ },
+ },
+ }),
+});
+
+export const progressColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width: 200,
+ cellRenderer: () => ({
+ component: ProgressBarCell,
+ params: {
+ data: (row, column, columns) => get(row, field),
+ color: "green",
+ },
+ }),
+});
+
+export const taskColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ cellRenderer: () => ({
+ component: TruncatedOptionsCell,
+ params: {
+ href: (option) => `/tasks/${option.id}/edit`,
+ formatterOptions: (option, row, column, columns) =>
+ option.element_name,
+ filterData: (row, column, columns) => {
+ return row.active_tasks;
+ },
+ },
+ }),
+});
+
+export const participantsColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ filter: {
+ dataType: "string",
+ operators: ["=", "in", "contains", "regex"],
+ resetTable: true,
+ },
+ cellRenderer: () => ({
+ component: ParticipantsCell,
+ params: {
+ click: (option, row, column, columns) => {
+ window.document.location = `/profile/${option.id}`;
+ },
+ formatter: (option, row, column, columns) => option.username,
+ initials: (option, row, column, columns) => option.username[0],
+ src: (option, row, column, columns) => option.avatar,
+ },
+ }),
+});
+
+export const statusColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ cellRenderer: () => ({
+ component: StatusCell,
+ }),
+});
+
+export const dateColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+ formatter: (row, column, columns) =>
+ formatDate(row[field], "datetime"),
+ filter: {
+ dataType: "datetime",
+ operators: ["between", ">", ">=", "<", "<="],
+ resetTable: true,
+ },
+});
+
+export const defaultColumn = ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+}) => ({
+ id,
+ field,
+ header,
+ resizable,
+ width,
+});
+
+export const getColumns = (type) => {
+ const columnsDefinition = {
+ default: [
+ requestNumberColumn(),
+ caseNumberColumn(),
+ caseTitleColumn(),
+ flagColumn(),
+ taskColumn(),
+ statusColumn(),
+ dateColumn(),
+ ],
+ };
+
+ return columnsDefinition[type] || columnsDefinition.default;
+};
+
+/// /////////////////////////////////////////////////////////
+// CONVERT DEFAULT COLUMNS FROM BE TO OUR FORMAT
+
+// const convertColumn = {
+// id: 'string', // id of the column
+// field: 'string', // variable to show ex: processRequest.case_number
+// header: 'string', // label of the column
+// resizable: true,
+// width: 144
+// };
+
+export const buildColumns = (defaultColumns) => {
+ const columns = [];
+
+ defaultColumns.forEach((column) => {
+ // Convert column format from type 'a' to type 'b'
+ const convertedColumn = {
+ id: column.field,
+ field: column.field,
+ header: column.label,
+ resizable: true,
+ width: column.width || 144,
+ };
+
+ let newColumn = null;
+
+ switch (column.field) {
+ case "id":
+ newColumn = requestNumberColumn(convertedColumn);
+ break;
+ case "case_number":
+ newColumn = caseNumberColumn(convertedColumn);
+ break;
+ case "case_title":
+ newColumn = caseTitleColumn(convertedColumn);
+ break;
+ case "name":
+ newColumn = textColumn(convertedColumn);
+ break;
+ case "active_tasks":
+ newColumn = taskColumn(convertedColumn);
+ break;
+ case "element_name":
+ newColumn = taskColumn(convertedColumn);
+ break;
+ case "participants":
+ newColumn = participantsColumn(convertedColumn);
+ break;
+ case "status":
+ newColumn = statusColumn(convertedColumn);
+ break;
+ case "last_stage_name":
+ newColumn = defaultColumn(convertedColumn);
+ break;
+ case "progress":
+ newColumn = progressColumn(convertedColumn);
+ break;
+ case "initiated_at":
+ newColumn = dateColumn(convertedColumn);
+ break;
+ case "completed_at":
+ newColumn = dateColumn(convertedColumn);
+ break;
+ default:
+ newColumn = defaultColumn(convertedColumn);
+ }
+
+ columns.push(newColumn);
+ });
+
+ return columns;
+};
+
+export const unbuildColumns = (columns) => {
+ return columns.map((col) => {
+ const original = {
+ label: col.header,
+ field: col.field,
+ width: col.width,
+ };
+
+ if (col.sortable) {
+ original.sortable = true;
+ }
+
+ if (col.filter?.dataType === "datetime") {
+ original.format = "datetime";
+ }
+
+ if (col.filter?.subject) {
+ original.filter_subject = col.filter.subject;
+ }
+
+ if (col.cellRenderer?.component?.name === "TruncatedOptionsCell") {
+ original.truncate = true;
+ }
+
+ return original;
+ });
+};
diff --git a/resources/js/processes-catalogue/components/home/config/filters.js b/resources/js/processes-catalogue/components/home/config/filters.js
new file mode 100644
index 0000000000..3439528a44
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/config/filters.js
@@ -0,0 +1,105 @@
+/**
+ * Only mao the subjects for filters
+ */
+export const subjectColumns = [
+ {
+ field: "id",
+ subject: {
+ type: "Field",
+ value: "id",
+ },
+ },
+ {
+ field: "case_number",
+ subject: {
+ type: "Field",
+ value: "case_number",
+ },
+ },
+ {
+ field: "case_title",
+ subject: {
+ type: "Field",
+ value: "case_title",
+ },
+ },
+ {
+ field: "name",
+ subject: {
+ type: "Field",
+ value: "name",
+ },
+ },
+ {
+ field: "stage",
+ subject: {
+ type: "Field",
+ value: "stage",
+ },
+ },
+ {
+ field: "progress",
+ subject: {
+ type: "Field",
+ value: "progress",
+ },
+ },
+ {
+ field: "participants",
+ subject: { type: "ParticipantsFullName" },
+ },
+ {
+ field: "status",
+ subject: { type: "Status" },
+ },
+ {
+ field: "initiated_at",
+ subject: {
+ type: "Field",
+ value: "initiated_at",
+ },
+ },
+ {
+ field: "completed_at",
+ subject: {
+ type: "Field",
+ value: "completed_at",
+ },
+ },
+];
+
+// Convert value filter from FilterableTable to value for AdvancedFilter
+export const buildValue = (operator, value) => {
+ switch (operator) {
+ case "=":
+ return value;
+ case "between":
+ return value.map((v) => v.value);
+ case "in":
+ return value.map((v) => v.value);
+ case "contains":
+ return value;
+ default:
+ return value;
+ }
+};
+
+export const buildFilters = ({ defaultColumns, filterData }) => {
+ const result = [];
+
+ filterData.forEach((f) => {
+ const filter = subjectColumns.find((column) => column.field === f.id);
+
+ const subject = {
+ type: "Field",
+ };
+
+ result.push({
+ subject: filter.subject || subject,
+ operator: f.operator,
+ value: buildValue(f.operator, f.value),
+ });
+ });
+
+ return result;
+};
diff --git a/resources/js/processes-catalogue/components/home/config/index.js b/resources/js/processes-catalogue/components/home/config/index.js
new file mode 100644
index 0000000000..b77d21e563
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/config/index.js
@@ -0,0 +1,3 @@
+export * from "./columns";
+export * from "./filters";
+export * from "./metrics";
diff --git a/resources/js/processes-catalogue/components/home/config/metrics.js b/resources/js/processes-catalogue/components/home/config/metrics.js
new file mode 100644
index 0000000000..f31e36cde3
--- /dev/null
+++ b/resources/js/processes-catalogue/components/home/config/metrics.js
@@ -0,0 +1,84 @@
+export const defaultMetrics = [{
+ id: 1,
+ icon: "fas fa-reply",
+ active: false,
+ color: "blue",
+},
+{
+ id: 2,
+ icon: "fas fa-user",
+ color: "amber",
+ active: false,
+},
+{
+ id: 3,
+ icon: "fas fa-user",
+ color: "green",
+ active: false,
+}];
+
+export const buildMetrics = (metrics) => {
+ const response = metrics.map((metric) => {
+ const mData = defaultMetrics.find((m) => m.id === metric.id);
+ if (!mData) {
+ return defaultMetrics[defaultMetrics.length - 1];
+ }
+ return {
+ id: metric.metric_id,
+ body: metric.metric_description,
+ header: metric.metric_count_description,
+ content: metric.metric_value,
+ percentage: metric.metric_value_unit || 100,
+ ...mData,
+ };
+ });
+
+ return response;
+};
+
+export const stagesColors = [
+ "red", "indigo", "sky", "white", "purple", "emerald", "yellow",
+ "indigo", "pink", "orange", "teal", "violet", "fuchsia", "rose",
+ "sky", "lime", "cyan", "gray", "black", "white",
+];
+
+export const buildStages = (stages) => stages.map((stage, index) => ({
+ id: stage.stage_id,
+ body: stage.stage_name,
+ header: stage.agregation_count,
+ content: stage.agregation_sum,
+ helper: stage.agregation_sum,
+ percentage: stage.percentage || 100,
+ color: stagesColors.at(index),
+}));
+
+export const verifyResponseMetrics = (response) => {
+ let isValid = true;
+ response.forEach((metric) => {
+ if (!metric.id) {
+ isValid = false;
+ }
+ if (!metric.body) {
+ isValid = false;
+ }
+ if (!metric.color) {
+ isValid = false;
+ }
+ if (typeof metric.content !== "number") {
+ isValid = false;
+ }
+ if (!metric.header) {
+ isValid = false;
+ }
+ if (!metric.icon) {
+ isValid = false;
+ }
+ if (!metric.percentage) {
+ isValid = false;
+ }
+ if (typeof metric.active !== "boolean") {
+ isValid = false;
+ }
+ });
+ return isValid;
+};
diff --git a/resources/js/processes-catalogue/components/mixins/CarouselMixin.js b/resources/js/processes-catalogue/components/mixins/CarouselMixin.js
new file mode 100644
index 0000000000..17a4ba0c3e
--- /dev/null
+++ b/resources/js/processes-catalogue/components/mixins/CarouselMixin.js
@@ -0,0 +1,46 @@
+export default {
+ methods: {
+ /**
+ * Get images from Media library related to process.
+ */
+ getLaunchpadImages() {
+ ProcessMaker.apiClient
+ .get(`process_launchpad/${this.process.id}`)
+ .then((response) => {
+ const firstResponse = response.data.shift();
+ const mediaArray = firstResponse.media;
+ const embedArray = firstResponse.embed;
+ mediaArray.forEach((media) => {
+ const mediaType = media.custom_properties.type ?? "image";
+ this.images.push({
+ url: media.original_url,
+ type: mediaType,
+ });
+ });
+ embedArray.forEach((embed) => {
+ const customProperties = JSON.parse(embed.custom_properties);
+ this.images.push({
+ url: customProperties.url,
+ type: customProperties.type,
+ });
+ });
+ // If no images were loaded Carousel container is not shown
+ if (this.images.length === 0) {
+ this.imagesLoaded = false;
+ }
+ // If only one image is loaded, rest of carousel must be completed with default image
+ if (this.images.length === 1) {
+ for (let i = 1; i <= 3; i += 1) {
+ this.images[i] = {
+ url: "/img/launchpad-images/defaultImage.svg",
+ type: "image",
+ };
+ }
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ },
+ },
+};
diff --git a/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue b/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue
index 6e694a0037..7b4ffeae16 100644
--- a/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue
+++ b/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue
@@ -1,5 +1,5 @@
-
+
{{ $t('Started Cases') }}
@@ -8,7 +8,10 @@
{{ count }}
-
-
-
![]()
-
{{ count }} {{ $t('Cases started') }}
-
-
-
-
-
-

-
-
-
-
diff --git a/resources/js/processes-catalogue/components/optionsMenu/ProcessDescription.vue b/resources/js/processes-catalogue/components/optionsMenu/ProcessDescription.vue
index 8834ebed24..8440fb3c6a 100644
--- a/resources/js/processes-catalogue/components/optionsMenu/ProcessDescription.vue
+++ b/resources/js/processes-catalogue/components/optionsMenu/ProcessDescription.vue
@@ -1,41 +1,38 @@
-