diff --git a/ProcessMaker/Http/Controllers/CasesController.php b/ProcessMaker/Http/Controllers/CasesController.php
index 36b60a3aaa..e8a94dcbab 100644
--- a/ProcessMaker/Http/Controllers/CasesController.php
+++ b/ProcessMaker/Http/Controllers/CasesController.php
@@ -41,8 +41,159 @@ public function index($type = null)
// This is a temporary API the engine team will provide the new
return view('cases.casesMain');
}
+ /**
+ * Cases Detail
+ *
+ * @param ProcessRequest $request
+ *
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
public function edit(ProcessRequest $request)
{
- return view('cases.edit');
+ if (!request()->input('skipInterstitial') && $request->status === 'ACTIVE') {
+ $startEvent = $request->tokens()->orderBy('id')->first();
+ if ($startEvent) {
+ $definition = $startEvent->getDefinition();
+ $allowInterstitial = false;
+ if (isset($definition['allowInterstitial'])) {
+ $allowInterstitial = filter_var(
+ $definition['allowInterstitial'],
+ FILTER_VALIDATE_BOOLEAN,
+ FILTER_NULL_ON_FAILURE
+ );
+ }
+ if ($allowInterstitial && $request->user_id == Auth::id() && request()->has('fromTriggerStartEvent')) {
+ $active = $request->tokens()
+ ->where('user_id', Auth::id())
+ ->where('element_type', 'task')
+ ->where('status', 'ACTIVE')
+ ->orderBy('id')->first();
+
+ // If the interstitial is enabled on the start event, then use it as the task
+ if ($active) {
+ $task = $allowInterstitial ? $startEvent : $active;
+ } else {
+ $task = $startEvent;
+ }
+
+ return redirect(route('tasks.edit', [
+ 'task' => $task->getKey(),
+ ]));
+ }
+ }
+ }
+
+ $userHasCommentsForRequest = Comment::where('commentable_type', ProcessRequest::class)
+ ->where('commentable_id', $request->id)
+ ->where('body', 'like', '%{{' . \Auth::user()->id . '}}%')
+ ->count() > 0;
+
+ $requestMedia = $request->media()->get()->pluck('id');
+
+ $userHasCommentsForMedia = Comment::where('commentable_type', \ProcessMaker\Models\Media::class)
+ ->whereIn('commentable_id', $requestMedia)
+ ->where('body', 'like', '%{{' . \Auth::user()->id . '}}%')
+ ->count() > 0;
+
+ if (!$userHasCommentsForMedia && !$userHasCommentsForRequest) {
+ $this->authorize('view', $request);
+ }
+
+ $request->participants;
+ $request->user;
+ $request->summary = $request->summary();
+
+ if ($request->status === 'CANCELED' && $request->process->cancel_screen_id) {
+ $request->summary_screen = $request->process->cancelScreen;
+ } else {
+ $request->summary_screen = $request->getSummaryScreen();
+ }
+ $request->request_detail_screen = Screen::find($request->process->request_detail_screen_id);
+
+ $canCancel = Auth::user()->can('cancel', $request->processVersion);
+ $canViewComments = (Auth::user()->hasPermissionsFor('comments')->count() > 0) || class_exists(PackageServiceProvider::class);
+ $canManuallyComplete = Auth::user()->is_administrator && $request->status === 'ERROR';
+ $canRetry = false;
+
+ if ($canManuallyComplete) {
+ $retry = RetryProcessRequest::for($request);
+
+ $canRetry = $retry->hasRetriableTasks() &&
+ !$retry->hasNonRetriableTasks() &&
+ !$retry->isChildRequest();
+ }
+
+ $files = \ProcessMaker\Models\Media::getFilesRequest($request);
+
+ $canPrintScreens = $this->canUserPrintScreen($request);
+
+ $manager = app(ScreenBuilderManager::class);
+ event(new ScreenBuilderStarting($manager, ($request->summary_screen) ? $request->summary_screen->type : 'FORM'));
+
+ $addons = [];
+ $dataActionsAddons = [];
+
+ $isProcessManager = $request->process?->manager_id === Auth::user()->id;
+
+ $eligibleRollbackTask = null;
+ $errorTask = RollbackProcessRequest::getErrorTask($request);
+ if ($errorTask) {
+ $eligibleRollbackTask = RollbackProcessRequest::eligibleRollbackTask($errorTask);
+ }
+ $this->summaryScreenTranslation($request);
+ return view('cases.edit', compact(
+ 'request',
+ 'files',
+ 'canCancel',
+ 'canViewComments',
+ 'canManuallyComplete',
+ 'canRetry',
+ 'manager',
+ 'canPrintScreens',
+ 'addons',
+ 'isProcessManager',
+ 'eligibleRollbackTask',
+ 'errorTask',
+ ));
+ }
+
+ /**
+ * the user may or may not print forms
+ *
+ * @param ProcessRequest $request
+ * @return bool
+ */
+ private function canUserPrintScreen(ProcessRequest $request)
+ {
+ //validate user is administrator
+ if (Auth::user()->is_administrator) {
+ return true;
+ }
+
+ //validate user is participant or requester
+ if (in_array(Auth::user()->id, $request->participants()->get()->pluck('id')->toArray())) {
+ return true;
+ }
+
+ // Any user with permissions Edit Request Data, Edit Task Data and view All Requests
+ if (Auth::user()->can('view-all_requests') && Auth::user()->can('edit-request_data') && Auth::user()->can('edit-task_data')) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Translates the summary screen strings
+ * @param ProcessRequest $request
+ * @return void
+ */
+ public function summaryScreenTranslation(ProcessRequest $request): void
+ {
+ if ($request->summary_screen) {
+ $processTranslation = new ProcessTranslation($request->process);
+ $translatedConf = $processTranslation->applyTranslations($request->summary_screen);
+ $request->summary_screen['config'] = $translatedConf;
+ }
}
}
diff --git a/resources/jscomposition/cases/casesDetail/api/data.js b/resources/jscomposition/cases/casesDetail/api/data.js
new file mode 100644
index 0000000000..15aef3da60
--- /dev/null
+++ b/resources/jscomposition/cases/casesDetail/api/data.js
@@ -0,0 +1,9 @@
+export default {};
+
+export const getRequestId = () => requestId;
+
+export const getRequestStatus = () => request.status;
+
+export const getComentableType = () => comentable_type;
+
+export const getProcessName = () => request.process.name;
diff --git a/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue b/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue
index b348088f5c..8840094815 100644
--- a/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue
+++ b/resources/jscomposition/cases/casesDetail/components/CaseDetail.vue
@@ -12,6 +12,7 @@ import { defineComponent, ref } from "vue";
import Tabs from "./Tabs.vue";
import TaskTable from "./TaskTable.vue";
import RequestTable from "./RequestTable.vue";
+import TabHistory from "./TabHistory.vue";
export default defineComponent({
components: { Tabs },
@@ -34,7 +35,7 @@ export default defineComponent({
name: translate.t("File Manager"), href: "#file_manager", current: "file_manager", show: false, content: "",
},
{
- name: translate.t("History"), href: "#history", current: "history", show: true, content: "",
+ name: translate.t("History"), href: "#history", current: "history", show: true, content: TabHistory,
},
{
name: translate.t("Requests"), href: "#requests", current: "requests", show: true, content: RequestTable,
diff --git a/resources/jscomposition/cases/casesDetail/components/TabHistory.vue b/resources/jscomposition/cases/casesDetail/components/TabHistory.vue
new file mode 100644
index 0000000000..cbff365a6f
--- /dev/null
+++ b/resources/jscomposition/cases/casesDetail/components/TabHistory.vue
@@ -0,0 +1,36 @@
+
+
+ {{ processName }}
+
+
+
+
+
diff --git a/resources/jscomposition/cases/casesDetail/config/columns.js b/resources/jscomposition/cases/casesDetail/config/columns.js
index 21f34b20e1..ac5248a257 100644
--- a/resources/jscomposition/cases/casesDetail/config/columns.js
+++ b/resources/jscomposition/cases/casesDetail/config/columns.js
@@ -1,4 +1,4 @@
-import BadgeContainer from "../../casesMain/components/BadgeContainer.vue";
+import BadgeContainer from "../../casesMain/components/BadgesSection.vue";
import AvatarContainer from "../../casesMain/components/AvatarContainer.vue";
export default {};
diff --git a/resources/jscomposition/cases/casesDetail/edit.js b/resources/jscomposition/cases/casesDetail/edit.js
index c633a6cb01..e8cb4cd9db 100644
--- a/resources/jscomposition/cases/casesDetail/edit.js
+++ b/resources/jscomposition/cases/casesDetail/edit.js
@@ -1,8 +1,366 @@
import Vue from "vue";
import CaseDetail from "./components/CaseDetail.vue";
+import Timeline from "../../../js/components/Timeline.vue";
+
+Vue.component("Timeline", Timeline);
new Vue({
el: "#case-detail",
components: { CaseDetail },
- data() {},
+ data() {
+ return {
+ activeTab: "pending",
+ showCancelRequest: false,
+ fieldsToUpdate: [],
+ jsonData: "",
+ selectedData: "",
+ monacoLargeOptions: {
+ automaticLayout: true,
+ },
+ showJSONEditor: false,
+ data: data,
+ requestId: requestId,
+ request: request,
+ files: files,
+ refreshTasks: 0,
+ canCancel: canCancel,
+ canViewPrint: canViewPrint,
+ status: "ACTIVE",
+ userRequested: [],
+ errorLogs: errorLogs,
+ disabled: false,
+ retryDisabled: false,
+ packages: [],
+ processId: processId,
+ canViewComments: canViewComments,
+ isObjectLoading: false,
+ showTree: false,
+ showTabs: true,
+ showInfo: true,
+ };
+ },
+ computed: {
+ activeErrors() {
+ return this.request.status === "ERROR";
+ },
+ activePending() {
+ return this.request.status === "ACTIVE";
+ },
+ /**
+ * Get the list of participants in the request.
+ *
+ */
+ participants() {
+ return this.request.participants;
+ },
+ /**
+ * Request Summary - that is blank place holder if there are in progress tasks,
+ * if the request is completed it will show key value pairs.
+ *
+ */
+ showSummary() {
+ return this.request.status === "ACTIVE" || this.request.status === "COMPLETED" || this.request.status === "CANCELED";
+ },
+ /**
+ * Show tasks if request status is not completed or pending
+ *
+ */
+ showTasks() {
+ return this.request.status !== "COMPLETED" && this.request.status !== "PENDING";
+ },
+ /**
+ * If the screen summary is configured.
+ */
+ showScreenSummary() {
+ return this.request.summary_screen !== null;
+ },
+ /**
+ * Get the summary of the Request.
+ *
+ */
+ summary() {
+ return this.request.summary;
+ },
+ /**
+ * Get Screen summary
+ * */
+ screenSummary() {
+ return this.request.summary_screen;
+ },
+ /**
+ * prepare data screen
+ */
+ dataSummary() {
+ const options = {};
+ this.request.summary.forEach((option) => {
+ if (option.type === "datetime") {
+ options[option.key] = moment(option.value)
+ .tz(window.ProcessMaker.user.timezone)
+ .format("MM/DD/YYYY HH:mm");
+ } else if (option.type === "date") {
+ options[option.key] = moment(option.value)
+ .tz(window.ProcessMaker.user.timezone)
+ .format("MM/DD/YYYY");
+ } else {
+ options[option.key] = option.value;
+ }
+ });
+ return options;
+ },
+ /**
+ * If the screen request detail is configured.
+ */
+ showScreenRequestDetail() {
+ return !!this.request.request_detail_screen;
+ },
+ /**
+ * Get Screen request detail
+ */
+ screenRequestDetail() {
+ return this.request.request_detail_screen ? this.request.request_detail_screen.config : null;
+ },
+ classStatusCard() {
+ const header = {
+ ACTIVE: "active-style",
+ COMPLETED: "active-style",
+ CANCELED: "canceled-style ",
+ ERROR: "canceled-style",
+ };
+ return `card-header text-status ${header[this.request.status.toUpperCase()]}`;
+ },
+ labelDate() {
+ const label = {
+ ACTIVE: "In Progress Since",
+ COMPLETED: "Completed On",
+ CANCELED: "Canceled ",
+ ERROR: "Failed On",
+ };
+ return label[this.request.status.toUpperCase()];
+ },
+ statusDate() {
+ const status = {
+ ACTIVE: this.request.created_at,
+ COMPLETED: this.request.completed_at,
+ CANCELED: this.request.updated_at,
+ ERROR: this.request.updated_at,
+ };
+
+ return status[this.request.status.toUpperCase()];
+ },
+ statusLabel() {
+ const status = {
+ ACTIVE: this.$t("In Progress"),
+ COMPLETED: this.$t("Completed"),
+ CANCELED: this.$t("Canceled"),
+ ERROR: this.$t("Error"),
+ };
+
+ return status[this.request.status.toUpperCase()];
+ },
+ requestBy() {
+ return [this.request.user];
+ },
+ panCommentInVueOptionsComponents() {
+ return "pan-comment" in Vue.options.components;
+ },
+ },
+ mounted() {
+ this.packages = window.ProcessMaker.requestShowPackages;
+ this.listenRequestUpdates();
+ this.cleanScreenButtons();
+ this.editJsonData();
+ },
+ methods: {
+ switchTab(tab) {
+ this.activeTab = tab;
+ if (tab === "overview") {
+ this.isObjectLoading = true;
+ }
+ ProcessMaker.EventBus.$emit("tab-switched", tab);
+ },
+ switchTabInfo(tab) {
+ this.showInfo = !this.showInfo;
+ if (window.Intercom) {
+ window.Intercom("update", { hide_default_launcher: tab === "comments" });
+ }
+ },
+ onLoadedObject() {
+ this.isObjectLoading = false;
+ },
+ requestStatusClass(status) {
+ const bubbleColor = {
+ active: "text-success",
+ inactive: "text-danger",
+ error: "text-danger",
+ draft: "text-warning",
+ archived: "text-info",
+ completed: "text-primary",
+ };
+ return `fas fa-circle ${bubbleColor[status.toLowerCase()]} small`;
+ },
+ // Data editor
+ updateRequestData() {
+ const data = JSON.parse(this.jsonData);
+ ProcessMaker.apiClient.put(`requests/${this.requestId}`, {
+ data: data,
+ }).then(() => {
+ this.fieldsToUpdate.splice(0);
+ ProcessMaker.alert(this.$t("The request data was saved."), "success");
+ });
+ },
+ saveJsonData() {
+ try {
+ const value = JSON.parse(this.jsonData);
+ this.updateRequestData();
+ } catch (e) {
+ // Invalid data
+ }
+ },
+ editJsonData() {
+ this.jsonData = JSON.stringify(this.data, null, 4);
+ },
+ /**
+ * Refresh the Request details.
+ *
+ */
+ refreshRequest() {
+ this.$refs.pending.fetch();
+ this.$refs.completed.fetch();
+ ProcessMaker.apiClient.get(`requests/${this.requestId}`, {
+ params: {
+ include: "participants,user,summary,summaryScreen",
+ },
+ }).then((response) => {
+ for (const attribute in response.data) {
+ this.updateModel(this.request, attribute, response.data[attribute]);
+ }
+ this.refreshTasks++;
+ });
+ },
+ /**
+ * Update a model property.
+ *
+ */
+ updateModel(obj, prop, value, defaultValue) {
+ const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
+ value = value !== undefined ? value : (descriptor ? obj[prop] : defaultValue);
+ if (descriptor && !(descriptor.get instanceof Function)) {
+ delete obj[prop];
+ Vue.set(obj, prop, value);
+ } else if (descriptor && obj[prop] !== value) {
+ Vue.set(obj, prop, value);
+ }
+ },
+ /**
+ * Listen for Request updates.
+ *
+ */
+ listenRequestUpdates() {
+ const userId = document.head.querySelector("meta[name=\"user-id\"]").content;
+ Echo.private(`ProcessMaker.Models.User.${userId}`).notification((token) => {
+ if (token.request_id === this.requestId) {
+ this.refreshRequest();
+ }
+ });
+ },
+ /**
+ * disable buttons in screen
+ */
+ cleanScreenButtons() {
+ if (this.showScreenSummary) {
+ this.$refs.screen.config[0].items.forEach((item) => {
+ item.config.disabled = true;
+ if (item.component === "FormButton") {
+ item.config.event = "";
+ item.config.variant = `${item.config.variant} disabled`;
+ }
+ });
+ }
+ },
+ okCancel() {
+ //single click
+ if (this.disabled) {
+ return;
+ }
+ this.disabled = true;
+ ProcessMaker.apiClient.put(`requests/${this.requestId}`, {
+ status: "CANCELED",
+ }).then(() => {
+ ProcessMaker.alert(this.$t("The request was canceled."), "success");
+ window.location.reload();
+ }).catch(() => {
+ this.disabled = false;
+ });
+ },
+ onCancel() {
+ ProcessMaker.confirmModal(
+ this.$t("Caution!"),
+ this.$t("Are you sure you want cancel this request?"),
+ "",
+ () => {
+ this.okCancel();
+ },
+ );
+ },
+ completeRequest() {
+ ProcessMaker.confirmModal(
+ this.$t("Caution!"),
+ this.$t("Are you sure you want to complete this request?"),
+ "",
+ () => {
+ ProcessMaker.apiClient.put(`requests/${this.requestId}`, {
+ status: "COMPLETED",
+ }).then(() => {
+ ProcessMaker.alert(this.$t("Request Completed"), "success");
+ window.location.reload();
+ });
+ });
+ },
+ retryRequest() {
+ const apiRequest = () => {
+ this.retryDisabled = true;
+ let success = true;
+
+ ProcessMaker.apiClient.put(`requests/${this.requestId}/retry`).then((response) => {
+ if (response.status !== 200) {
+ return;
+ }
+
+ const { message } = response.data;
+ success = response.data.success || false;
+
+ if (success) {
+ if (Array.isArray(message)) {
+ for (const line of message) {
+ ProcessMaker.alert(this.$t(line), "success");
+ }
+ }
+ } else {
+ ProcessMaker.alert(this.$t("Request could not be retried"), "danger");
+ }
+ }).finally(() => setTimeout(() => window.location.reload(), success ? 3000 : 1000));
+ };
+
+ ProcessMaker.confirmModal(
+ this.$t("Confirm"),
+ this.$t("Are you sure you want to retry this request?"),
+ "default",
+ apiRequest,
+ );
+ },
+ rollback(errorTaskId, rollbackToName) {
+ ProcessMaker.confirmModal(
+ this.$t("Confirm"),
+ this.$t("Are you sure you want to rollback to the task @{{name}}? Warning! This request will continue as the current published process version.",
+ { name: rollbackToName },
+ ),
+ "default",
+ () => {
+ ProcessMaker.apiClient.post(`tasks/${errorTaskId}/rollback`).then(response => {
+ window.location.reload();
+ });
+ },
+ );
+ },
+ },
});
diff --git a/resources/views/cases/edit.blade.php b/resources/views/cases/edit.blade.php
index b95a5fba74..17f9346c91 100644
--- a/resources/views/cases/edit.blade.php
+++ b/resources/views/cases/edit.blade.php
@@ -21,5 +21,17 @@
@endsection
@section('js')
+
@endsection