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

+ + + + + + +{{/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 }} +