From fe25a1f607da35f63a6f2efbd3fca3ccdc91d5f2 Mon Sep 17 00:00:00 2001
From: tangoyankee
Date: Tue, 12 Nov 2024 12:11:26 -0500
Subject: [PATCH] 1254 Create Artifact Record as part of Project Creation -
Added artifact creation endpoint and returning artifact id from server to
client - Create a form through which users may submit and attach documents
(client)
Co-authored-by: horatio
Co-authored-by: tangoyankee
---
.github/workflows/production.yml | 5 +-
.github/workflows/qa.yml | 4 +-
.github/workflows/staging.yml | 3 +-
client/.gitignore | 4 +-
.../app/components/packages/projects/new.js | 64 -----
.../{packages => }/projects/new.hbs | 32 ++-
client/app/components/projects/new.js | 116 ++++++++
.../projects-new-add-contacts.hbs} | 22 +-
.../projects-new-attached-documents.hbs | 22 ++
.../projects/projects-new-attachments.hbs | 132 +++++++++
.../projects/projects-new-attachments.js | 49 ++++
.../projects/projects-new-information.hbs | 64 +++++
.../projects-new-project-description.hbs | 16 ++
client/app/controllers/projects.js | 20 +-
client/app/helpers/optionset.js | 48 ++--
client/app/models/artifact.js | 3 +
client/app/models/project-new.js | 124 ++++++++
client/app/models/project.js | 90 ++++--
client/app/optionsets/applicant.js | 12 +
client/app/optionsets/project.js | 24 ++
client/app/routes/projects/new.js | 28 +-
client/app/services/file-manager.js | 61 ++--
client/app/templates/project.hbs | 22 +-
client/app/templates/projects.hbs | 19 +-
client/app/templates/projects/new.hbs | 2 +-
.../submittable-projects-new-form.js | 23 ++
server/package.json | 6 +-
server/src/artifacts/artifacts.service.ts | 31 ++
server/src/config/config.service.ts | 4 +-
server/src/contact/contact.controller.ts | 14 +-
server/src/projects/projects.attrs.ts | 2 +
server/src/projects/projects.controller.ts | 25 +-
server/src/projects/projects.module.ts | 6 +-
server/src/projects/projects.service.ts | 75 ++++-
server/src/provider/msal.provider.ts | 8 +-
server/yarn.lock | 267 ++++++++++++++----
36 files changed, 1154 insertions(+), 293 deletions(-)
delete mode 100644 client/app/components/packages/projects/new.js
rename client/app/components/{packages => }/projects/new.hbs (60%)
create mode 100644 client/app/components/projects/new.js
rename client/app/components/{packages/projects/projects-new-information.hbs => projects/projects-new-add-contacts.hbs} (87%)
create mode 100644 client/app/components/projects/projects-new-attached-documents.hbs
create mode 100644 client/app/components/projects/projects-new-attachments.hbs
create mode 100644 client/app/components/projects/projects-new-attachments.js
create mode 100644 client/app/components/projects/projects-new-information.hbs
create mode 100644 client/app/components/projects/projects-new-project-description.hbs
create mode 100644 client/app/models/project-new.js
diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml
index d85a680f..d86bc547 100644
--- a/.github/workflows/production.yml
+++ b/.github/workflows/production.yml
@@ -28,7 +28,7 @@ jobs:
deploy-server:
name: 🚀 Deploy server
needs: test-client
- environment:
+ environment:
name: production
url: https://applicants-api.nycplanningdigital.com
runs-on: ubuntu-latest
@@ -46,6 +46,7 @@ jobs:
appdir: server
env:
HD_FEATURE_FLAG_SELF_SERVICE: ${{ vars.FEATURE_FLAG_SELF_SERVICE }}
+ HD_LETTER_FILETYPE_UUID: ${{ vars.LETTER_FILETYPE_UUID }}
HD_ADO_PRINCIPAL: ${{ secrets.ADO_PRINCIPAL }}
HD_AUTHORITY_HOST_URL: ${{ secrets.AUTHORITY_HOST_URL }}
HD_CITYPAY_AGENCYID: ${{ secrets.CITYPAY_AGENCYID }}
@@ -107,7 +108,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 12.x
- - name: Install application dependencies
+ - name: Install application dependencies
working-directory: client
run: yarn install --immutable --immutable-cache --check-cache
- name: Build client
diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml
index b6c79aad..936cf67d 100644
--- a/.github/workflows/qa.yml
+++ b/.github/workflows/qa.yml
@@ -46,6 +46,7 @@ jobs:
appdir: server
env:
HD_FEATURE_FLAG_SELF_SERVICE: ${{ vars.FEATURE_FLAG_SELF_SERVICE }}
+ HD_LETTER_FILETYPE_UUID: ${{ vars.LETTER_FILETYPE_UUID }}
HD_ADO_PRINCIPAL: ${{ secrets.ADO_PRINCIPAL }}
HD_AUTHORITY_HOST_URL: ${{ secrets.AUTHORITY_HOST_URL }}
HD_CITYPAY_AGENCYID: ${{ secrets.CITYPAY_AGENCYID }}
@@ -102,7 +103,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 12.x
- - name: Install application dependencies
+ - name: Install application dependencies
working-directory: client
run: yarn install --immutable --immutable-cache --check-cache
- name: Build client
@@ -119,4 +120,3 @@ jobs:
--site ${{secrets.NETLIFY_SITE_ID}} \
--auth ${{secrets.NETLIFY_AUTH_TOKEN}} \
--message "${{ github.event.head_commit.message }}"
-
diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml
index b2326ace..7fb12dcc 100644
--- a/.github/workflows/staging.yml
+++ b/.github/workflows/staging.yml
@@ -50,6 +50,7 @@ jobs:
appdir: server
env:
HD_FEATURE_FLAG_SELF_SERVICE: ${{ vars.FEATURE_FLAG_SELF_SERVICE }}
+ HD_LETTER_FILETYPE_UUID: ${{ vars.LETTER_FILETYPE_UUID }}
HD_ADO_PRINCIPAL: ${{ secrets.ADO_PRINCIPAL }}
HD_AUTHORITY_HOST_URL: ${{ secrets.AUTHORITY_HOST_URL }}
HD_CLIENT_ID: ${{ secrets.CLIENT_ID }}
@@ -94,7 +95,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 12.x
- - name: Install application dependencies
+ - name: Install application dependencies
working-directory: client
run: yarn install
- name: Build client
diff --git a/client/.gitignore b/client/.gitignore
index 5108621b..3da6f6e2 100644
--- a/client/.gitignore
+++ b/client/.gitignore
@@ -1,12 +1,12 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
-/dist/
+**/dist
/tmp/
# dependencies
/bower_components/
-/node_modules/
+**/node_modules
# misc
/.env*
diff --git a/client/app/components/packages/projects/new.js b/client/app/components/packages/projects/new.js
deleted file mode 100644
index 9d68fd65..00000000
--- a/client/app/components/packages/projects/new.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import Component from '@glimmer/component';
-import { action } from '@ember/object';
-import { inject as service } from '@ember/service';
-import SubmittableProjectsNewForm from '../../../validations/submittable-projects-new-form';
-
-export default class ProjectsNewFormComponent extends Component {
- validations = {
- SubmittableProjectsNewForm,
- };
-
- @service
- router;
-
- @service
- store;
-
- @action
- async submitPackage() {
- const primaryContactInput = {
- first: this.args.package.primaryContactFirstName,
- last: this.args.package.primaryContactLastName,
- email: this.args.package.primaryContactEmail,
- phone: this.args.package.primaryContactPhone,
- role: 'contact',
- };
-
- const applicantInput = {
- first: this.args.package.applicantFirstName,
- last: this.args.package.applicantLastName,
- email: this.args.package.applicantEmail,
- phone: this.args.package.applicantPhone,
- role: 'applicant',
- };
-
- const contactInputs = [primaryContactInput, applicantInput];
- try {
- const contactPromises = contactInputs
- .map((contact) => this.store.queryRecord('contact',
- {
- email: contact.email,
- includeAllStatusCodes: true,
- }));
-
- const contacts = await Promise.all(contactPromises);
-
- const verifiedContactPromises = contacts.map((contact, index) => {
- if (contact.id === '-1') {
- const contactInput = contactInputs[index];
- const contactModel = this.store.createRecord('contact', {
- firstname: contactInput.first,
- lastname: contactInput.last,
- emailaddress1: contactInput.email,
- telephone1: contactInput.phone,
- });
- return contactModel.save();
- }
- return contact;
- });
- await Promise.all(verifiedContactPromises);
- } catch {
- console.log('Save new project package error');
- }
- }
-}
diff --git a/client/app/components/packages/projects/new.hbs b/client/app/components/projects/new.hbs
similarity index 60%
rename from client/app/components/packages/projects/new.hbs
rename to client/app/components/projects/new.hbs
index 589721b5..2ac761fe 100644
--- a/client/app/components/packages/projects/new.hbs
+++ b/client/app/components/projects/new.hbs
@@ -20,24 +20,44 @@
Planning will contact you with the next steps.
-
+
+
+
+
+
+
+
-
diff --git a/client/app/components/projects/new.js b/client/app/components/projects/new.js
new file mode 100644
index 00000000..16e2f237
--- /dev/null
+++ b/client/app/components/projects/new.js
@@ -0,0 +1,116 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import SubmittableProjectsNewForm from '../../validations/submittable-projects-new-form';
+import { optionset } from '../../helpers/optionset';
+import config from '../../config/environment';
+
+export default class ProjectsNewFormComponent extends Component {
+ validations = {
+ SubmittableProjectsNewForm,
+ };
+
+ @service
+ router;
+
+ @service
+ store;
+
+ get boroughOptions() {
+ return optionset(['project', 'boroughs', 'list']);
+ }
+
+ get applicantOptions() {
+ return optionset(['applicant', 'dcpApplicantType', 'list']);
+ }
+
+ @action
+ async submitProject() {
+ const primaryContactInput = {
+ first: this.args.package.primaryContactFirstName,
+ last: this.args.package.primaryContactLastName,
+ email: this.args.package.primaryContactEmail,
+ phone: this.args.package.primaryContactPhone,
+ };
+
+ const applicantInput = {
+ first: this.args.package.applicantFirstName,
+ last: this.args.package.applicantLastName,
+ email: this.args.package.applicantEmail,
+ phone: this.args.package.applicantPhone,
+ };
+
+ const contactInputs = [primaryContactInput, applicantInput];
+
+ try {
+ const contactPromises = contactInputs.map((contact) => this.store.queryRecord('contact', {
+ email: contact.email,
+ includeAllStatusCodes: true,
+ }));
+
+ const contacts = await Promise.all(contactPromises);
+
+ const verifiedContactPromises = contacts.map((contact, index) => {
+ if (contact.id === '-1') {
+ const contactInput = contactInputs[index];
+ const contactModel = this.store.createRecord('contact', {
+ firstname: contactInput.first,
+ lastname: contactInput.last,
+ emailaddress1: contactInput.email,
+ telephone1: contactInput.phone,
+ });
+ return contactModel.save();
+ }
+ return contact;
+ });
+
+ const [verifiedPrimaryContact, verifiedApplicant] = await Promise.all(
+ verifiedContactPromises,
+ );
+
+ const authSessionRaw = localStorage.getItem('ember_simple_auth-session');
+
+ if (authSessionRaw === null) {
+ throw new Error('unauthorized');
+ }
+ const authSession = JSON.parse(authSessionRaw);
+ const {
+ authenticated: { access_token: accessToken },
+ } = authSession;
+ if (accessToken === undefined) {
+ throw new Error('unauthorized');
+ }
+
+ const response = await fetch(`${config.host}/projects`, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ attributes: {
+ dcpProjectname: this.args.package.projectName,
+ dcpBorough: this.args.package.borough.code,
+ dcpApplicanttype: this.args.package.applicantType.code,
+ dcpProjectbrief: this.args.package.projectBrief,
+ _dcpApplicantadministratorCustomerValue:
+ verifiedPrimaryContact.id,
+ _dcpApplicantCustomerValue: verifiedApplicant.id,
+ },
+ },
+ }),
+ });
+ const { data: project } = await response.json();
+
+ this.args.package.saveAttachedFiles(project.attributes['dcp-artifactsid']);
+
+ this.router.transitionTo('project', project.id);
+
+ } catch {
+ /* eslint-disable-next-line no-console */
+ console.error('Error while creating project');
+ }
+ }
+}
diff --git a/client/app/components/packages/projects/projects-new-information.hbs b/client/app/components/projects/projects-new-add-contacts.hbs
similarity index 87%
rename from client/app/components/packages/projects/projects-new-information.hbs
rename to client/app/components/projects/projects-new-add-contacts.hbs
index 50a16594..ca67a752 100644
--- a/client/app/components/packages/projects/projects-new-information.hbs
+++ b/client/app/components/projects/projects-new-add-contacts.hbs
@@ -1,19 +1,4 @@
{{#let @form as |form|}}
-
-
-
- Project Name
-
-
-
-
-
Primary Contact
This contact is the person who will respond to public inquiries regarding the application.
@@ -70,13 +55,15 @@
id={{Q.questionId}}
@showCounter={{true}}
@maxlength="10"
+ placeholder='2125551212'
/>
Applicant
The owner, entity, or representative of the project described in this application.
- If using a company or organization, please split the name in the "First Name" and "Last Name" fields. For example, "NYC
+ If using a company or organization, please split the name in the "First Name" and "Last Name" fields. For example,
+ "NYC
Planning" would be: First Name: NYC, Last Name: Planning
@@ -132,7 +119,8 @@
id={{Q.questionId}}
@showCounter={{true}}
@maxlength="10"
+ placeholder='2125551212'
/>
-{{/let}}
\ No newline at end of file
+{{/let}}
diff --git a/client/app/components/projects/projects-new-attached-documents.hbs b/client/app/components/projects/projects-new-attached-documents.hbs
new file mode 100644
index 00000000..7f66f035
--- /dev/null
+++ b/client/app/components/projects/projects-new-attached-documents.hbs
@@ -0,0 +1,22 @@
+{{#let @form as |form|}}
+<@form.Section @title='Attached Documents'>
+
+ Please attach the required items list on the
+
+ Informational Interest Meeting checklist
+ in one PDF document. The maximum
+ size for a document is 50MB.
+
+
+
+
+
+@form.Section>
+
+{{/let}}
diff --git a/client/app/components/projects/projects-new-attachments.hbs b/client/app/components/projects/projects-new-attachments.hbs
new file mode 100644
index 00000000..996aa3d5
--- /dev/null
+++ b/client/app/components/projects/projects-new-attachments.hbs
@@ -0,0 +1,132 @@
+
+
+
+ Attachments
+
+
+
+ {{#each @fileManager.existingFiles as |file idx|}}
+
+
+
+
+
+ {{file.timeCreated}}
+
+
+
+
+
+
+
+
+
+ {{/each}}
+
+
+ {{#if (or @fileManager.filesToDelete @fileManager.filesToUpload.files)}}
+ {{#if @fileManager.existingFiles}}
+
+ {{/if}}
+
+
+ To be
+ {{if @fileManager.filesToUpload.files 'uploaded'}}
+ {{if (and @fileManager.filesToDelete @fileManager.filesToUpload.files) '/'}}
+ {{if @fileManager.filesToDelete 'deleted'}}
+ when you save the project:
+
+ {{/if}}
+
+
+ {{#each @fileManager.filesToDelete as |file idx|}}
+
+
+
+
+ {{file.name}}
+
+
+
+
+ TO BE DELETED
+
+
+
+
+
+
+
+
+
+ {{/each}}
+
+
+
+ {{#each @fileManager.filesToUpload.files as |file idx|}}
+
+
+
+
+ {{file.name}}
+
+
+
+
+ TO BE ADDED
+
+
+
+
+
+
+
+
+
+ {{/each}}
+
+
+
+
+ Choose Files
+
+
+
+ The size limit for each file is 50 MB. You can upload up to 1 GB of files.
+
+
+
\ No newline at end of file
diff --git a/client/app/components/projects/projects-new-attachments.js b/client/app/components/projects/projects-new-attachments.js
new file mode 100644
index 00000000..840c1b80
--- /dev/null
+++ b/client/app/components/projects/projects-new-attachments.js
@@ -0,0 +1,49 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+/**
+ * This component wires a fileManager to the attachments UI.
+ * @param {Artifact Model} artifact
+ */
+export default class ProjectsNewAttachmentsComponent extends Component {
+ get fileManager() {
+ // should be an instance of FileManager
+ return this.args.fileManager;
+ }
+
+ @action
+ markFileForDeletion(file) {
+ this.fileManager.markFileForDeletion(file);
+
+ this.args.package.documents = this.fileManager.existingFiles;
+ }
+
+ @action
+ unmarkFileForDeletion(file) {
+ this.fileManager.unMarkFileForDeletion(file);
+ }
+
+ // This action doesn't perform any file selection.
+ // That part is automatically handled by the
+ // ember-file-upload addon.
+ // Here we manually increment the number of files to
+ // upload to update the fileManager isDirty state.
+ @action
+ trackFileForUpload() {
+ /* eslint-disable-next-line no-console */
+ this.fileManager.trackFileForUpload();
+ this.args.package.documents = [
+ ...this.args.package.documents,
+ ...this.fileManager.filesToUpload.files,
+ ];
+ }
+
+ @action
+ deselectFileForUpload(file) {
+ this.fileManager.deselectFileForUpload(file);
+
+ this.args.package.documents = this.args.package.documents.filter(
+ (document) => document !== file,
+ );
+ }
+}
diff --git a/client/app/components/projects/projects-new-information.hbs b/client/app/components/projects/projects-new-information.hbs
new file mode 100644
index 00000000..4f5f0b6e
--- /dev/null
+++ b/client/app/components/projects/projects-new-information.hbs
@@ -0,0 +1,64 @@
+{{#let @form as |form|}}
+
+
+
+ Project Name
+
+
+
+
+
+ Borough
+
+
+ {{borough.label}}
+
+ {{#if form.errors.borough}}
+ {{#each form.errors.borough.validation as |message|}}
+ {{message}}
+ {{/each}}
+ {{/if}}
+
+
+
+ Applicant Type
+
+
+ Identify if you represent a public agency or are a private entity.
+
+
+ {{dcpApplicantType.label}}
+
+ {{#if form.errors.applicantType}}
+ {{#each form.errors.applicantType.validation as |message|}}
+ {{message}}
+
+ {{/each}}
+ {{/if}}
+
+
+{{/let}}
+
diff --git a/client/app/components/projects/projects-new-project-description.hbs b/client/app/components/projects/projects-new-project-description.hbs
new file mode 100644
index 00000000..c489e857
--- /dev/null
+++ b/client/app/components/projects/projects-new-project-description.hbs
@@ -0,0 +1,16 @@
+{{#let @form as |form|}}
+
+
+
+ Please replace information in the brackets to the best of your ability.
+
+
+
+
+
+{{/let}}
diff --git a/client/app/controllers/projects.js b/client/app/controllers/projects.js
index 3311fd31..466c879c 100644
--- a/client/app/controllers/projects.js
+++ b/client/app/controllers/projects.js
@@ -1,5 +1,7 @@
import Controller from '@ember/controller';
import { sort } from '@ember/object/computed';
+import config from 'client/config/environment';
+import { tracked } from '@glimmer/tracking';
import {
STATUSCODE,
DCPVISIBILITY,
@@ -20,19 +22,23 @@ export function packageIsToDo(projectPackages) {
}
export default class ProjectsController extends Controller {
+ @tracked
+ selfServiceFlagOn = config.featureFlagSelfService;
// TODO: organize this business logic as computed properties on the projects model
// Projects awaiting the applicant's submission
// (includes active projects with packages that haven't been submitted)
get toDoProjects() {
// Check that at least ONE of the packages is currently editable
- return this.model.filter((project) => packageIsToDo(project.pasPackages)
- || packageIsToDo(project.rwcdsPackages)
- || (packageIsToDo(project.landusePackages))
- || (packageIsToDo(project.easPackages))
- || (packageIsToDo(project.scopeOfWorkDraftPackages))
- || (packageIsToDo(project.eisPackages))
- || (packageIsToDo(project.technicalMemoPackages)));
+ return this.model.filter(
+ (project) => packageIsToDo(project.pasPackages)
+ || packageIsToDo(project.rwcdsPackages)
+ || packageIsToDo(project.landusePackages)
+ || packageIsToDo(project.easPackages)
+ || packageIsToDo(project.scopeOfWorkDraftPackages)
+ || packageIsToDo(project.eisPackages)
+ || packageIsToDo(project.technicalMemoPackages),
+ );
}
// Projects in NYC Planning's hands
diff --git a/client/app/helpers/optionset.js b/client/app/helpers/optionset.js
index a5d3aa29..9b8b284c 100644
--- a/client/app/helpers/optionset.js
+++ b/client/app/helpers/optionset.js
@@ -38,6 +38,7 @@ const OPTIONSET_LOOKUP = {
applicant: {
dcpState: APPLICANT_OPTIONSETS.DCPSTATE,
dcpType: APPLICANT_OPTIONSETS.DCPTYPE,
+ dcpApplicantType: APPLICANT_OPTIONSETS.DCP_APPLICANTTYPE,
},
bbl: {
boroughs: BBL_OPTIONSETS.BOROUGHS,
@@ -62,6 +63,7 @@ const OPTIONSET_LOOKUP = {
dcpVisibility: PROJECT_OPTIONSETS.DCPVISIBILITY,
statuscode: PROJECT_OPTIONSETS.STATUSCODE,
equityReportFields: YES_NO_UNSURE_SMALLINT,
+ boroughs: PROJECT_OPTIONSETS.BOROUGHS,
},
landuseForm: {
dcpOtherparties: YES_NO,
@@ -78,22 +80,29 @@ const OPTIONSET_LOOKUP = {
dcpHistoricdistrict: YES_NO,
dcpDesignation: LANDUSE_FORM_OPTIONSETS.DCPDESIGNATION,
dcpDisposition: LANDUSE_FORM_OPTIONSETS.DCPDISPOSITION,
- dcpProjecthousingplanudaap: LANDUSE_FORM_OPTIONSETS.DCPPROJECTHOUSINGPLANUDAAP,
+ dcpProjecthousingplanudaap:
+ LANDUSE_FORM_OPTIONSETS.DCPPROJECTHOUSINGPLANUDAAP,
dcpMannerofdisposition: LANDUSE_FORM_OPTIONSETS.DCPMANNEROFDISPOSITION,
- dcpRestrictionsandconditionsdispositiontab: LANDUSE_FORM_OPTIONSETS.DCPRESTRICTIONSANDCONDITIONSDISPOSITIONTAB,
+ dcpRestrictionsandconditionsdispositiontab:
+ LANDUSE_FORM_OPTIONSETS.DCPRESTRICTIONSANDCONDITIONSDISPOSITIONTAB,
dcpOfficespaceleaseopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpAcquisitionopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpSiteselectionopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
- dcpIndicatetypeoffacility: LANDUSE_FORM_OPTIONSETS.DCPINDICATETYPEOFFACILITY,
+ dcpIndicatetypeoffacility:
+ LANDUSE_FORM_OPTIONSETS.DCPINDICATETYPEOFFACILITY,
dcpExistingfacilityproposedtoremainopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
- dcpExistingfacilityproposedtoremainandexpand: COMMON_OPTIONSETS.YES_NO_INTEGER,
- dcpExistingfacilityreplacementinanewlocation: COMMON_OPTIONSETS.YES_NO_INTEGER,
+ dcpExistingfacilityproposedtoremainandexpand:
+ COMMON_OPTIONSETS.YES_NO_INTEGER,
+ dcpExistingfacilityreplacementinanewlocation:
+ COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpNewfacilityopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpIsprojectlistedinstatementofneedsopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
- dcpDidboroughpresidentproposealternativesite: COMMON_OPTIONSETS.YES_NO_INTEGER,
+ dcpDidboroughpresidentproposealternativesite:
+ COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpLegalinstrument: COMMON_OPTIONSETS.YES_NO_UNSURE,
dcpRelatedacquisitionofpropertyopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
- dcpOnlychangetheeliminationofamappedbutunimp: LANDUSE_FORM_OPTIONSETS.DCPONLYCHANGETHEELIMINATIONOFAMAPPEDBUTUNIMP,
+ dcpOnlychangetheeliminationofamappedbutunimp:
+ LANDUSE_FORM_OPTIONSETS.DCPONLYCHANGETHEELIMINATIONOFAMAPPEDBUTUNIMP,
dcpYesmappedbutunimprovedstreetelimated: COMMON_OPTIONSETS.YES_NO,
dcpEstablishstreetopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpEstablishparkopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
@@ -110,10 +119,11 @@ const OPTIONSET_LOOKUP = {
dcpChangestreetgradeopt: COMMON_OPTIONSETS.YES_NO_INTEGER,
dcpChangeeasement: LANDUSE_FORM_OPTIONSETS.DCPEASEMENTS,
dcpTypedisposition: LANDUSE_FORM_OPTIONSETS.DCPTYPEDISPOSITION,
- dcpTotalzoningareatoberezoned: LANDUSE_FORM_OPTIONSETS.DCPTOTALZONINGAREATOBEREZONED,
- dcpHaurbandevelopmentactionareaudaap: COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
+ dcpTotalzoningareatoberezoned:
+ LANDUSE_FORM_OPTIONSETS.DCPTOTALZONINGAREATOBEREZONED,
+ dcpHaurbandevelopmentactionareaudaap:
+ COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
dcpHddispositionofurbanrenewalsite: COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
-
},
rwcdsForm: {
dcpHasprojectchangedsincesubmissionofthepas: YES_NO,
@@ -148,19 +158,23 @@ const OPTIONSET_LOOKUP = {
dcpApplicantispublicagencyactions: YES_NO,
dcpIstheactiontoauthorizeorpermitanopenuse: YES_NO,
dcpIstheactiontoauthorizeacommercial: YES_NO,
- dcpIndicatewhetheractionisamodification: LANDUSE_ACTION_OPTIONSETS.DCPINDICATEWHETHERACTIONISAMODIFICATION,
+ dcpIndicatewhetheractionisamodification:
+ LANDUSE_ACTION_OPTIONSETS.DCPINDICATEWHETHERACTIONISAMODIFICATION,
dcpModsubjectto197c: YES_NO,
- dcpPreviouslyapprovedactioncode: LANDUSE_ACTION_OPTIONSETS.DCPPREVIOUSLYAPPROVEDACTIONCODE,
+ dcpPreviouslyapprovedactioncode:
+ LANDUSE_ACTION_OPTIONSETS.DCPPREVIOUSLYAPPROVEDACTIONCODE,
actions: AFFECTED_ZONING_RESOLUTION_ACTION,
},
sitedatahForm: {
dcpSitetobedisposed: YES_NO,
},
landuseGeography: {
- dcpIsthesiteimprovedunimprovedorpartlyimp: LANDUSE_GEOGRAPHY_OPTIONSETS.DCPISTHESITEIMPROVEDUNIMPROVEDORPARTLYIMP,
+ dcpIsthesiteimprovedunimprovedorpartlyimp:
+ LANDUSE_GEOGRAPHY_OPTIONSETS.DCPISTHESITEIMPROVEDUNIMPROVEDORPARTLYIMP,
},
zoningMapChange: {
- dcpExistingzoningdistrictvaluenew: ZONING_MAP_CHANGE_OPTIONSETS.DCPEXISTINGZONINGDISTRICTVALUE,
+ dcpExistingzoningdistrictvaluenew:
+ ZONING_MAP_CHANGE_OPTIONSETS.DCPEXISTINGZONINGDISTRICTVALUE,
},
relatedAction: {
// Actually a boolean field in CRM, not picklist
@@ -169,8 +183,10 @@ const OPTIONSET_LOOKUP = {
ceqrInvoiceQuestionnaire: {
dcpSquarefeet: CEQR_INVOICE_QUESTIONNAIRE_OPTIONSETS.SQUARE_FEET,
dcpIsthesoleaapplicantagovtagency: COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
- dcpProjectspolelyconsistactionsnotmeasurable: COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
- dcpProjectmodificationtoapreviousapproval: COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
+ dcpProjectspolelyconsistactionsnotmeasurable:
+ COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
+ dcpProjectmodificationtoapreviousapproval:
+ COMMON_OPTIONSETS.YES_NO_PICKLIST_CODE,
dcpRespectivedecrequired: COMMON_OPTIONSETS.YES_NO,
},
};
diff --git a/client/app/models/artifact.js b/client/app/models/artifact.js
index f6baabaf..56667272 100644
--- a/client/app/models/artifact.js
+++ b/client/app/models/artifact.js
@@ -33,6 +33,9 @@ export default class ArtifactModel extends Model {
@belongsTo('project', { async: false })
project;
+ @belongsTo('project-new', { async: false })
+ projectNew
+
@attr()
dcpName;
diff --git a/client/app/models/project-new.js b/client/app/models/project-new.js
new file mode 100644
index 00000000..9e86c7fd
--- /dev/null
+++ b/client/app/models/project-new.js
@@ -0,0 +1,124 @@
+import Model, { attr, belongsTo } from '@ember-data/model';
+import { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import FileManager from '../services/file-manager';
+
+export default class ProjectNew extends Model {
+ createFileQueue() {
+ if (this.fileManager) {
+ this.fileManager.existingFiles = this.documents;
+ } else {
+ const fileQueue = this.fileQueue.create(`artifact${this.id}`);
+
+ this.fileManager = new FileManager(
+ this.id,
+ 'artifact',
+ this.documents,
+ [],
+ fileQueue,
+ this.session
+ );
+ }
+ }
+
+ // Since file upload doesn't perform requests through
+ // an Ember Model save() process, it doesn't automatically
+ // hydrate the package.adapterError property. When an error occurs
+ // during upload we have to manually hydrate a custom error property
+ // to trigger the error box displayed to the user.
+ @tracked
+ fileUploadErrors = null;
+
+ @service
+ session;
+
+ @service
+ fileQueue;
+
+ @belongsTo('projects', { async: false })
+ projects;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ projectName;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ borough;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ applicantType;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ primaryContactFirstName;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ primaryContactLastName;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ primaryContactEmail;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ primaryContactPhone;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ applicantFirstName;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ applicantLastName;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ applicantEmail;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ applicantPhone;
+
+ @attr('string', {
+ defaultValue: '',
+ })
+ projectBrief;
+
+ @attr('string', {
+ defaultValue: () => [],
+ })
+ documents;
+
+ async saveAttachedFiles(instanceId) {
+ try {
+ await this.fileManager.save(instanceId);
+ } catch (e) {
+ console.log('Error saving files: ', e); // eslint-disable-line no-console
+
+ // See comment on the tracked fileUploadError property
+ // definition above.
+ this.fileUploadErrors = [
+ {
+ code: 'UPLOAD_DOC_FAILED',
+ title: 'Failed to upload documents',
+ detail:
+ 'An error occured while uploading your documents. Please refresh and retry.',
+ },
+ ];
+ }
+ }
+}
diff --git a/client/app/models/project.js b/client/app/models/project.js
index dcf7ae07..670dbab0 100644
--- a/client/app/models/project.js
+++ b/client/app/models/project.js
@@ -3,7 +3,7 @@ import { optionset } from '../helpers/optionset';
export default class ProjectModel extends Model {
// The human-readable, descriptive name.
- // e.g. "Marcus Garvey Blvd Project"
+ // e.g. 'Marcus Garvey Blvd Project'
@attr dcpProjectname;
// The CRM Project 5-letter ID.
@@ -85,18 +85,22 @@ export default class ProjectModel extends Model {
@hasMany('milestone', { async: false })
milestones;
- get isDirty () {
+ get isDirty() {
return this.hasDirtyAttributes || (this.artifact && this.artifact.isDirty);
}
get publicStatusGeneralPublicProject() {
- const isGeneralPublic = this.dcpVisibility === optionset(['project', 'dcpVisibility', 'code', 'GENERAL_PUBLIC']);
+ const isGeneralPublic = this.dcpVisibility
+ === optionset(['project', 'dcpVisibility', 'code', 'GENERAL_PUBLIC']);
return this.dcpPublicstatus && isGeneralPublic;
}
get pasPackages() {
const pasPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'PAS_PACKAGE']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'PAS_PACKAGE']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -105,7 +109,10 @@ export default class ProjectModel extends Model {
get rwcdsPackages() {
const rwcdsPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'RWCDS']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'RWCDS']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -124,7 +131,10 @@ export default class ProjectModel extends Model {
get draftLandusePackages() {
const landusePackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'DRAFT_LU_PACKAGE']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'DRAFT_LU_PACKAGE']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -133,7 +143,10 @@ export default class ProjectModel extends Model {
get filedLandusePackages() {
const landusePackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'FILED_LU_PACKAGE']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'FILED_LU_PACKAGE']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -142,23 +155,26 @@ export default class ProjectModel extends Model {
get postCertLUPackages() {
return this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'POST_CERT_LU']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'POST_CERT_LU']),
+ )
.sortBy('dcpPackageversion')
.reverse();
}
get easPackages() {
- const easPackages = [
- ...this.filedEasPackages,
- ...this.draftEasPackages,
- ];
+ const easPackages = [...this.filedEasPackages, ...this.draftEasPackages];
return easPackages;
}
get draftEasPackages() {
const easPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'DRAFT_EAS']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'DRAFT_EAS']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -167,7 +183,10 @@ export default class ProjectModel extends Model {
get filedEasPackages() {
const easPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'FILED_EAS']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'FILED_EAS']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -176,7 +195,15 @@ export default class ProjectModel extends Model {
get scopeOfWorkDraftPackages() {
const scopeOfWorkDraftPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(['package', 'dcpPackagetype', 'code', 'DRAFT_SCOPE_OF_WORK']))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset([
+ 'package',
+ 'dcpPackagetype',
+ 'code',
+ 'DRAFT_SCOPE_OF_WORK',
+ ]),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -184,19 +211,17 @@ export default class ProjectModel extends Model {
}
get eisPackages() {
- const eisPackages = [
- ...this.feisPackages,
- ...this.deisPackages,
- ];
+ const eisPackages = [...this.feisPackages, ...this.deisPackages];
return eisPackages;
}
get deisPackages() {
const eisPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(
- ['package', 'dcpPackagetype', 'code', 'PDEIS'],
- ))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'PDEIS']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -205,9 +230,10 @@ export default class ProjectModel extends Model {
get feisPackages() {
const eisPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(
- ['package', 'dcpPackagetype', 'code', 'EIS'],
- ))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'EIS']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -216,9 +242,10 @@ export default class ProjectModel extends Model {
get technicalMemoPackages() {
const technicalMemoPackages = this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(
- ['package', 'dcpPackagetype', 'code', 'TECHNICAL_MEMO'],
- ))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'TECHNICAL_MEMO']),
+ )
.sortBy('dcpPackageversion')
.reverse();
@@ -227,9 +254,10 @@ export default class ProjectModel extends Model {
get workingPackages() {
return this.packages
- .filter((projectPackage) => projectPackage.dcpPackagetype === optionset(
- ['package', 'dcpPackagetype', 'code', 'WORKING_PACKAGE'],
- ))
+ .filter(
+ (projectPackage) => projectPackage.dcpPackagetype
+ === optionset(['package', 'dcpPackagetype', 'code', 'WORKING_PACKAGE']),
+ )
.sortBy('dcpPackageversion')
.reverse();
}
diff --git a/client/app/optionsets/applicant.js b/client/app/optionsets/applicant.js
index 44da738f..72843b16 100644
--- a/client/app/optionsets/applicant.js
+++ b/client/app/optionsets/applicant.js
@@ -248,9 +248,21 @@ export const DCPSTATE = {
},
};
+export const DCP_APPLICANTTYPE = {
+ OTHER_PUBLIC_AGENCY: {
+ label: 'Other Public Agency',
+ code: 717170001,
+ },
+ PRIVATE: {
+ label: 'Private',
+ code: 717170002,
+ },
+};
+
const APPLICANT_OPTIONSETS = {
DCPTYPE,
DCPSTATE,
+ DCP_APPLICANTTYPE,
};
export default APPLICANT_OPTIONSETS;
diff --git a/client/app/optionsets/project.js b/client/app/optionsets/project.js
index 17687208..0393eae6 100644
--- a/client/app/optionsets/project.js
+++ b/client/app/optionsets/project.js
@@ -71,10 +71,34 @@ export const STATUSCODE = {
},
};
+export const BOROUGHS = {
+ BRONX: {
+ code: 717170000,
+ label: 'Bronx',
+ },
+ BROOKLYN: {
+ code: 717170002,
+ label: 'Brooklyn',
+ },
+ MANHATTAN: {
+ code: 717170001,
+ label: 'Manhattan',
+ },
+ QUEENS: {
+ code: 717170003,
+ label: 'Queens',
+ },
+ STATEN_ISLAND: {
+ code: 717170004,
+ label: 'Staten Island',
+ },
+};
+
const PROJECT_OPTIONSETS = {
DCPPUBLICSTATUS,
DCPVISIBILITY,
STATUSCODE,
+ BOROUGHS,
};
export default PROJECT_OPTIONSETS;
diff --git a/client/app/routes/projects/new.js b/client/app/routes/projects/new.js
index d2673806..f49105d6 100644
--- a/client/app/routes/projects/new.js
+++ b/client/app/routes/projects/new.js
@@ -1,21 +1,21 @@
import Route from '@ember/routing/route';
-import { inject as service } from '@ember/service';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
+import { inject as service } from '@ember/service';
+
+export default class ProjectsNewRoute extends Route.extend(
+ AuthenticatedRouteMixin,
+) {
+ authenticationRoute = '/';
-export default class ProjectsNewRoute extends Route.extend(AuthenticatedRouteMixin) {
- @service store;
+ @service
+ store
async model() {
- return {
- projectName: '',
- primaryContactFirstName: '',
- primaryContactLastName: '',
- primaryContactEmail: '',
- primaryContactPhone: '',
- applicantFirstName: '',
- applicantLastName: '',
- applicantEmail: '',
- applicantPhone: '',
- };
+ const projectsNewForm = this.store.createRecord('project-new');
+
+ projectsNewForm.id = `new${self.crypto.randomUUID()}`;
+ projectsNewForm.createFileQueue();
+
+ return projectsNewForm;
}
}
diff --git a/client/app/services/file-manager.js b/client/app/services/file-manager.js
index c516b586..2bfc26dc 100644
--- a/client/app/services/file-manager.js
+++ b/client/app/services/file-manager.js
@@ -12,7 +12,10 @@ export default class FileManager {
filesToUpload, // EmberFileUpload Queue Object
session,
) {
- console.assert(entityType === 'package' || entityType === 'artifact', "entityType must be 'package' or 'artifact'");
+ console.assert(
+ entityType === 'package' || entityType === 'artifact',
+ "entityType must be 'package' or 'artifact'"
+ );
this.recordId = recordId;
this.entityType = entityType;
@@ -61,18 +64,23 @@ export default class FileManager {
this.numFilesToUpload -= 1;
}
- async uploadFiles() {
+ async uploadFiles(instanceId = this.recordId) {
for (let i = 0; i < this.filesToUpload.files.length; i += 1) {
- await this.filesToUpload.files[i].upload(`${ENV.host}/documents/${this.entityType}`, { // eslint-disable-line
- fileKey: 'file',
- headers: {
- Authorization: `Bearer ${this.session.data.authenticated.access_token}`,
- },
- data: {
- instanceId: this.recordId,
- entityName: this.entityType === 'artifact' ? 'dcp_artifacts' : 'dcp_package',
- },
- });
+ await this.filesToUpload.files[i].upload(
+ `${ENV.host}/documents/${this.entityType}`,
+ {
+ // eslint-disable-line
+ fileKey: 'file',
+ headers: {
+ Authorization: `Bearer ${this.session.data.authenticated.access_token}`,
+ },
+ data: {
+ instanceId,
+ entityName:
+ this.entityType === 'artifact' ? 'dcp_artifacts' : 'dcp_package',
+ },
+ }
+ );
}
}
@@ -82,21 +90,26 @@ export default class FileManager {
// TODO: If this is not possible, rework this to be a
// POST request to a differently named endpoint, like
// deleteDocument
- return Promise.all(this.filesToDelete.map((file) => fetch(
- `${ENV.host}/documents?serverRelativeUrl=${file.serverRelativeUrl}`, {
- method: 'DELETE',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${this.session.data.authenticated.access_token}`,
- },
- },
- )));
+ return Promise.all(
+ this.filesToDelete.map((file) =>
+ fetch(
+ `${ENV.host}/documents?serverRelativeUrl=${file.serverRelativeUrl}`,
+ {
+ method: 'DELETE',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.session.data.authenticated.access_token}`,
+ },
+ }
+ )
+ )
+ );
}
- async save() {
+ async save(instanceId) {
// See TODO at top of this file.
- await this.uploadFiles();
+ await this.uploadFiles(instanceId);
await this.deleteFiles();
diff --git a/client/app/templates/project.hbs b/client/app/templates/project.hbs
index e34e0560..0bc7a924 100644
--- a/client/app/templates/project.hbs
+++ b/client/app/templates/project.hbs
@@ -23,10 +23,10 @@
Status
-
- {{#each
+
+ {{#each
(filter-by 'statuscode' 'In Progress' @model.milestones)
- as |milestone|}}
+ as |milestone|}}
{{milestone.dcpMilestoneValue}}
{{/each}}
@@ -50,13 +50,13 @@
- Milestone planned completion dates are an estimate based on the amount of time that is
- generally expected for the Department to review materials submitted to it, and for the
- applicant to prepare and submit those materials for review. This date is not guaranteed,
- may not accurately reflect timelines for review, and is subject to change.
- Note that all Filed EAS or DEIS review periods are dependent on other City agencies'
- reviews of materials and are less predictable as a result. Agency’s target duration are the
- goal durations for milestones of this type for all projects. For the most accurate information
+ Milestone planned completion dates are an estimate based on the amount of time that is
+ generally expected for the Department to review materials submitted to it, and for the
+ applicant to prepare and submit those materials for review. This date is not guaranteed,
+ may not accurately reflect timelines for review, and is subject to change.
+ Note that all Filed EAS or DEIS review periods are dependent on other City agencies'
+ reviews of materials and are less predictable as a result. Agency’s target duration are the
+ goal durations for milestones of this type for all projects. For the most accurate information
concerning timelines and review, please contact the lead DCP planner assigned to this project.
@@ -115,7 +115,7 @@
Milestones
-
*=Planned completion dates may be different from the agency’s target duration based
+
*=Planned completion dates may be different from the agency’s target duration based
on application quality, complexity, and overall volume awaiting review.
{{! end left/main column }}
+