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 @@ + + + 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