From 8ec6441a5b3dd76de40ee98d1111052e920d8e12 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Sat, 15 May 2021 20:32:42 -0400 Subject: [PATCH] DDAH View (#581) * Add react-based DDAH acknowledge page --- .../controllers/public/ddahs_controller.rb | 22 ++ backend/app/mailers/ddah_mailer.rb | 4 +- backend/app/services/offer_service.rb | 4 +- backend/app/views/ddahs/ddah-template.html | 3 + backend/config/routes.rb | 1 + frontend/src/api/defs/types/raw-types.ts | 10 + frontend/src/api/mockAPI/public-routes.js | 9 +- frontend/src/views/public/ddahs/index.tsx | 196 +++++++++++++ frontend/src/views/public/ddahs/view-ddah.css | 274 ++++++++++++++++++ frontend/src/views/public/routes/index.tsx | 4 + 10 files changed, 522 insertions(+), 5 deletions(-) create mode 100644 frontend/src/views/public/ddahs/index.tsx create mode 100644 frontend/src/views/public/ddahs/view-ddah.css diff --git a/backend/app/controllers/public/ddahs_controller.rb b/backend/app/controllers/public/ddahs_controller.rb index ed6314ea6..475c559d6 100644 --- a/backend/app/controllers/public/ddahs_controller.rb +++ b/backend/app/controllers/public/ddahs_controller.rb @@ -43,6 +43,28 @@ def accept render_success {} end + # /public/ddahs//details + def details + return unless valid_ddah?(url_token: params[:ddah_id]) + + render_success( + { + approved_date: @ddah.approved_date, + accepted_date: @ddah.accepted_date, + revised_date: @ddah.revised_date, + emailed_date: @ddah.emailed_date, + position_code: @ddah.assignment.position.position_code, + position_title: @ddah.assignment.position.position_title, + status: + if @ddah.accepted_date.blank? + 'unacknowledged' + else + 'acknowledged' + end + } + ) + end + # /public/ddahs//view def view return unless valid_ddah?(url_token: params[:ddah_id]) diff --git a/backend/app/mailers/ddah_mailer.rb b/backend/app/mailers/ddah_mailer.rb index 19ed1a2f2..fd6fa50ae 100644 --- a/backend/app/mailers/ddah_mailer.rb +++ b/backend/app/mailers/ddah_mailer.rb @@ -50,9 +50,9 @@ def generate_vars(ddah) @ta_coordinator_email = Rails.application.config.ta_coordinator_email # TODO: This seems too hard-coded. Is there another way to get the route? @url = - "#{Rails.application.config.base_url}/public/ddahs/#{ + "#{Rails.application.config.base_url}/#/public/ddahs/#{ ddah.url_token - }/view" + }" @subs = { ddah: @ddah, diff --git a/backend/app/services/offer_service.rb b/backend/app/services/offer_service.rb index 5b25eefbe..45d1f90eb 100644 --- a/backend/app/services/offer_service.rb +++ b/backend/app/services/offer_service.rb @@ -21,9 +21,9 @@ def subs ta_coordinator_email: @offer.ta_coordinator_email, # TODO: This seems too hard-coded. Is there another way to get the route? url: - "#{Rails.application.config.base_url}/public/contracts/#{ + "#{Rails.application.config.base_url}/#/public/contracts/#{ @offer.url_token - }/view", + }", nag_count: @offer.nag_count, status_message: status_message, changes_summary: changes_from_previous diff --git a/backend/app/views/ddahs/ddah-template.html b/backend/app/views/ddahs/ddah-template.html index 97b02109b..cbcfc4a79 100644 --- a/backend/app/views/ddahs/ddah-template.html +++ b/backend/app/views/ddahs/ddah-template.html @@ -37,6 +37,9 @@ text-align: right; font-weight: normal; } + .appointment-summary td { + text-align: left; + } .letter-head p { margin-bottom: 0.5in; } diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 9d0d6145f..dc6235caa 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -252,6 +252,7 @@ def matches?(request) end resources :ddahs, format: nil, only: %i[show] do get :view, format: false, to: 'ddahs#view' + get :details, format: false, to: 'ddahs#details' post :accept, format: false, to: 'ddahs#accept' end resources :postings, only: %i[show] do diff --git a/frontend/src/api/defs/types/raw-types.ts b/frontend/src/api/defs/types/raw-types.ts index 81aa54bc8..8958f2ada 100644 --- a/frontend/src/api/defs/types/raw-types.ts +++ b/frontend/src/api/defs/types/raw-types.ts @@ -108,6 +108,16 @@ export interface RawDdah extends HasId { duties: RawDuty[]; } +export interface RawDdahDetails { + approved_date: string | null; + accepted_date: string | null; + revised_date: string | null; + emailed_date: string | null; + position_code: string; + position_title: string | null; + status: "acknowledged" | "unacknowledged"; +} + export interface RawPosting extends HasId { name: string; intro_text: string | null; diff --git a/frontend/src/api/mockAPI/public-routes.js b/frontend/src/api/mockAPI/public-routes.js index 6f04de483..c59dfe130 100644 --- a/frontend/src/api/mockAPI/public-routes.js +++ b/frontend/src/api/mockAPI/public-routes.js @@ -16,6 +16,13 @@ export const publicRoutes = { summary: "View a ddah with an accept dialog", returns: wrappedPropTypes.any, }), + "/public/ddahs/:token/details": documentCallback({ + func: () => { + throw new Error("Not implemented in Mock API"); + }, + summary: "Get JSON information about a DDAH", + returns: wrappedPropTypes.any, + }), "/public/contracts/:token": documentCallback({ func: () => { throw new Error("Not implemented in Mock API"); @@ -54,7 +61,7 @@ export const publicRoutes = { }), }, post: { - "/public/ddahs/:ddah_id/accept": documentCallback({ + "/public/ddahs/:token/accept": documentCallback({ func: () => { throw new Error("Not implemented in Mock API"); }, diff --git a/frontend/src/views/public/ddahs/index.tsx b/frontend/src/views/public/ddahs/index.tsx new file mode 100644 index 000000000..947b5b4ad --- /dev/null +++ b/frontend/src/views/public/ddahs/index.tsx @@ -0,0 +1,196 @@ +import React from "react"; +import { Button, Modal, Spinner } from "react-bootstrap"; +import { useParams } from "react-router-dom"; +import { RawDdahDetails } from "../../../api/defs/types"; +import { apiGET, apiPOST } from "../../../libs/api-utils"; + +import "./view-ddah.css"; + +function capitalize(text: string) { + return text + .split(/\s+/) + .map((word) => word.charAt(0).toLocaleUpperCase() + word.slice(1)) + .join(" "); +} + +export function DdahView() { + const params = useParams<{ url_token?: string } | null>(); + const url_token = params?.url_token; + const [ddah, setDdah] = React.useState(null); + const [decision, setDecision] = React.useState<"accept" | null>(null); + const [signature, setSignature] = React.useState(""); + const [ + confirmationDialogVisible, + setConfirmationDialogVisible, + ] = React.useState(false); + const [waiting, setWaiting] = React.useState(false); + + // If the offer's status has been set to accepted/rejected/withdrawn, + // no further interaction with the offer is permitted. + const frozen = ["acknowledged"].includes(ddah?.status || ""); + + React.useEffect(() => { + async function fetchOffer() { + try { + const details = await apiGET( + `/public/ddahs/${url_token}/details`, + true + ); + setDdah(details); + } catch (e) { + console.warn(e); + } + } + fetchOffer(); + }, [setDdah, url_token]); + + async function submitDecision() { + if (decision == null) { + throw new Error("Cannot submit a `null` decision"); + } + const data = { decision, signature: signature || null }; + await apiPOST(`/public/ddahs/${url_token}/${decision}`, data, true); + } + async function confirmClicked() { + setWaiting(true); + await submitDecision(); + setWaiting(false); + window.location.reload(true); + } + + if (url_token == null) { + return Unknown URL token.; + } + + if (ddah == null) { + return Loading...; + } + + const position_code = ddah.position_code; + const status = ddah.status; + + return ( +
+
+

+ Description of Duties and Allocation of Hours for{" "} + {position_code} +

+
+
+
+

+ +

+

+ Status: + + {" "} + {capitalize(status)} + +

+
+

+ Please acknowledge receipt of this Description of + Duties and Allocation of Hours form below. If there + are any issues with your described duties or you + need further clarification, please contact your + course supervisor(s). +

+
+ setDecision("accept")} + type="radio" + value="accept" + id="radio-accept" + name="decision" + disabled={frozen} + /> + +
+
+ + + setSignature(e.target.value) + } + /> +
.
+
+
+ +
+
+
+
+
+ +
+
+ setConfirmationDialogVisible(false)} + > + + Acknowledge DDAH + + + Are you sure you want to acknowledge the DDAH? + + + + + + +
+ ); +} diff --git a/frontend/src/views/public/ddahs/view-ddah.css b/frontend/src/views/public/ddahs/view-ddah.css new file mode 100644 index 000000000..119b4df4d --- /dev/null +++ b/frontend/src/views/public/ddahs/view-ddah.css @@ -0,0 +1,274 @@ +* { + box-sizing: border-box; +} +#app-body, +.contract-page { + height: 100%; + display: flex; + flex-direction: column; +} +html, +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + position: relative; + font-family: sans-serif; +} +body { + display: flex; + flex-direction: column; +} +.contract-page .header, +.contract-page .content, +.contract-page .footer { + position: relative; +} + +.contract-page .header { + padding: 5px; + background-color: #313131; + color: white; + z-index: 100; +} + +.contract-page .header h1 { + margin: 0; + text-align: center; + font-size: larger; +} + +.contract-page #logout { + float: right; +} + +.contract-page .content { + flex-grow: 1; + background-color: #f2f5f5; + display: flex; + flex-direction: row; +} + +.contract-page .footer { + background-color: #bcbcbc; +} + +.contract-page .footer p { + padding: 8px; + margin: 0px; +} + +.contract-page .accept, +.contract-page .accepted { + color: #008e00; +} + +.contract-page .reject, +.contract-page .rejected { + color: #e23400; +} + +.contract-page .pending, +.contract-page .provisional, +.contract-page .withdrawn { + color: #fda400; +} + +.contract-page .capitalize { + text-transform: capitalize; +} + +.contract-page input[type="radio"] + label { + font-weight: bold; + cursor: pointer; + display: inline-block; + position: relative; + padding: 4px; + margin-left: 25px; + transition: color 0.2s; + user-select: none; +} + +.contract-page input[type="radio"]:focus + label { + outline: thin dotted; + outline-offset: -3px; +} + +.contract-page input[type="radio"] + label::before { + content: ""; + display: block; + text-align: center; + position: absolute; + height: 20px; + width: 20px; + right: 100%; + top: 50%; + transform: translateY(-50%); + font-size: 18px; + font-weight: bold; + border: 1px solid black; + border-radius: 2px; +} + +.contract-page input[type="radio"]:disabled + label { + color: #929292; +} + +.contract-page input[type="radio"]:checked + label::before { + color: inherit; + content: "✓"; +} + +.contract-page input[type="radio"]:checked + label { + color: #008e00; +} + +.contract-page input[type="radio"]:checked + label[for="radio-reject"] { + color: #e23400; +} + +.contract-page input[type="radio"] + label:hover { + text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.57); +} + +.contract-page input[type="radio"]:not(:disabled) + label:hover::before { + background-color: #ffffff; + text-shadow: none; + box-shadow: 0px 0px 3px 0px rgba(95, 95, 95, 0.55); +} + +.contract-page input[type="radio"] { + width: 0px; + height: 0px; + opacity: 0; + /* display: none; */ + -moz-appearance: unset; +} + +.contract-page #radio-accept:checked ~ .signature { + opacity: 1; +} + +.contract-page #radio-accept:checked ~ .signature input { + display: inline-block; +} + +.contract-page #radio-accept:checked ~ .signature .input-placeholder { + display: none; +} + +.contract-page .signature input { + display: none; + width: 20em; +} + +.contract-page .signature { + opacity: 0; + flex-basis: 100%; + transition: opacity 0.3s; + padding: 4px; + display: flex; + justify-content: center; +} + +.contract-page .decision-container { + display: flex; + justify-content: center; + flex-wrap: wrap; +} + +.contract-page .button { + text-decoration: none; + border: 1px solid transparent; + user-select: none; + vertical-align: middle; + border-radius: 3px; + background-color: #08b9ef; + color: #ffffff; + padding: 6px 12px; + text-transform: uppercase; + cursor: pointer; + display: inline-block; +} + +.contract-page .button:hover { + background-color: #00c3ff; +} + +.contract-page .button:active { + background-color: #05a0d0; +} + +.contract-page .button:focus { + outline: thin dotted; + outline-offset: -3px; +} + +.contract-page input[type="text"] { + background-color: white; + line-height: 1.5; + padding: 0em 0.3em; + font-size: 1em; + color: black; + border: 1px solid #d1d1d1; + border-radius: 4px; + width: 20em; +} + +.contract-page input[type="text"]:focus { + border: 1px solid #08b9ef; +} + +.contract-page .input-placeholder { + display: inline-block; + width: 20em; + background-color: white; + line-height: 1.5; + border: 1px solid transparent; + border-radius: 4px; + color: white; +} + +.contract-page .decision { + position: relative; + flex-grow: 0; + flex-basis: 400px; + padding: 10px; + text-align: center; + box-shadow: 0px 0px 15px 2px rgba(0, 0, 0, 0.44); + z-index: 50; +} + +.contract-page .decision h3 { + font-weight: normal; + font-size: 1em; +} + +.contract-page .contract-view { + background-color: white; + flex-grow: 1; + overflow: hidden; +} + +.contract-page iframe { + width: 100%; + height: 100%; + background: transparent; + border: none; +} + +@media only screen and (max-width: 1000px) { + .contract-page .content { + flex-direction: column-reverse; + } + .contract-page .decision { + flex-basis: auto; + border: none; + } +} + +.contract-page .spinner-surround { + padding-right: 4px; + vertical-align: 5%; +} diff --git a/frontend/src/views/public/routes/index.tsx b/frontend/src/views/public/routes/index.tsx index 7a0a7d477..54aed13c1 100644 --- a/frontend/src/views/public/routes/index.tsx +++ b/frontend/src/views/public/routes/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { ContractView } from "../contracts"; +import { DdahView } from "../ddahs"; import { PostingView } from "../postings"; export function PublicRoutes() { @@ -9,6 +10,9 @@ export function PublicRoutes() { + + +