diff --git a/moped-database/migrations/1704744986000_substantial_completion_date/down.sql b/moped-database/migrations/1704744986000_substantial_completion_date/down.sql new file mode 100644 index 0000000000..a657013843 --- /dev/null +++ b/moped-database/migrations/1704744986000_substantial_completion_date/down.sql @@ -0,0 +1,625 @@ +-- revert to version 1700515730257_fix_delete_component_bug +DROP VIEW public.project_list_view cascade; + +CREATE OR REPLACE VIEW public.project_list_view +AS WITH project_person_list_lookup AS ( + SELECT + mpp.project_id, + string_agg(DISTINCT concat(mu.first_name, ' ', mu.last_name, ':', mpr.project_role_name), ','::text) AS project_team_members + FROM moped_proj_personnel mpp + JOIN moped_users mu ON mpp.user_id = mu.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE mpp.is_deleted = false + AND mppr.is_deleted = false + GROUP BY mpp.project_id + ), funding_sources_lookup AS ( + SELECT + mpf_1.project_id, + string_agg(mfs.funding_source_name, ', '::text) AS funding_source_name + FROM moped_proj_funding mpf_1 + LEFT JOIN moped_fund_sources mfs ON mpf_1.funding_source_id = mfs.funding_source_id + WHERE mpf_1.is_deleted = false + GROUP BY mpf_1.project_id + ), project_type_lookup AS ( + SELECT + mpt.project_id, + string_agg(mt.type_name, ', '::text) AS type_name + FROM moped_project_types mpt + LEFT JOIN moped_types mt ON mpt.project_type_id = mt.type_id AND mpt.is_deleted = false + GROUP BY mpt.project_id + ), child_project_lookup AS ( + SELECT jsonb_agg(children.project_id) AS children_project_ids, + children.parent_project_id AS parent_id + FROM moped_project AS children + JOIN moped_project AS parent ON (parent.project_id = children.parent_project_id) + WHERE children.is_deleted = false + GROUP BY parent_id + ), work_activities AS ( + SELECT + project_id, + string_agg(task_order_objects.task_order_object ->> 'display_name'::text, + ', '::text) AS task_order_names, + string_agg(task_order_objects.task_order_object ->> 'task_order'::text, + ', '::text) AS task_order_names_short, + jsonb_agg(DISTINCT task_order_objects.task_order_object) FILTER (WHERE task_order_objects.task_order_object IS NOT NULL) AS task_orders, + string_agg(DISTINCT mpwa.workgroup_contractor, + ', '::text) AS workgroup_contractors, + string_agg(mpwa.contract_number, + ', '::text) AS contract_numbers FROM moped_proj_work_activity mpwa + LEFT JOIN LATERAL jsonb_array_elements(mpwa.task_orders) task_order_objects (task_order_object) ON TRUE WHERE 1 = 1 + AND mpwa.is_deleted = FALSE + GROUP BY + mpwa.project_id + ), moped_proj_components_subtypes AS ( + SELECT + mpc.project_id, + string_agg(DISTINCT mc.component_name_full, ', '::text) AS components + FROM moped_proj_components mpc + LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id + WHERE mpc.is_deleted = FALSE + GROUP BY mpc.project_id + ) + SELECT + mp.project_id, + mp.project_name, + mp.project_description, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + mp.updated_at, + current_phase.phase_name as current_phase, + current_phase.phase_key as current_phase_key, + current_phase.phase_name_simple as current_phase_simple, + ppll.project_team_members, + me.entity_name AS project_sponsor, + mel.entity_name AS project_lead, + mpps.name AS public_process_status, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + proj_notes.project_note, + proj_notes.date_created as project_note_date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders, + (SELECT project_name + FROM moped_project + WHERE project_id = mp.parent_project_id + ) as parent_project_name, + cpl.children_project_ids, + string_agg(DISTINCT me2.entity_name, ', '::text) AS project_partner, + (SELECT JSON_AGG(json_build_object('signal_id', feature_signals.signal_id, 'knack_id', feature_signals.knack_id, 'location_name', feature_signals.location_name, 'signal_type', feature_signals.signal_type, 'id', feature_signals.id)) + FROM moped_proj_components components + LEFT JOIN feature_signals + ON (feature_signals.component_id = components.project_component_id) + WHERE TRUE + AND components.is_deleted = false + AND components.project_id = mp.project_id + AND feature_signals.signal_id is not null + AND feature_signals.is_deleted = false + ) as project_feature, + fsl.funding_source_name, + ptl.type_name, + ( -- get the date of the construction phase with the earliest start date + SELECT min(phases.phase_start) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 9 -- phase_id 9 is construction + AND phases.is_deleted = false + ) AS construction_start_date, + ( -- get the date of the completion phase with the latest end date + SELECT max(phases.phase_end) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 11 -- phase_id 11 is complete + AND phases.is_deleted = false + ) AS completion_end_date, + ( -- get me a list of the inspectors for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Inspector'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_inspector, + ( -- get me a list of the designers for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Designer'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_designer, + ( -- get me all of the tags added to a project + SELECT string_agg(tags.name, ', '::text) AS string_agg + FROM moped_proj_tags ptags + JOIN moped_tags tags ON ptags.tag_id = tags.id + WHERE 1 = 1 + AND ptags.is_deleted = false + AND ptags.project_id = mp.project_id + GROUP BY ptags.project_id) AS project_tags, + concat(added_by_user.first_name, ' ', added_by_user.last_name) AS added_by, + mpcs.components + FROM moped_project mp + LEFT JOIN project_person_list_lookup ppll ON mp.project_id = ppll.project_id + LEFT JOIN funding_sources_lookup fsl ON fsl.project_id = mp.project_id + LEFT JOIN project_type_lookup ptl ON ptl.project_id = mp.project_id + LEFT JOIN moped_entity me ON me.entity_id = mp.project_sponsor + LEFT JOIN moped_entity mel ON mel.entity_id = mp.project_lead_id + LEFT JOIN moped_proj_partners mpp2 ON mp.project_id = mpp2.project_id AND mpp2.is_deleted = false + LEFT JOIN moped_entity me2 ON mpp2.entity_id = me2.entity_id + LEFT JOIN work_activities on work_activities.project_id = mp.project_id + LEFT JOIN moped_users added_by_user ON mp.added_by = added_by_user.user_id + LEFT JOIN current_phase_view current_phase on mp.project_id = current_phase.project_id + LEFT JOIN moped_public_process_statuses mpps ON mpps.id = mp.public_process_status_id + LEFT JOIN child_project_lookup cpl on cpl.parent_id = mp.project_id + LEFT JOIN moped_proj_components_subtypes mpcs on mpcs.project_id = mp.project_id + LEFT JOIN LATERAL + ( + SELECT mpn.project_note, mpn.date_created + FROM moped_proj_notes mpn + WHERE mpn.project_id = mp.project_id AND mpn.project_note_type = 2 AND mpn.is_deleted = false + ORDER BY mpn.date_created DESC + LIMIT 1 + ) as proj_notes on true + WHERE + mp.is_deleted = false + GROUP BY + mp.project_id, + mp.project_name, + mp.project_description, + ppll.project_team_members, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + me.entity_name, + mel.entity_name, + mp.updated_at, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + current_phase.phase_name, + current_phase.phase_key, + current_phase.phase_name_simple, + ptl.type_name, + mpcs.components, + fsl.funding_source_name, + added_by_user.first_name, + added_by_user.last_name, + mpps.name, + cpl.children_project_ids, + proj_notes.project_note, + proj_notes.date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders; + +-- revert to version 1700515730257_fix_delete_component_bug +CREATE OR REPLACE VIEW component_arcgis_online_view AS ( + SELECT + mpc.project_id, + comp_geography.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + comp_geography.length_feet_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + subcomponents.subcomponents, + work_types.work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.is_deleted is_project_component_deleted, + plv.is_deleted is_project_deleted, + mpc.interim_project_component_id, + mpc.completion_date, + mpc.srts_id, + mpc.location_description, + plv.project_name, + plv.project_description, + plv.ecapris_subproject_id, + plv.updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple as component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + COALESCE(mph.phase_name, current_phase.phase_name) AS current_phase_name, + COALESCE(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partner, + plv.task_order_names, + plv.funding_source_name, + plv.type_name, + plv.project_note, + plv.project_note_date_created, + plv.construction_start_date, + plv.completion_end_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.knack_project_id as knack_data_tracker_project_record_id, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text as project_url, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text || '?tab=map&project_component_id=' || mpc.project_component_id :: text as component_url, + added_by + FROM + moped_proj_components mpc + LEFT JOIN ( + -- group feature properties by project component ID + SELECT + component_id AS project_component_id, + STRING_AGG(DISTINCT id :: text, ', ') AS feature_ids, + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(geography) + ) + ):: json AS "geometry", + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(line_geography) + ) + ):: json AS "line_geometry", + STRING_AGG(DISTINCT signal_id :: text, ', ') AS signal_ids, + SUM(length_feet) as length_feet_total + FROM + ( + -- union all features + SELECT + id, + feature_signals.component_id, + feature_signals.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_signals.geography, 7):: geometry + ) AS line_geography, + feature_signals.signal_id, + NULL AS length_feet + FROM + feature_signals + WHERE + feature_signals.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_street_segments.component_id, + feature_street_segments.geography :: geometry, + feature_street_segments.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_street_segments + WHERE + feature_street_segments.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_intersections.component_id, + feature_intersections.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_intersections.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_intersections + WHERE + feature_intersections.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_points.component_id, + feature_drawn_points.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_drawn_points.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_drawn_points + WHERE + feature_drawn_points.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography :: geometry, + feature_drawn_lines.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_drawn_lines + WHERE + feature_drawn_lines.is_deleted = FALSE + ) feature_union + GROUP BY + component_id + ) comp_geography ON comp_geography.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group council districts by project component id + SELECT + component_id AS project_component_id, + STRING_AGG( + DISTINCT council_district_id :: text, + ', ' + ) AS council_districts + FROM + features_council_districts + LEFT JOIN features ON features.id = features_council_districts.feature_id + WHERE + features.is_deleted = FALSE + GROUP BY + component_id + ) council_districts ON council_districts.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group subcomponents by project component id + SELECT + project_component_id, + string_agg(ms.subcomponent_name, ', ') subcomponents + FROM + moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + GROUP BY + project_component_id + ) subcomponents ON subcomponents.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group work types by project component id + SELECT + project_component_id, + string_agg(mwt.name, ', ') work_types + FROM + moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + GROUP BY + project_component_id + ) work_types ON work_types.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group project component tags by project component id + SELECT + project_component_id, + string_agg(mct.type || ' - ' || mct.name, ', ') component_tags + FROM + moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + GROUP BY + project_component_id + ) component_tags ON component_tags.project_component_id = mpc.project_component_id + LEFT JOIN project_list_view plv ON plv.project_id = mpc.project_id + LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id + LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id + LEFT JOIN moped_components mc ON mc.component_id = mpc.component_id +WHERE + mpc.is_deleted = FALSE + AND plv.is_deleted = FALSE +); + +CREATE OR REPLACE VIEW component_arcgis_online_view AS ( + SELECT + mpc.project_id, + comp_geography.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + comp_geography.length_feet_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + subcomponents.subcomponents, + work_types.work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.is_deleted is_project_component_deleted, + plv.is_deleted is_project_deleted, + mpc.interim_project_component_id, + mpc.completion_date, + mpc.srts_id, + mpc.location_description, + plv.project_name, + plv.project_description, + plv.ecapris_subproject_id, + plv.updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple as component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + COALESCE(mph.phase_name, current_phase.phase_name) AS current_phase_name, + COALESCE(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partner, + plv.task_order_names, + plv.funding_source_name, + plv.type_name, + plv.project_note, + plv.project_note_date_created, + plv.construction_start_date, + plv.completion_end_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.knack_project_id as knack_data_tracker_project_record_id, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text as project_url, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text || '?tab=map&project_component_id=' || mpc.project_component_id :: text as component_url, + added_by + FROM + moped_proj_components mpc + LEFT JOIN ( + -- group feature properties by project component ID + SELECT + component_id AS project_component_id, + STRING_AGG(DISTINCT id :: text, ', ') AS feature_ids, + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(geography) + ) + ):: json AS "geometry", + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(line_geography) + ) + ):: json AS "line_geometry", + STRING_AGG(DISTINCT signal_id :: text, ', ') AS signal_ids, + SUM(length_feet) as length_feet_total + FROM + ( + -- union all features + SELECT + id, + feature_signals.component_id, + feature_signals.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_signals.geography, 7):: geometry + ) AS line_geography, + feature_signals.signal_id, + NULL AS length_feet + FROM + feature_signals + WHERE + feature_signals.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_street_segments.component_id, + feature_street_segments.geography :: geometry, + feature_street_segments.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_street_segments + WHERE + feature_street_segments.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_intersections.component_id, + feature_intersections.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_intersections.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_intersections + WHERE + feature_intersections.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_points.component_id, + feature_drawn_points.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_drawn_points.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_drawn_points + WHERE + feature_drawn_points.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography :: geometry, + feature_drawn_lines.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_drawn_lines + WHERE + feature_drawn_lines.is_deleted = FALSE + ) feature_union + GROUP BY + component_id + ) comp_geography ON comp_geography.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group council districts by project component id + SELECT + component_id AS project_component_id, + STRING_AGG( + DISTINCT council_district_id :: text, + ', ' + ) AS council_districts + FROM + features_council_districts + LEFT JOIN features ON features.id = features_council_districts.feature_id + WHERE + features.is_deleted = FALSE + GROUP BY + component_id + ) council_districts ON council_districts.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group subcomponents by project component id + SELECT + project_component_id, + string_agg(ms.subcomponent_name, ', ') subcomponents + FROM + moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + GROUP BY + project_component_id + ) subcomponents ON subcomponents.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group work types by project component id + SELECT + project_component_id, + string_agg(mwt.name, ', ') work_types + FROM + moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + GROUP BY + project_component_id + ) work_types ON work_types.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group project component tags by project component id + SELECT + project_component_id, + string_agg(mct.type || ' - ' || mct.name, ', ') component_tags + FROM + moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + GROUP BY + project_component_id + ) component_tags ON component_tags.project_component_id = mpc.project_component_id + LEFT JOIN project_list_view plv ON plv.project_id = mpc.project_id + LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id + LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id + LEFT JOIN moped_components mc ON mc.component_id = mpc.component_id +WHERE + mpc.is_deleted = FALSE + AND plv.is_deleted = FALSE +); diff --git a/moped-database/migrations/1704744986000_substantial_completion_date/up.sql b/moped-database/migrations/1704744986000_substantial_completion_date/up.sql new file mode 100644 index 0000000000..bb11f65fc0 --- /dev/null +++ b/moped-database/migrations/1704744986000_substantial_completion_date/up.sql @@ -0,0 +1,453 @@ +DROP VIEW project_list_view CASCADE; +CREATE OR REPLACE VIEW public.project_list_view +AS WITH project_person_list_lookup AS ( + SELECT + mpp.project_id, + string_agg(DISTINCT concat(mu.first_name, ' ', mu.last_name, ':', mpr.project_role_name), ','::text) AS project_team_members + FROM moped_proj_personnel mpp + JOIN moped_users mu ON mpp.user_id = mu.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE mpp.is_deleted = false + AND mppr.is_deleted = false + GROUP BY mpp.project_id + ), funding_sources_lookup AS ( + SELECT + mpf_1.project_id, + string_agg(mfs.funding_source_name, ', '::text) AS funding_source_name + FROM moped_proj_funding mpf_1 + LEFT JOIN moped_fund_sources mfs ON mpf_1.funding_source_id = mfs.funding_source_id + WHERE mpf_1.is_deleted = false + GROUP BY mpf_1.project_id + ), project_type_lookup AS ( + SELECT + mpt.project_id, + string_agg(mt.type_name, ', '::text) AS type_name + FROM moped_project_types mpt + LEFT JOIN moped_types mt ON mpt.project_type_id = mt.type_id AND mpt.is_deleted = false + GROUP BY mpt.project_id + ), child_project_lookup AS ( + SELECT jsonb_agg(children.project_id) AS children_project_ids, + children.parent_project_id AS parent_id + FROM moped_project AS children + JOIN moped_project AS parent ON (parent.project_id = children.parent_project_id) + WHERE children.is_deleted = false + GROUP BY parent_id + ), work_activities AS ( + SELECT + project_id, + string_agg(task_order_objects.task_order_object ->> 'display_name'::text, + ', '::text) AS task_order_names, + string_agg(task_order_objects.task_order_object ->> 'task_order'::text, + ', '::text) AS task_order_names_short, + jsonb_agg(DISTINCT task_order_objects.task_order_object) FILTER (WHERE task_order_objects.task_order_object IS NOT NULL) AS task_orders, + string_agg(DISTINCT mpwa.workgroup_contractor, + ', '::text) AS workgroup_contractors, + string_agg(mpwa.contract_number, + ', '::text) AS contract_numbers FROM moped_proj_work_activity mpwa + LEFT JOIN LATERAL jsonb_array_elements(mpwa.task_orders) task_order_objects (task_order_object) ON TRUE WHERE 1 = 1 + AND mpwa.is_deleted = FALSE + GROUP BY + mpwa.project_id + ), moped_proj_components_subtypes AS ( + SELECT + mpc.project_id, + string_agg(DISTINCT mc.component_name_full, ', '::text) AS components + FROM moped_proj_components mpc + LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id + WHERE mpc.is_deleted = FALSE + GROUP BY mpc.project_id + ) + SELECT + mp.project_id, + mp.project_name, + mp.project_description, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + mp.updated_at, + current_phase.phase_name as current_phase, + current_phase.phase_key as current_phase_key, + current_phase.phase_name_simple as current_phase_simple, + ppll.project_team_members, + me.entity_name AS project_sponsor, + mel.entity_name AS project_lead, + mpps.name AS public_process_status, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + proj_notes.project_note, + proj_notes.date_created as project_note_date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders, + (SELECT project_name + FROM moped_project + WHERE project_id = mp.parent_project_id + ) as parent_project_name, + cpl.children_project_ids, + string_agg(DISTINCT me2.entity_name, ', '::text) AS project_partner, + (SELECT JSON_AGG(json_build_object('signal_id', feature_signals.signal_id, 'knack_id', feature_signals.knack_id, 'location_name', feature_signals.location_name, 'signal_type', feature_signals.signal_type, 'id', feature_signals.id)) + FROM moped_proj_components components + LEFT JOIN feature_signals + ON (feature_signals.component_id = components.project_component_id) + WHERE TRUE + AND components.is_deleted = false + AND components.project_id = mp.project_id + AND feature_signals.signal_id is not null + AND feature_signals.is_deleted = false + ) as project_feature, + fsl.funding_source_name, + ptl.type_name, + ( -- get the date of the construction phase with the earliest start date + SELECT min(phases.phase_start) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 9 -- phase_id 9 is construction + AND phases.is_deleted = false + ) AS construction_start_date, + ( -- get the date of the completion phase with the latest end date + SELECT max(phases.phase_end) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 11 -- phase_id 11 is complete + AND phases.is_deleted = false + ) AS completion_end_date, + ( -- get the earliest confirmed phase_start or phase_end with a simple phase of 'Complete' + SELECT + min(min_confirmed_date) + FROM ( + -- earliest confirmed phase start + SELECT + min(phases.phase_start) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_start IS NOT NULL + AND phases.is_phase_start_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + UNION ALL + -- earliest confirmed phase end + SELECT + min(phases.phase_end) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_end IS NOT NULL + AND phases.is_phase_end_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + ) min_confirmed_dates + ) AS substantial_completion_date, + ( -- get me a list of the inspectors for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Inspector'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_inspector, + ( -- get me a list of the designers for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Designer'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_designer, + ( -- get me all of the tags added to a project + SELECT string_agg(tags.name, ', '::text) AS string_agg + FROM moped_proj_tags ptags + JOIN moped_tags tags ON ptags.tag_id = tags.id + WHERE 1 = 1 + AND ptags.is_deleted = false + AND ptags.project_id = mp.project_id + GROUP BY ptags.project_id) AS project_tags, + concat(added_by_user.first_name, ' ', added_by_user.last_name) AS added_by, + mpcs.components + FROM moped_project mp + LEFT JOIN project_person_list_lookup ppll ON mp.project_id = ppll.project_id + LEFT JOIN funding_sources_lookup fsl ON fsl.project_id = mp.project_id + LEFT JOIN project_type_lookup ptl ON ptl.project_id = mp.project_id + LEFT JOIN moped_entity me ON me.entity_id = mp.project_sponsor + LEFT JOIN moped_entity mel ON mel.entity_id = mp.project_lead_id + LEFT JOIN moped_proj_partners mpp2 ON mp.project_id = mpp2.project_id AND mpp2.is_deleted = false + LEFT JOIN moped_entity me2 ON mpp2.entity_id = me2.entity_id + LEFT JOIN work_activities on work_activities.project_id = mp.project_id + LEFT JOIN moped_users added_by_user ON mp.added_by = added_by_user.user_id + LEFT JOIN current_phase_view current_phase on mp.project_id = current_phase.project_id + LEFT JOIN moped_public_process_statuses mpps ON mpps.id = mp.public_process_status_id + LEFT JOIN child_project_lookup cpl on cpl.parent_id = mp.project_id + LEFT JOIN moped_proj_components_subtypes mpcs on mpcs.project_id = mp.project_id + LEFT JOIN LATERAL + ( + SELECT mpn.project_note, mpn.date_created + FROM moped_proj_notes mpn + WHERE mpn.project_id = mp.project_id AND mpn.project_note_type = 2 AND mpn.is_deleted = false + ORDER BY mpn.date_created DESC + LIMIT 1 + ) as proj_notes on true + WHERE + mp.is_deleted = false + GROUP BY + mp.project_id, + mp.project_name, + mp.project_description, + ppll.project_team_members, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + me.entity_name, + mel.entity_name, + mp.updated_at, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + current_phase.phase_name, + current_phase.phase_key, + current_phase.phase_name_simple, + ptl.type_name, + mpcs.components, + fsl.funding_source_name, + added_by_user.first_name, + added_by_user.last_name, + mpps.name, + cpl.children_project_ids, + proj_notes.project_note, + proj_notes.date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders; + +-- add substantial_completion_date to agol view +CREATE OR REPLACE VIEW component_arcgis_online_view AS ( + SELECT + mpc.project_id, + comp_geography.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + comp_geography.length_feet_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + subcomponents.subcomponents, + work_types.work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.is_deleted is_project_component_deleted, + plv.is_deleted is_project_deleted, + mpc.interim_project_component_id, + mpc.completion_date, + COALESCE(mpc.completion_date, substantial_completion_date) as substantial_completion_date, + mpc.srts_id, + mpc.location_description, + plv.project_name, + plv.project_description, + plv.ecapris_subproject_id, + plv.updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple as component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + COALESCE(mph.phase_name, current_phase.phase_name) AS current_phase_name, + COALESCE(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partner, + plv.task_order_names, + plv.funding_source_name, + plv.type_name, + plv.project_note, + plv.project_note_date_created, + plv.construction_start_date, + plv.completion_end_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.knack_project_id as knack_data_tracker_project_record_id, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text as project_url, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text || '?tab=map&project_component_id=' || mpc.project_component_id :: text as component_url, + added_by + FROM + moped_proj_components mpc + LEFT JOIN ( + -- group feature properties by project component ID + SELECT + component_id AS project_component_id, + STRING_AGG(DISTINCT id :: text, ', ') AS feature_ids, + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(geography) + ) + ):: json AS "geometry", + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(line_geography) + ) + ):: json AS "line_geometry", + STRING_AGG(DISTINCT signal_id :: text, ', ') AS signal_ids, + SUM(length_feet) as length_feet_total + FROM + ( + -- union all features + SELECT + id, + feature_signals.component_id, + feature_signals.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_signals.geography, 7):: geometry + ) AS line_geography, + feature_signals.signal_id, + NULL AS length_feet + FROM + feature_signals + WHERE + feature_signals.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_street_segments.component_id, + feature_street_segments.geography :: geometry, + feature_street_segments.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_street_segments + WHERE + feature_street_segments.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_intersections.component_id, + feature_intersections.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_intersections.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_intersections + WHERE + feature_intersections.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_points.component_id, + feature_drawn_points.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_drawn_points.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_drawn_points + WHERE + feature_drawn_points.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography :: geometry, + feature_drawn_lines.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_drawn_lines + WHERE + feature_drawn_lines.is_deleted = FALSE + ) feature_union + GROUP BY + component_id + ) comp_geography ON comp_geography.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group council districts by project component id + SELECT + component_id AS project_component_id, + STRING_AGG( + DISTINCT council_district_id :: text, + ', ' + ) AS council_districts + FROM + features_council_districts + LEFT JOIN features ON features.id = features_council_districts.feature_id + WHERE + features.is_deleted = FALSE + GROUP BY + component_id + ) council_districts ON council_districts.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group subcomponents by project component id + SELECT + project_component_id, + string_agg(ms.subcomponent_name, ', ') subcomponents + FROM + moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = FALSE + GROUP BY + project_component_id + ) subcomponents ON subcomponents.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group work types by project component id + SELECT + project_component_id, + string_agg(mwt.name, ', ') work_types + FROM + moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = FALSE + GROUP BY + project_component_id + ) work_types ON work_types.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group project component tags by project component id + SELECT + project_component_id, + string_agg(mct.type || ' - ' || mct.name, ', ') component_tags + FROM + moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = FALSE + GROUP BY + project_component_id + ) component_tags ON component_tags.project_component_id = mpc.project_component_id + LEFT JOIN project_list_view plv ON plv.project_id = mpc.project_id + LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id + LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id + LEFT JOIN moped_components mc ON mc.component_id = mpc.component_id +WHERE + mpc.is_deleted = FALSE + AND plv.is_deleted = FALSE +); diff --git a/moped-database/migrations/1704906960000_remove_phase_confirm_trigger/down.sql b/moped-database/migrations/1704906960000_remove_phase_confirm_trigger/down.sql new file mode 100644 index 0000000000..2e7d94e978 --- /dev/null +++ b/moped-database/migrations/1704906960000_remove_phase_confirm_trigger/down.sql @@ -0,0 +1,23 @@ +-- revert to 1697679713991_confirmed_date_trigger +CREATE OR REPLACE FUNCTION moped_proj_phases_confirmed_dates() RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF NEW.phase_start <= current_date AND NEW.is_phase_start_confirmed != TRUE THEN + new.is_phase_start_confirmed := true; + END IF; + IF NEW.phase_end <= current_date AND NEW.is_phase_end_confirmed != TRUE THEN + new.is_phase_end_confirmed := true; + END IF; + IF NEW.phase_start > current_date THEN + new.is_phase_start_confirmed := false; + END IF; + IF NEW.phase_end > current_date THEN + new.is_phase_end_confirmed := false; + END IF; + RETURN NEW; + END; +$$; + +CREATE TRIGGER set_moped_proj_phases_confirmed_dates_trigger BEFORE INSERT OR UPDATE ON public.moped_proj_phases + FOR EACH ROW EXECUTE FUNCTION public.moped_proj_phases_confirmed_dates(); diff --git a/moped-database/migrations/1704906960000_remove_phase_confirm_trigger/up.sql b/moped-database/migrations/1704906960000_remove_phase_confirm_trigger/up.sql new file mode 100644 index 0000000000..2be33490d0 --- /dev/null +++ b/moped-database/migrations/1704906960000_remove_phase_confirm_trigger/up.sql @@ -0,0 +1,3 @@ +DROP TRIGGER set_moped_proj_phases_confirmed_dates_trigger ON public.moped_proj_phases; + +DROP FUNCTION moped_proj_phases_confirmed_dates; diff --git a/moped-database/migrations/1704906960001_phase_timestamps/down.sql b/moped-database/migrations/1704906960001_phase_timestamps/down.sql new file mode 100644 index 0000000000..ebc271f82b --- /dev/null +++ b/moped-database/migrations/1704906960001_phase_timestamps/down.sql @@ -0,0 +1,497 @@ +DROP VIEW IF EXISTS component_arcgis_online_view; +DROP VIEW IF EXISTS project_list_view; + +-- change column types to dates +ALTER TABLE moped_proj_phases + ALTER COLUMN phase_start TYPE date, + ALTER COLUMN phase_end TYPE date; + +-- disable event triggers and assign UTC to all existing dates +SET session_replication_role = replica; +UPDATE + moped_proj_phases +SET + phase_start = dates_to_fix.phase_start, + phase_end = dates_to_fix.phase_end +FROM ( + SELECT + project_id, + phase_start AT TIME ZONE 'America/Chicago' AT TIME ZONE 'UTC' AS phase_start, + phase_end AT TIME ZONE 'America/Chicago' AT TIME ZONE 'UTC' AS phase_end + FROM + moped_proj_phases) AS dates_to_fix +WHERE + moped_proj_phases.project_id = dates_to_fix.project_id; + +-- replace timezone of component completion dates which were imported from Access DB as UTC +-- there is no way to undo this operation: +-- UPDATE +-- moped_proj_components +-- SET +-- completion_date = dates_to_fix.completion_date +-- FROM ( +-- SELECT +-- project_component_id, +-- completion_date AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago' AS completion_date +-- FROM +-- moped_proj_components +-- WHERE +-- -- if the timestamp is 00:00:00 it was imported incorrectly as UTC time +-- completion_date IS NOT NULL +-- AND extract(hour FROM completion_date) = 0) as dates_to_fix +-- WHERE +-- moped_proj_components.project_component_id = dates_to_fix.project_component_id; + +-- rebuild dropped views to version 1704744986000_substantial_completion_date +CREATE OR REPLACE VIEW public.project_list_view +AS WITH project_person_list_lookup AS ( + SELECT + mpp.project_id, + string_agg(DISTINCT concat(mu.first_name, ' ', mu.last_name, ':', mpr.project_role_name), ','::text) AS project_team_members + FROM moped_proj_personnel mpp + JOIN moped_users mu ON mpp.user_id = mu.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE mpp.is_deleted = false + AND mppr.is_deleted = false + GROUP BY mpp.project_id + ), funding_sources_lookup AS ( + SELECT + mpf_1.project_id, + string_agg(mfs.funding_source_name, ', '::text) AS funding_source_name + FROM moped_proj_funding mpf_1 + LEFT JOIN moped_fund_sources mfs ON mpf_1.funding_source_id = mfs.funding_source_id + WHERE mpf_1.is_deleted = false + GROUP BY mpf_1.project_id + ), project_type_lookup AS ( + SELECT + mpt.project_id, + string_agg(mt.type_name, ', '::text) AS type_name + FROM moped_project_types mpt + LEFT JOIN moped_types mt ON mpt.project_type_id = mt.type_id AND mpt.is_deleted = false + GROUP BY mpt.project_id + ), child_project_lookup AS ( + SELECT jsonb_agg(children.project_id) AS children_project_ids, + children.parent_project_id AS parent_id + FROM moped_project AS children + JOIN moped_project AS parent ON (parent.project_id = children.parent_project_id) + WHERE children.is_deleted = false + GROUP BY parent_id + ), work_activities AS ( + SELECT + project_id, + string_agg(task_order_objects.task_order_object ->> 'display_name'::text, + ', '::text) AS task_order_names, + string_agg(task_order_objects.task_order_object ->> 'task_order'::text, + ', '::text) AS task_order_names_short, + jsonb_agg(DISTINCT task_order_objects.task_order_object) FILTER (WHERE task_order_objects.task_order_object IS NOT NULL) AS task_orders, + string_agg(DISTINCT mpwa.workgroup_contractor, + ', '::text) AS workgroup_contractors, + string_agg(mpwa.contract_number, + ', '::text) AS contract_numbers FROM moped_proj_work_activity mpwa + LEFT JOIN LATERAL jsonb_array_elements(mpwa.task_orders) task_order_objects (task_order_object) ON TRUE WHERE 1 = 1 + AND mpwa.is_deleted = FALSE + GROUP BY + mpwa.project_id + ), moped_proj_components_subtypes AS ( + SELECT + mpc.project_id, + string_agg(DISTINCT mc.component_name_full, ', '::text) AS components + FROM moped_proj_components mpc + LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id + WHERE mpc.is_deleted = FALSE + GROUP BY mpc.project_id + ) + SELECT + mp.project_id, + mp.project_name, + mp.project_description, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + mp.updated_at, + current_phase.phase_name as current_phase, + current_phase.phase_key as current_phase_key, + current_phase.phase_name_simple as current_phase_simple, + ppll.project_team_members, + me.entity_name AS project_sponsor, + mel.entity_name AS project_lead, + mpps.name AS public_process_status, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + proj_notes.project_note, + proj_notes.date_created as project_note_date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders, + (SELECT project_name + FROM moped_project + WHERE project_id = mp.parent_project_id + ) as parent_project_name, + cpl.children_project_ids, + string_agg(DISTINCT me2.entity_name, ', '::text) AS project_partner, + (SELECT JSON_AGG(json_build_object('signal_id', feature_signals.signal_id, 'knack_id', feature_signals.knack_id, 'location_name', feature_signals.location_name, 'signal_type', feature_signals.signal_type, 'id', feature_signals.id)) + FROM moped_proj_components components + LEFT JOIN feature_signals + ON (feature_signals.component_id = components.project_component_id) + WHERE TRUE + AND components.is_deleted = false + AND components.project_id = mp.project_id + AND feature_signals.signal_id is not null + AND feature_signals.is_deleted = false + ) as project_feature, + fsl.funding_source_name, + ptl.type_name, + ( -- get the date of the construction phase with the earliest start date + SELECT min(phases.phase_start) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 9 -- phase_id 9 is construction + AND phases.is_deleted = false + ) AS construction_start_date, + ( -- get the date of the completion phase with the latest end date + SELECT max(phases.phase_end) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 11 -- phase_id 11 is complete + AND phases.is_deleted = false + ) AS completion_end_date, + ( -- get the earliest confirmed phase_start or phase_end with a simple phase of 'Complete' + SELECT + min(min_confirmed_date) + FROM ( + -- earliest confirmed phase start + SELECT + min(phases.phase_start) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_start IS NOT NULL + AND phases.is_phase_start_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + UNION ALL + -- earliest confirmed phase end + SELECT + min(phases.phase_end) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_end IS NOT NULL + AND phases.is_phase_end_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + ) min_confirmed_dates + ) AS substantial_completion_date, + ( -- get me a list of the inspectors for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Inspector'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_inspector, + ( -- get me a list of the designers for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Designer'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_designer, + ( -- get me all of the tags added to a project + SELECT string_agg(tags.name, ', '::text) AS string_agg + FROM moped_proj_tags ptags + JOIN moped_tags tags ON ptags.tag_id = tags.id + WHERE 1 = 1 + AND ptags.is_deleted = false + AND ptags.project_id = mp.project_id + GROUP BY ptags.project_id) AS project_tags, + concat(added_by_user.first_name, ' ', added_by_user.last_name) AS added_by, + mpcs.components + FROM moped_project mp + LEFT JOIN project_person_list_lookup ppll ON mp.project_id = ppll.project_id + LEFT JOIN funding_sources_lookup fsl ON fsl.project_id = mp.project_id + LEFT JOIN project_type_lookup ptl ON ptl.project_id = mp.project_id + LEFT JOIN moped_entity me ON me.entity_id = mp.project_sponsor + LEFT JOIN moped_entity mel ON mel.entity_id = mp.project_lead_id + LEFT JOIN moped_proj_partners mpp2 ON mp.project_id = mpp2.project_id AND mpp2.is_deleted = false + LEFT JOIN moped_entity me2 ON mpp2.entity_id = me2.entity_id + LEFT JOIN work_activities on work_activities.project_id = mp.project_id + LEFT JOIN moped_users added_by_user ON mp.added_by = added_by_user.user_id + LEFT JOIN current_phase_view current_phase on mp.project_id = current_phase.project_id + LEFT JOIN moped_public_process_statuses mpps ON mpps.id = mp.public_process_status_id + LEFT JOIN child_project_lookup cpl on cpl.parent_id = mp.project_id + LEFT JOIN moped_proj_components_subtypes mpcs on mpcs.project_id = mp.project_id + LEFT JOIN LATERAL + ( + SELECT mpn.project_note, mpn.date_created + FROM moped_proj_notes mpn + WHERE mpn.project_id = mp.project_id AND mpn.project_note_type = 2 AND mpn.is_deleted = false + ORDER BY mpn.date_created DESC + LIMIT 1 + ) as proj_notes on true + WHERE + mp.is_deleted = false + GROUP BY + mp.project_id, + mp.project_name, + mp.project_description, + ppll.project_team_members, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + me.entity_name, + mel.entity_name, + mp.updated_at, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + current_phase.phase_name, + current_phase.phase_key, + current_phase.phase_name_simple, + ptl.type_name, + mpcs.components, + fsl.funding_source_name, + added_by_user.first_name, + added_by_user.last_name, + mpps.name, + cpl.children_project_ids, + proj_notes.project_note, + proj_notes.date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders; + +-- add substantial_completion_date to agol view +CREATE OR REPLACE VIEW component_arcgis_online_view AS ( + SELECT + mpc.project_id, + comp_geography.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + comp_geography.length_feet_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + subcomponents.subcomponents, + work_types.work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.is_deleted is_project_component_deleted, + plv.is_deleted is_project_deleted, + mpc.interim_project_component_id, + mpc.completion_date, + COALESCE(mpc.completion_date, substantial_completion_date) as substantial_completion_date, + mpc.srts_id, + mpc.location_description, + plv.project_name, + plv.project_description, + plv.ecapris_subproject_id, + plv.updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple as component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + COALESCE(mph.phase_name, current_phase.phase_name) AS current_phase_name, + COALESCE(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partner, + plv.task_order_names, + plv.funding_source_name, + plv.type_name, + plv.project_note, + plv.project_note_date_created, + plv.construction_start_date, + plv.completion_end_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.knack_project_id as knack_data_tracker_project_record_id, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text as project_url, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text || '?tab=map&project_component_id=' || mpc.project_component_id :: text as component_url, + added_by + FROM + moped_proj_components mpc + LEFT JOIN ( + -- group feature properties by project component ID + SELECT + component_id AS project_component_id, + STRING_AGG(DISTINCT id :: text, ', ') AS feature_ids, + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(geography) + ) + ):: json AS "geometry", + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(line_geography) + ) + ):: json AS "line_geometry", + STRING_AGG(DISTINCT signal_id :: text, ', ') AS signal_ids, + SUM(length_feet) as length_feet_total + FROM + ( + -- union all features + SELECT + id, + feature_signals.component_id, + feature_signals.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_signals.geography, 7):: geometry + ) AS line_geography, + feature_signals.signal_id, + NULL AS length_feet + FROM + feature_signals + WHERE + feature_signals.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_street_segments.component_id, + feature_street_segments.geography :: geometry, + feature_street_segments.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_street_segments + WHERE + feature_street_segments.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_intersections.component_id, + feature_intersections.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_intersections.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_intersections + WHERE + feature_intersections.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_points.component_id, + feature_drawn_points.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_drawn_points.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_drawn_points + WHERE + feature_drawn_points.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography :: geometry, + feature_drawn_lines.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_drawn_lines + WHERE + feature_drawn_lines.is_deleted = FALSE + ) feature_union + GROUP BY + component_id + ) comp_geography ON comp_geography.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group council districts by project component id + SELECT + component_id AS project_component_id, + STRING_AGG( + DISTINCT council_district_id :: text, + ', ' + ) AS council_districts + FROM + features_council_districts + LEFT JOIN features ON features.id = features_council_districts.feature_id + WHERE + features.is_deleted = FALSE + GROUP BY + component_id + ) council_districts ON council_districts.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group subcomponents by project component id + SELECT + project_component_id, + string_agg(ms.subcomponent_name, ', ') subcomponents + FROM + moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = FALSE + GROUP BY + project_component_id + ) subcomponents ON subcomponents.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group work types by project component id + SELECT + project_component_id, + string_agg(mwt.name, ', ') work_types + FROM + moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = FALSE + GROUP BY + project_component_id + ) work_types ON work_types.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group project component tags by project component id + SELECT + project_component_id, + string_agg(mct.type || ' - ' || mct.name, ', ') component_tags + FROM + moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = FALSE + GROUP BY + project_component_id + ) component_tags ON component_tags.project_component_id = mpc.project_component_id + LEFT JOIN project_list_view plv ON plv.project_id = mpc.project_id + LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id + LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id + LEFT JOIN moped_components mc ON mc.component_id = mpc.component_id +WHERE + mpc.is_deleted = FALSE + AND plv.is_deleted = FALSE +); diff --git a/moped-database/migrations/1704906960001_phase_timestamps/up.sql b/moped-database/migrations/1704906960001_phase_timestamps/up.sql new file mode 100644 index 0000000000..c7c63cb2b1 --- /dev/null +++ b/moped-database/migrations/1704906960001_phase_timestamps/up.sql @@ -0,0 +1,500 @@ +-- this migration makes the `moped_proj_phases` timezone aware and corrects the existing date +-- values which are in central time but stored as UTC + +-- we must drop dependent views to change the column types :/ +DROP VIEW IF EXISTS component_arcgis_online_view; +DROP VIEW IF EXISTS project_list_view; + +-- change column types from dates to timestamps with timezones +ALTER TABLE moped_proj_phases + ALTER COLUMN phase_start TYPE timestamp WITH time zone, + ALTER COLUMN phase_end TYPE timestamp WITH time zone; + +-- disable event triggers and assign timezone to all existing dates +SET session_replication_role = replica; +UPDATE + moped_proj_phases +SET + phase_start = dates_to_fix.phase_start, + phase_end = dates_to_fix.phase_end +FROM ( + SELECT + project_phase_id, + phase_start AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago' AS phase_start, + phase_end AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago' AS phase_end + FROM + moped_proj_phases) AS dates_to_fix +WHERE + moped_proj_phases.project_phase_id = dates_to_fix.project_phase_id; + +-- replace timezone of component completion dates which were imported from Access DB as UTC +UPDATE + moped_proj_components +SET + completion_date = dates_to_fix.completion_date +FROM ( + SELECT + project_component_id, + completion_date AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago' AS completion_date + FROM + moped_proj_components + WHERE + -- if the timestamp is 00:00:00 it was imported incorrectly as UTC time + completion_date IS NOT NULL + AND extract(hour FROM completion_date) = 0) as dates_to_fix +WHERE + moped_proj_components.project_component_id = dates_to_fix.project_component_id; + +-- rebuild dropped views to version 1704744986000_substantial_completion_date +CREATE OR REPLACE VIEW public.project_list_view +AS WITH project_person_list_lookup AS ( + SELECT + mpp.project_id, + string_agg(DISTINCT concat(mu.first_name, ' ', mu.last_name, ':', mpr.project_role_name), ','::text) AS project_team_members + FROM moped_proj_personnel mpp + JOIN moped_users mu ON mpp.user_id = mu.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE mpp.is_deleted = false + AND mppr.is_deleted = false + GROUP BY mpp.project_id + ), funding_sources_lookup AS ( + SELECT + mpf_1.project_id, + string_agg(mfs.funding_source_name, ', '::text) AS funding_source_name + FROM moped_proj_funding mpf_1 + LEFT JOIN moped_fund_sources mfs ON mpf_1.funding_source_id = mfs.funding_source_id + WHERE mpf_1.is_deleted = false + GROUP BY mpf_1.project_id + ), project_type_lookup AS ( + SELECT + mpt.project_id, + string_agg(mt.type_name, ', '::text) AS type_name + FROM moped_project_types mpt + LEFT JOIN moped_types mt ON mpt.project_type_id = mt.type_id AND mpt.is_deleted = false + GROUP BY mpt.project_id + ), child_project_lookup AS ( + SELECT jsonb_agg(children.project_id) AS children_project_ids, + children.parent_project_id AS parent_id + FROM moped_project AS children + JOIN moped_project AS parent ON (parent.project_id = children.parent_project_id) + WHERE children.is_deleted = false + GROUP BY parent_id + ), work_activities AS ( + SELECT + project_id, + string_agg(task_order_objects.task_order_object ->> 'display_name'::text, + ', '::text) AS task_order_names, + string_agg(task_order_objects.task_order_object ->> 'task_order'::text, + ', '::text) AS task_order_names_short, + jsonb_agg(DISTINCT task_order_objects.task_order_object) FILTER (WHERE task_order_objects.task_order_object IS NOT NULL) AS task_orders, + string_agg(DISTINCT mpwa.workgroup_contractor, + ', '::text) AS workgroup_contractors, + string_agg(mpwa.contract_number, + ', '::text) AS contract_numbers FROM moped_proj_work_activity mpwa + LEFT JOIN LATERAL jsonb_array_elements(mpwa.task_orders) task_order_objects (task_order_object) ON TRUE WHERE 1 = 1 + AND mpwa.is_deleted = FALSE + GROUP BY + mpwa.project_id + ), moped_proj_components_subtypes AS ( + SELECT + mpc.project_id, + string_agg(DISTINCT mc.component_name_full, ', '::text) AS components + FROM moped_proj_components mpc + LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id + WHERE mpc.is_deleted = FALSE + GROUP BY mpc.project_id + ) + SELECT + mp.project_id, + mp.project_name, + mp.project_description, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + mp.updated_at, + current_phase.phase_name as current_phase, + current_phase.phase_key as current_phase_key, + current_phase.phase_name_simple as current_phase_simple, + ppll.project_team_members, + me.entity_name AS project_sponsor, + mel.entity_name AS project_lead, + mpps.name AS public_process_status, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + proj_notes.project_note, + proj_notes.date_created as project_note_date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders, + (SELECT project_name + FROM moped_project + WHERE project_id = mp.parent_project_id + ) as parent_project_name, + cpl.children_project_ids, + string_agg(DISTINCT me2.entity_name, ', '::text) AS project_partner, + (SELECT JSON_AGG(json_build_object('signal_id', feature_signals.signal_id, 'knack_id', feature_signals.knack_id, 'location_name', feature_signals.location_name, 'signal_type', feature_signals.signal_type, 'id', feature_signals.id)) + FROM moped_proj_components components + LEFT JOIN feature_signals + ON (feature_signals.component_id = components.project_component_id) + WHERE TRUE + AND components.is_deleted = false + AND components.project_id = mp.project_id + AND feature_signals.signal_id is not null + AND feature_signals.is_deleted = false + ) as project_feature, + fsl.funding_source_name, + ptl.type_name, + ( -- get the date of the construction phase with the earliest start date + SELECT min(phases.phase_start) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 9 -- phase_id 9 is construction + AND phases.is_deleted = false + ) AS construction_start_date, + ( -- get the date of the completion phase with the latest end date + SELECT max(phases.phase_end) + FROM moped_proj_phases phases + WHERE true + AND phases.project_id = mp.project_id + AND phases.phase_id = 11 -- phase_id 11 is complete + AND phases.is_deleted = false + ) AS completion_end_date, + ( -- get the earliest confirmed phase_start or phase_end with a simple phase of 'Complete' + SELECT + min(min_confirmed_date) + FROM ( + -- earliest confirmed phase start + SELECT + min(phases.phase_start) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_start IS NOT NULL + AND phases.is_phase_start_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + UNION ALL + -- earliest confirmed phase end + SELECT + min(phases.phase_end) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_end IS NOT NULL + AND phases.is_phase_end_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + ) min_confirmed_dates + ) AS substantial_completion_date, + ( -- get me a list of the inspectors for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Inspector'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_inspector, + ( -- get me a list of the designers for this project + SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg + FROM moped_proj_personnel mpp + JOIN moped_users users ON mpp.user_id = users.user_id + JOIN moped_proj_personnel_roles mppr ON mpp.project_personnel_id = mppr.project_personnel_id + JOIN moped_project_roles mpr ON mppr.project_role_id = mpr.project_role_id + WHERE 1 = 1 + AND mpr.project_role_name = 'Designer'::text + AND mpp.is_deleted = false + AND mppr.is_deleted = false + AND mpp.project_id = mp.project_id + GROUP BY mpp.project_id) AS project_designer, + ( -- get me all of the tags added to a project + SELECT string_agg(tags.name, ', '::text) AS string_agg + FROM moped_proj_tags ptags + JOIN moped_tags tags ON ptags.tag_id = tags.id + WHERE 1 = 1 + AND ptags.is_deleted = false + AND ptags.project_id = mp.project_id + GROUP BY ptags.project_id) AS project_tags, + concat(added_by_user.first_name, ' ', added_by_user.last_name) AS added_by, + mpcs.components + FROM moped_project mp + LEFT JOIN project_person_list_lookup ppll ON mp.project_id = ppll.project_id + LEFT JOIN funding_sources_lookup fsl ON fsl.project_id = mp.project_id + LEFT JOIN project_type_lookup ptl ON ptl.project_id = mp.project_id + LEFT JOIN moped_entity me ON me.entity_id = mp.project_sponsor + LEFT JOIN moped_entity mel ON mel.entity_id = mp.project_lead_id + LEFT JOIN moped_proj_partners mpp2 ON mp.project_id = mpp2.project_id AND mpp2.is_deleted = false + LEFT JOIN moped_entity me2 ON mpp2.entity_id = me2.entity_id + LEFT JOIN work_activities on work_activities.project_id = mp.project_id + LEFT JOIN moped_users added_by_user ON mp.added_by = added_by_user.user_id + LEFT JOIN current_phase_view current_phase on mp.project_id = current_phase.project_id + LEFT JOIN moped_public_process_statuses mpps ON mpps.id = mp.public_process_status_id + LEFT JOIN child_project_lookup cpl on cpl.parent_id = mp.project_id + LEFT JOIN moped_proj_components_subtypes mpcs on mpcs.project_id = mp.project_id + LEFT JOIN LATERAL + ( + SELECT mpn.project_note, mpn.date_created + FROM moped_proj_notes mpn + WHERE mpn.project_id = mp.project_id AND mpn.project_note_type = 2 AND mpn.is_deleted = false + ORDER BY mpn.date_created DESC + LIMIT 1 + ) as proj_notes on true + WHERE + mp.is_deleted = false + GROUP BY + mp.project_id, + mp.project_name, + mp.project_description, + ppll.project_team_members, + mp.ecapris_subproject_id, + mp.date_added, + mp.is_deleted, + me.entity_name, + mel.entity_name, + mp.updated_at, + mp.interim_project_id, + mp.parent_project_id, + mp.knack_project_id, + current_phase.phase_name, + current_phase.phase_key, + current_phase.phase_name_simple, + ptl.type_name, + mpcs.components, + fsl.funding_source_name, + added_by_user.first_name, + added_by_user.last_name, + mpps.name, + cpl.children_project_ids, + proj_notes.project_note, + proj_notes.date_created, + work_activities.workgroup_contractors, + work_activities.contract_numbers, + work_activities.task_order_names, + work_activities.task_order_names_short, + work_activities.task_orders; + +-- add substantial_completion_date to agol view +CREATE OR REPLACE VIEW component_arcgis_online_view AS ( + SELECT + mpc.project_id, + comp_geography.project_component_id, + comp_geography.feature_ids, + mpc.component_id, + comp_geography.geometry, + comp_geography.line_geometry, + comp_geography.signal_ids, + council_districts.council_districts, + comp_geography.length_feet_total, + mc.component_name, + mc.component_subtype, + mc.component_name_full, + subcomponents.subcomponents, + work_types.work_types, + component_tags.component_tags, + mpc.description AS component_description, + mpc.is_deleted is_project_component_deleted, + plv.is_deleted is_project_deleted, + mpc.interim_project_component_id, + mpc.completion_date, + COALESCE(mpc.completion_date, substantial_completion_date) as substantial_completion_date, + mpc.srts_id, + mpc.location_description, + plv.project_name, + plv.project_description, + plv.ecapris_subproject_id, + plv.updated_at, + mpc.phase_id AS component_phase_id, + mph.phase_name AS component_phase_name, + mph.phase_name_simple as component_phase_name_simple, + current_phase.phase_id AS project_phase_id, + current_phase.phase_name AS project_phase_name, + current_phase.phase_name_simple AS project_phase_name_simple, + COALESCE(mph.phase_name, current_phase.phase_name) AS current_phase_name, + COALESCE(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple, + plv.project_team_members, + plv.project_sponsor, + plv.project_lead, + plv.public_process_status, + plv.interim_project_id, + plv.project_partner, + plv.task_order_names, + plv.funding_source_name, + plv.type_name, + plv.project_note, + plv.project_note_date_created, + plv.construction_start_date, + plv.completion_end_date, + plv.project_inspector, + plv.project_designer, + plv.project_tags, + plv.workgroup_contractors, + plv.contract_numbers, + plv.knack_project_id as knack_data_tracker_project_record_id, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text as project_url, + 'https://mobility.austin.gov/moped/projects/' || plv.project_id :: text || '?tab=map&project_component_id=' || mpc.project_component_id :: text as component_url, + added_by + FROM + moped_proj_components mpc + LEFT JOIN ( + -- group feature properties by project component ID + SELECT + component_id AS project_component_id, + STRING_AGG(DISTINCT id :: text, ', ') AS feature_ids, + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(geography) + ) + ):: json AS "geometry", + ST_AsGeoJSON( + ST_Union( + ARRAY_AGG(line_geography) + ) + ):: json AS "line_geometry", + STRING_AGG(DISTINCT signal_id :: text, ', ') AS signal_ids, + SUM(length_feet) as length_feet_total + FROM + ( + -- union all features + SELECT + id, + feature_signals.component_id, + feature_signals.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_signals.geography, 7):: geometry + ) AS line_geography, + feature_signals.signal_id, + NULL AS length_feet + FROM + feature_signals + WHERE + feature_signals.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_street_segments.component_id, + feature_street_segments.geography :: geometry, + feature_street_segments.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_street_segments + WHERE + feature_street_segments.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_intersections.component_id, + feature_intersections.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_intersections.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_intersections + WHERE + feature_intersections.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_points.component_id, + feature_drawn_points.geography :: geometry, + ST_ExteriorRing( + ST_Buffer(feature_drawn_points.geography, 7):: geometry + ) AS line_geography, + NULL AS signal_id, + NULL AS length_feet + FROM + feature_drawn_points + WHERE + feature_drawn_points.is_deleted = FALSE + UNION ALL + SELECT + id, + feature_drawn_lines.component_id, + feature_drawn_lines.geography :: geometry, + feature_drawn_lines.geography :: geometry as line_geography, + NULL AS signal_id, + length_feet + FROM + feature_drawn_lines + WHERE + feature_drawn_lines.is_deleted = FALSE + ) feature_union + GROUP BY + component_id + ) comp_geography ON comp_geography.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group council districts by project component id + SELECT + component_id AS project_component_id, + STRING_AGG( + DISTINCT council_district_id :: text, + ', ' + ) AS council_districts + FROM + features_council_districts + LEFT JOIN features ON features.id = features_council_districts.feature_id + WHERE + features.is_deleted = FALSE + GROUP BY + component_id + ) council_districts ON council_districts.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group subcomponents by project component id + SELECT + project_component_id, + string_agg(ms.subcomponent_name, ', ') subcomponents + FROM + moped_proj_components_subcomponents mpcs + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = FALSE + GROUP BY + project_component_id + ) subcomponents ON subcomponents.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group work types by project component id + SELECT + project_component_id, + string_agg(mwt.name, ', ') work_types + FROM + moped_proj_component_work_types mpcwt + LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = FALSE + GROUP BY + project_component_id + ) work_types ON work_types.project_component_id = mpc.project_component_id + LEFT JOIN ( + -- group project component tags by project component id + SELECT + project_component_id, + string_agg(mct.type || ' - ' || mct.name, ', ') component_tags + FROM + moped_proj_component_tags mpct + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = FALSE + GROUP BY + project_component_id + ) component_tags ON component_tags.project_component_id = mpc.project_component_id + LEFT JOIN project_list_view plv ON plv.project_id = mpc.project_id + LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id + LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id + LEFT JOIN moped_components mc ON mc.component_id = mpc.component_id +WHERE + mpc.is_deleted = FALSE + AND plv.is_deleted = FALSE +); diff --git a/moped-database/views/component_arcgis_online_view.sql b/moped-database/views/component_arcgis_online_view.sql index 6b2e7b2be5..e6c8edc527 100644 --- a/moped-database/views/component_arcgis_online_view.sql +++ b/moped-database/views/component_arcgis_online_view.sql @@ -1,6 +1,6 @@ --- current version 1699891030861_change_contractor_column_name +-- current_version: 1704744986000_substantial_completion_date +DROP VIEW component_arcgis_online_view; -DROP VIEW IF EXISTS component_arcgis_online_view; CREATE OR REPLACE VIEW component_arcgis_online_view AS ( SELECT mpc.project_id, @@ -22,7 +22,8 @@ CREATE OR REPLACE VIEW component_arcgis_online_view AS ( mpc.is_deleted is_project_component_deleted, plv.is_deleted is_project_deleted, mpc.interim_project_component_id, - mpc.completion_date, + mpc.completion_date, + COALESCE(mpc.completion_date, substantial_completion_date) as substantial_completion_date, mpc.srts_id, mpc.location_description, plv.project_name, @@ -173,7 +174,8 @@ CREATE OR REPLACE VIEW component_arcgis_online_view AS ( string_agg(ms.subcomponent_name, ', ') subcomponents FROM moped_proj_components_subcomponents mpcs - LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id + WHERE mpcs.is_deleted = FALSE GROUP BY project_component_id ) subcomponents ON subcomponents.project_component_id = mpc.project_component_id @@ -185,6 +187,7 @@ CREATE OR REPLACE VIEW component_arcgis_online_view AS ( FROM moped_proj_component_work_types mpcwt LEFT JOIN moped_work_types mwt ON mpcwt.work_type_id = mwt.id + WHERE mpcwt.is_deleted = FALSE GROUP BY project_component_id ) work_types ON work_types.project_component_id = mpc.project_component_id @@ -195,7 +198,8 @@ CREATE OR REPLACE VIEW component_arcgis_online_view AS ( string_agg(mct.type || ' - ' || mct.name, ', ') component_tags FROM moped_proj_component_tags mpct - LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id + WHERE mpct.is_deleted = FALSE GROUP BY project_component_id ) component_tags ON component_tags.project_component_id = mpc.project_component_id diff --git a/moped-database/views/project_list_view.sql b/moped-database/views/project_list_view.sql index a855f54e6a..4f67cb86c9 100644 --- a/moped-database/views/project_list_view.sql +++ b/moped-database/views/project_list_view.sql @@ -1,4 +1,4 @@ --- latest version 1700515730257_fix_delete_component_bug +-- latest version 1704744986000_substantial_completion_date DROP VIEW project_list_view CASCADE; CREATE OR REPLACE VIEW public.project_list_view @@ -119,6 +119,39 @@ AS WITH project_person_list_lookup AS ( AND phases.phase_id = 11 -- phase_id 11 is complete AND phases.is_deleted = false ) AS completion_end_date, + ( -- get the earliest confirmed phase_start or phase_end with a simple phase of 'Complete' + SELECT + min(min_confirmed_date) + FROM ( + -- earliest confirmed phase start + SELECT + min(phases.phase_start) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_start IS NOT NULL + AND phases.is_phase_start_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + UNION ALL + -- earliest confirmed phase end + SELECT + min(phases.phase_end) AS min_confirmed_date + FROM + moped_proj_phases phases + LEFT JOIN moped_phases ON phases.phase_id = moped_phases.phase_id + WHERE + TRUE + AND phases.phase_end IS NOT NULL + AND phases.is_phase_end_confirmed = TRUE + AND phases.project_id = mp.project_id + AND moped_phases.phase_name_simple = 'Complete' + AND phases.is_deleted = FALSE + ) min_confirmed_dates + ) AS substantial_completion_date, ( -- get me a list of the inspectors for this project SELECT string_agg(concat(users.first_name, ' ', users.last_name), ', '::text) AS string_agg FROM moped_proj_personnel mpp diff --git a/moped-editor/src/components/forms/ControlledCheckbox.js b/moped-editor/src/components/forms/ControlledCheckbox.js new file mode 100644 index 0000000000..34d14e3e71 --- /dev/null +++ b/moped-editor/src/components/forms/ControlledCheckbox.js @@ -0,0 +1,37 @@ +import React from "react"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { Controller } from "react-hook-form"; + +/** + * A react-hook-form wrapper of the MUI Checkbox component + * @param {object} control - react-hook-form `control` object from useController - required + * @param {string} name - unique field name which be used in react-hook-form data object + * @param {string} label - the label to render next to the checkbox + * @param {object} checkboxProps additional optional MUI checkbox props such as `sx` or `icon` + * @return {JSX.Element} + */ +const ControlledCheckbox = ({ name, control, label, ...checkBoxProps }) => { + return ( + { + return ( + field.onChange(e.target.checked)} + {...checkBoxProps} + /> + } + /> + ); + }} + /> + ); +}; + +export default ControlledCheckbox; diff --git a/moped-editor/src/components/forms/ControlledDateField.js b/moped-editor/src/components/forms/ControlledDateField.js new file mode 100644 index 0000000000..7f4b6cf257 --- /dev/null +++ b/moped-editor/src/components/forms/ControlledDateField.js @@ -0,0 +1,79 @@ +import React from "react"; +import { DatePicker } from "@mui/x-date-pickers"; +import { Controller } from "react-hook-form"; + +/** + * Test if an input is not null and can be coerced to a Date object. + * @param {any} value - any value, but in our case either a string, a Date object, + * an Invalid Date object, or null + * @returns true if the value is not null and can be coerced to a Date object + */ +const isValidDateStringOrObject = (value) => { + return value !== null && !isNaN(new Date(value)); +}; + +/** + * A react-hook-form wrapper of the MUI DatePicker component. + * @param {object} control - react-hook-form `control` object from useController - required + * @param {string} name - unique field name which be used in react-hook-form data object + * @param {string} label - the label to render next to the checkbox + * @param {bool} error - if the error state is active (triggers red outline around textfield) + * @param {object} datePickerProps additional optional MUI date picker props + * @return {JSX.Element} + */ +const ControlledDateField = ({ + name, + control, + label, + error, + ...datePickerProps +}) => { + return ( + { + /** + * The MUI component requires a Date object as the input value. + * So we try to construct a Date object from the value in + * react-hook-form. + * + * Otherwise just pass whatever the value is in state + */ + let value = field.value; + if (isValidDateStringOrObject(value)) { + value = new Date(field.value); + } + return ( + { + /** + * This component's value is a Date object or an Invalid Date object. + * If the date object is valid, we can convert it to an ISO string + * and store the string in react-hook-form state. + * + * If the date object is invalid, we store the invalid date in + * react-hook-form-state and let the Yup schema validation prevent + * form submit. + */ + const valueToStore = isValidDateStringOrObject(newValue) + ? newValue.toISOString() + : newValue; + field.onChange(valueToStore); + }} + {...datePickerProps} + /> + ); + }} + /> + ); +}; + +export default ControlledDateField; diff --git a/moped-editor/src/components/forms/ControlledSwitch.js b/moped-editor/src/components/forms/ControlledSwitch.js new file mode 100644 index 0000000000..c49d4dc618 --- /dev/null +++ b/moped-editor/src/components/forms/ControlledSwitch.js @@ -0,0 +1,38 @@ +import Switch from "@mui/material/Switch"; +import { Controller } from "react-hook-form"; +import FormControlLabel from "@mui/material/FormControlLabel"; + +/** + * A react-hook-form wrapper of the MUI Switch component + * @param {object} control - react-hook-form `control` object from useController - required + * @param {string} name - unique field name which be used in react-hook-form data object + * @param {string} label - the label to render next to the checkbox + * @param {object} switchProps additional optional MUI switch props + * @return {JSX.Element} + */ +const ControlledSwitch = ({ name, control, label, ...switchProps }) => { + return ( + { + return ( + field.onChange(e.target.checked)} + color="primary" + inputProps={{ "aria-label": "primary checkbox" }} + {...switchProps} + /> + } + /> + ); + }} + /> + ); +}; + +export default ControlledSwitch; diff --git a/moped-editor/src/queries/project.js b/moped-editor/src/queries/project.js index a348cf43c0..32c9c5e8f0 100644 --- a/moped-editor/src/queries/project.js +++ b/moped-editor/src/queries/project.js @@ -141,9 +141,7 @@ export const SUMMARY_QUERY = gql` is_deleted: { _eq: false } } ) { - project_geography( - where: { is_deleted: { _eq: false } } - ) { + project_geography(where: { is_deleted: { _eq: false } }) { council_districts } moped_proj_components(where: { is_deleted: { _eq: false } }) { @@ -290,7 +288,10 @@ export const TIMELINE_QUERY = gql` project_id phase_start phase_end + phase_id subphase_id + is_phase_start_confirmed + is_phase_end_confirmed moped_subphase { subphase_id subphase_name @@ -301,9 +302,7 @@ export const TIMELINE_QUERY = gql` phase_name } } - moped_milestones( - where: { is_deleted: { _eq: false } } - ) { + moped_milestones(where: { is_deleted: { _eq: false } }) { milestone_id milestone_name } @@ -326,11 +325,37 @@ export const TIMELINE_QUERY = gql` } `; +export const ADD_PROJECT_PHASE = gql` + mutation AddProjectPhase( + $objects: [moped_proj_phases_insert_input!]! + $current_phase_ids_to_clear: [Int!] = [] + ) { + insert_moped_proj_phases(objects: $objects) { + returning { + phase_id + phase_description + phase_start + phase_end + project_phase_id + is_current_phase + project_id + } + } + update_moped_proj_phases( + _set: { is_current_phase: false } + where: { project_phase_id: { _in: $current_phase_ids_to_clear } } + ) { + affected_rows + } + } +`; + // use this to update a single moped_proj_phase -export const UPDATE_PROJECT_PHASES_MUTATION = gql` +export const UPDATE_PROJECT_PHASE = gql` mutation ProjectPhasesMutation( $project_phase_id: Int! $object: moped_proj_phases_set_input! + $current_phase_ids_to_clear: [Int!] = [] ) { update_moped_proj_phases_by_pk( pk_columns: { project_phase_id: $project_phase_id } @@ -345,15 +370,21 @@ export const UPDATE_PROJECT_PHASES_MUTATION = gql` is_current_phase phase_description } + update_moped_proj_phases( + _set: { is_current_phase: false } + where: { project_phase_id: { _in: $current_phase_ids_to_clear } } + ) { + affected_rows + } } `; -// provide an array of project_phase_id's to set them not current -export const CLEAR_CURRENT_PROJECT_PHASES_MUTATION = gql` - mutation ClearCurrentProjectPhasePKs($ids: [Int!]!) { +// Delete a project phase **and** make it not current +export const DELETE_PROJECT_PHASE = gql` + mutation DeleteProjectPhase($project_phase_id: Int!) { update_moped_proj_phases( - _set: { is_current_phase: false } - where: { project_phase_id: { _in: $ids } } + _set: { is_deleted: true, is_current_phase: false } + where: { project_phase_id: { _eq: $project_phase_id } } ) { affected_rows } @@ -389,18 +420,6 @@ export const UPDATE_PROJECT_MILESTONES_MUTATION = gql` } `; -// Delete a project phase **and** make it not current -export const DELETE_PROJECT_PHASE = gql` - mutation DeleteProjectPhase($project_phase_id: Int!) { - update_moped_proj_phases( - _set: { is_deleted: true, is_current_phase: false } - where: { project_phase_id: { _eq: $project_phase_id } } - ) { - affected_rows - } - } -`; - export const DELETE_PROJECT_MILESTONE = gql` mutation DeleteProjectMilestone($project_milestone_id: Int!) { update_moped_proj_milestones( @@ -412,22 +431,6 @@ export const DELETE_PROJECT_MILESTONE = gql` } `; -export const ADD_PROJECT_PHASE = gql` - mutation AddProjectPhase($objects: [moped_proj_phases_insert_input!]!) { - insert_moped_proj_phases(objects: $objects) { - returning { - phase_id - phase_description - phase_start - phase_end - project_phase_id - is_current_phase - project_id - } - } - } -`; - export const ADD_PROJECT_MILESTONE = gql` mutation AddProjectMilestone( $objects: [moped_proj_milestones_insert_input!]! diff --git a/moped-editor/src/views/projects/newProjectView/NewProjectView.js b/moped-editor/src/views/projects/newProjectView/NewProjectView.js index 14dc97d91f..30dbe43493 100644 --- a/moped-editor/src/views/projects/newProjectView/NewProjectView.js +++ b/moped-editor/src/views/projects/newProjectView/NewProjectView.js @@ -7,8 +7,7 @@ import { Card, CardContent, } from "@mui/material"; -import makeStyles from '@mui/styles/makeStyles'; -import { format } from "date-fns"; +import makeStyles from "@mui/styles/makeStyles"; import DefineProjectForm from "./DefineProjectForm"; import Page from "src/components/Page"; import { useQuery, useMutation } from "@apollo/client"; @@ -145,7 +144,8 @@ const NewProjectView = () => { { phase_id: 1, is_current_phase: true, - phase_start: format(Date.now(), "yyyy-MM-dd"), + phase_start: new Date(new Date().setHours(0, 0, 0, 0)), + is_phase_start_confirmed: true, }, ], }, diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js index 661f120b62..23e974adc8 100644 --- a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js +++ b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js @@ -374,13 +374,17 @@ export const useResetDependentFieldOnParentFieldChange = ({ // when the parent value changes, compare to previous value // if it is different, reset the dependent field to its default useEffect(() => { + if (disable) return; // keep update from firing if the parent value hasn't changed - if ( - get(parentValue, comparisonVariable) === - get(previousParentFormValue, comparisonVariable) - ) + if (comparisonVariable) { + if ( + get(parentValue, comparisonVariable) === + get(previousParentFormValue, comparisonVariable) + ) + return; + } else if (parentValue === previousParentFormValue) { return; - if (disable) return; + } setValue(dependentFieldName, valueToSet); setPreviousParentValue(parentValue); diff --git a/moped-editor/src/views/projects/projectView/ProjectPhase/helpers.js b/moped-editor/src/views/projects/projectView/ProjectPhase/helpers.js new file mode 100644 index 0000000000..2bbf909812 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectPhase/helpers.js @@ -0,0 +1,179 @@ +import { useMemo } from "react"; +import * as yup from "yup"; + +export const phaseValidationSchema = yup.object().shape({ + phase_id: yup.number().nullable().required("Phase is required"), + subphase_id: yup.number().nullable().optional(), + phase_start: yup + .date() + .nullable() + .optional() + .when("is_current_phase", { + is: true, + then: (schema) => + schema.required("Start date is required when phase is current"), + }) + .typeError("Invalid Date"), + phase_end: yup.date().nullable().optional().typeError("Invalid Date"), + is_current_phase: yup.boolean(), + is_phase_start_confirmed: yup.boolean(), + is_phase_end_confirmed: yup.boolean(), + phase_description: yup + .string() + .max(500, "Must be less than 500 characters") + .nullable(), + project_phase_id: yup.number().nullable().optional(), + project_id: yup.number().required(), +}); + +const DEFAULT_FORM_VALUES = { + project_phase_id: null, + phase_id: null, + subphase_id: null, + phase_start: null, + is_phase_start_confirmed: false, + phase_end: null, + is_phase_end_confirmed: false, + phase_description: null, + is_current_phase: false, + project_id: null, +}; + +/** + * Hook which provides initial form values + * @param {object} phase - an optoinal `moped_proj_phase` object whose values will + * override the DEFAULT_FORM_VALUES + */ +export const useDefaultValues = (phase) => + useMemo(() => { + // initialize form with default values plus the project id + let defaultValues = { + ...DEFAULT_FORM_VALUES, + project_id: phase.project_id, + }; + + if (phase.project_phase_id) { + // we are editing a phase: update all defaults from phase + Object.keys(DEFAULT_FORM_VALUES).forEach((key) => { + defaultValues[key] = phase[key]; + }); + } + return defaultValues; + }, [phase]); + +/** + * Hook which returns an array of subphase options given an input `phase_id` + * and an array of `moped_phases` objects + */ +export const useSubphases = (phase_id, phases) => + useMemo( + () => + phase_id + ? phases.find((p) => p.phase_id === phase_id)?.moped_subphases || [] + : [], + [phase_id, phases] + ); + +/** + * Hook which returns an object of phase IDs with their name. Taking + * the shape of { [phase_id]: phase_name } + */ +export const usePhaseNameLookup = (phases) => + useMemo( + () => + phases.reduce( + (obj, item) => + Object.assign(obj, { + [item.phase_id]: item.phase_name, + }), + {} + ), + [phases] + ); + +/** + * Hook which returns an object of subphase IDs with their name. Taking + * the shape of { [subphase_id]: subphase_name } + */ +export const useSubphaseNameLookup = (subphases) => + useMemo( + () => + subphases.reduce( + (obj, item) => + Object.assign(obj, { + [item.subphase_id]: item.subphase_name, + }), + {} + ), + [subphases] + ); + +/** + * Hook which returns an array of project_phase_ids of the project's current phase(s). + * Although only one phase should ever be current, we handle the possibilty that there + * are multiple + * @param {Array} projectPhases - array of this project's moped_proj_phases + * @return {Array} of project_phase_id's of current project phases + */ +export const useCurrentProjectPhaseIDs = (projectPhases) => + useMemo( + () => + projectPhases + ? projectPhases + .filter(({ is_current_phase }) => is_current_phase) + .map(({ project_phase_id }) => project_phase_id) + : [], + [projectPhases] + ); + +/** + * Hook which returns an array of `moped_proj_phases.project_phase_id`s which + * need to have their `is_current` flag cleared. + * @param {int} thisProjectPhaseId - the `project_phase_id` that is being edited + * @param {bool} isCurrent - if the phase that is being edited is set as the current phase + * @param {array} currentProjectPhaseIds - an array of all project_phase_ids that are marked as current. + * (this is the output of the useCurrentProjectPhaseIDs hook) + * @return {Array} of project_phase_id's which need to set to `is_current` = false + */ +export const useCurrentPhaseIdsToClear = ( + thisProjectPhaseId, + isCurrent, + currentProjectPhaseIds +) => { + if (!isCurrent) { + // nothing to do + return []; + } + // return all project phase IDs except the one we're editing + return currentProjectPhaseIds.filter( + (projectPhaseId) => projectPhaseId !== thisProjectPhaseId + ); +}; + +export const onSubmitPhase = ({ + data, + mutate, + currentPhaseIdsToClear, + onSubmitCallback, +}) => { + const { project_phase_id, ...formData } = data; + + const variables = { + current_phase_ids_to_clear: currentPhaseIdsToClear, + }; + + if (!project_phase_id) { + // inserting a new mutation - which has a slightly different + // variable shape bc the mutation supports multiple inserts + // via the phase template feature + variables.objects = [formData]; + } else { + variables.project_phase_id = project_phase_id; + variables.object = formData; + } + + mutate({ + variables, + refetchQueries: ["ProjectSummary"], + }).then(() => onSubmitCallback()); +}; diff --git a/moped-editor/src/views/projects/projectView/ProjectPhaseDateConfirmationPopover.js b/moped-editor/src/views/projects/projectView/ProjectPhaseDateConfirmationPopover.js new file mode 100644 index 0000000000..67eaa341a1 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectPhaseDateConfirmationPopover.js @@ -0,0 +1,49 @@ +import { useState } from "react"; +import { Popover, Typography } from "@mui/material"; + +/** + * Shows a popover indicating if a date is "estimated" + * @param {boolean} isEnabled - if the popover should be enabled/active + * @param {string} dataType - the date type that we be included in popover text. expecting `start` or `end` + */ +const DateConfirmationPopover = ({ children, isEnabled, dateType }) => { + const [anchorEl, setAnchorEl] = useState(null); + + const handlePopoverOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handlePopoverClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + return ( +
+ {children} + + {`Estimated ${dateType} date`} + +
+ ); +}; + +export default DateConfirmationPopover; diff --git a/moped-editor/src/views/projects/projectView/ProjectPhaseDialog.js b/moped-editor/src/views/projects/projectView/ProjectPhaseDialog.js new file mode 100644 index 0000000000..c9947627b3 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectPhaseDialog.js @@ -0,0 +1,43 @@ +import IconButton from "@mui/material/IconButton"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import CloseIcon from "@mui/icons-material/Close"; +import ProjectPhaseForm from "./ProjectPhaseForm"; + +const ProjectPhaseDialog = ({ + onClose, + phase, + phases, + currentProjectPhaseIds, + onSubmitCallback, +}) => { + const titleText = phase.project_phase_id ? "Edit phase" : "Add phase"; + return ( + + + {titleText} + + + + + + + + + ); +}; + +export default ProjectPhaseDialog; diff --git a/moped-editor/src/views/projects/projectView/ProjectPhaseForm.js b/moped-editor/src/views/projects/projectView/ProjectPhaseForm.js new file mode 100644 index 0000000000..92a2187ecb --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectPhaseForm.js @@ -0,0 +1,292 @@ +import { useEffect } from "react"; +import { useMutation } from "@apollo/client"; +import { useForm } from "react-hook-form"; +import Alert from "@mui/material/Alert"; +import Button from "@mui/material/Button"; +import CheckCircle from "@mui/icons-material/CheckCircle"; +import CircularProgress from "@mui/material/CircularProgress"; +import FormControl from "@mui/material/FormControl"; +import FormHelperText from "@mui/material/FormHelperText"; +import Grid from "@mui/material/Grid"; +import { yupResolver } from "@hookform/resolvers/yup"; +import ControlledAutocomplete from "src/components/forms/ControlledAutocomplete"; +import ControlledDateField from "src/components/forms/ControlledDateField"; +import ControlledTextInput from "src/components/forms/ControlledTextInput"; +import ControlledCheckbox from "src/components/forms/ControlledCheckbox"; +import ControlledSwitch from "src/components/forms/ControlledSwitch"; +import { + phaseValidationSchema, + onSubmitPhase, + useDefaultValues, + useSubphases, + useCurrentPhaseIdsToClear, +} from "./ProjectPhase/helpers"; +import { useResetDependentFieldOnParentFieldChange } from "./ProjectComponents/utils/form"; +import { UPDATE_PROJECT_PHASE, ADD_PROJECT_PHASE } from "src/queries/project"; + +const ProjectPhaseForm = ({ + phase, + phases, + currentProjectPhaseIds, + onSubmitCallback, +}) => { + const isNewPhase = !phase.project_phase_id; + + const defaultValues = useDefaultValues(phase); + + /** initiatlize react hook form with validation */ + const { + handleSubmit, + control, + watch, + setValue, + formState: { isDirty, errors: formErrors }, + } = useForm({ + defaultValues, + resolver: yupResolver(phaseValidationSchema), + }); + + const subphases = useSubphases(watch("phase_id"), phases); + + const isCurrentPhase = watch("is_current_phase"); + + useResetDependentFieldOnParentFieldChange({ + parentValue: watch("phase_id"), + dependentFieldName: "subphase_id", + valueToSet: null, + setValue, + }); + + const [mutate, mutationState] = useMutation( + isNewPhase ? ADD_PROJECT_PHASE : UPDATE_PROJECT_PHASE + ); + + const currentPhaseIdsToClear = useCurrentPhaseIdsToClear( + phase.project_phase_id, + isCurrentPhase, + currentProjectPhaseIds + ); + + const [phase_start, phase_end] = watch(["phase_start", "phase_end"]); + + /** + * Defaults is_phase_start_confirmed to true if date is today or before + */ + useEffect(() => { + if (phase_start !== defaultValues.phase_start) { + // phase start has been edited + if ( + phase_start && + new Date(phase_start).getTime() < new Date().getTime() + ) { + // date is in the past, so default to confirmed + setValue("is_phase_start_confirmed", true); + } else if ( + phase_start && + new Date(phase_start).getTime() > new Date().getTime() + ) { + // date is in the future, so default to not confirmed + setValue("is_phase_start_confirmed", false); + } + } + // clear confirmed status as needed + if (phase_start === null) { + setValue("is_phase_start_confirmed", false); + } + }, [phase_start, defaultValues, setValue]); + + /** + * Defaults is_phase_end_confirmed to true if date is today or before + */ + useEffect(() => { + if (phase_end !== defaultValues.phase_end) { + // phase end has been edited + if (phase_end && new Date(phase_end).getTime() <= new Date().getTime()) { + // date is in the past, so default to confirmed + setValue("is_phase_end_confirmed", true); + } else if ( + phase_end && + new Date(phase_end).getTime() > new Date().getTime() + ) { + // date is in the future, so default to not confirmed + setValue("is_phase_end_confirmed", false); + } + } + // clear confirmed status as needed + if (phase_end === null) { + setValue("is_phase_end_confirmed", false); + } + }, [phase_end, defaultValues, setValue]); + + if (mutationState.error) { + console.error(mutationState.error); + return ( + + + + Something went wrong. Refresh the page to try again. + + + + ); + } + + return ( +
+ onSubmitPhase({ + data, + currentPhaseIdsToClear, + mutate, + onSubmitCallback, + }) + )} + autoComplete="off" + > + + + + + field.onChange(phase?.phase_id || null) + } + valueHandler={(value) => + value ? phases.find((p) => p.phase_id === value) : null + } + isOptionEqualToValue={(option, selectedOption) => + option?.phase_id === selectedOption?.phase_id + } + getOptionLabel={(option) => option?.phase_name || ""} + error={!!formErrors?.phase_id} + /> + {formErrors?.phase_id && ( + {formErrors.phase_id.message} + )} + + + + + 0} + onChangeHandler={(subphase, field) => + field.onChange(subphase?.subphase_id || null) + } + valueHandler={(value) => + value + ? subphases.find((s) => s.subphase_id === value) || null + : null + } + isOptionEqualToValue={(option, selectedOption) => + option?.subphase_id === selectedOption?.subphase_id + } + getOptionLabel={(option) => option?.subphase_name || ""} + error={!!formErrors?.subphase_id} + /> + {formErrors?.subphase_id && ( + {formErrors.subphase_id.message} + )} + + + + + + {formErrors?.phase_start && ( + {formErrors?.phase_start.message} + )} + + + + + + + + + + + {formErrors?.phase_end && ( + {formErrors.phase_end.message} + )} + + + + + + + + + + + {formErrors?.phase_description && ( + + {formErrors.phase_description.message} + + )} + + + + + + + + + + + + + +
+ ); +}; + +export default ProjectPhaseForm; diff --git a/moped-editor/src/views/projects/projectView/ProjectPhaseToolbar.js b/moped-editor/src/views/projects/projectView/ProjectPhaseToolbar.js new file mode 100644 index 0000000000..fb8c221e54 --- /dev/null +++ b/moped-editor/src/views/projects/projectView/ProjectPhaseToolbar.js @@ -0,0 +1,24 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import ButtonDropdownMenu from "src/components/ButtonDropdownMenu"; + +/** Custom toolbar title that resembles material table titles */ +const ProjectPhaseToolbar = ({ addAction, setIsDialogOpen }) => ( + + + Phases + +
+ +
+
+); + +export default ProjectPhaseToolbar; diff --git a/moped-editor/src/views/projects/projectView/ProjectPhases.js b/moped-editor/src/views/projects/projectView/ProjectPhases.js index 17445690b0..1e01a286c5 100644 --- a/moped-editor/src/views/projects/projectView/ProjectPhases.js +++ b/moped-editor/src/views/projects/projectView/ProjectPhases.js @@ -1,411 +1,231 @@ -import React, { useState } from "react"; - -// Material -import { - CircularProgress, - Typography, - FormControl, - FormHelperText, - TextField, - Box, -} from "@mui/material"; -import Autocomplete from "@mui/material/Autocomplete"; +import { useCallback, useMemo, useState } from "react"; +import { useMutation } from "@apollo/client"; +import { CircularProgress, Box, IconButton } from "@mui/material"; +import { DataGrid } from "@mui/x-data-grid"; import { green } from "@mui/material/colors"; - import { EditOutlined as EditOutlinedIcon, DeleteOutline as DeleteOutlineIcon, CheckCircleOutline, } from "@mui/icons-material"; -import MaterialTable, { - MTableEditRow, - MTableAction, - MTableToolbar, -} from "@material-table/core"; -import typography from "../../../theme/typography"; - -// Query -import { - UPDATE_PROJECT_PHASES_MUTATION, - DELETE_PROJECT_PHASE, - ADD_PROJECT_PHASE, - CLEAR_CURRENT_PROJECT_PHASES_MUTATION, -} from "../../../queries/project"; -import { PAGING_DEFAULT_COUNT } from "../../../constants/tables"; -import { useMutation } from "@apollo/client"; -import { format } from "date-fns"; -import parseISO from "date-fns/parseISO"; - -import DateFieldEditComponent from "./DateFieldEditComponent"; -import ToggleEditComponent from "./ToggleEditComponent"; -import DropDownSelectComponent from "./DropDownSelectComponent"; -import ButtonDropdownMenu from "../../../components/ButtonDropdownMenu"; +import ProjectPhaseToolbar from "./ProjectPhaseToolbar"; import PhaseTemplateModal from "./PhaseTemplateModal"; - -/** - * Identify any current moped_proj_phases - * @param {Int} newCurrentPhaseId - the ID of the phase that should be marked as current - optional - * @param {Array} existingProjPhases - array of this project's moped_proj_phases - * @return {Array} array of moped_proj_phases.project_phase_id primary keys - */ -const getCurrentPhaseIDs = (newCurrentPhaseId, existingProjPhases) => - existingProjPhases - .filter( - ({ is_current_phase, project_phase_id }) => - is_current_phase && project_phase_id !== newCurrentPhaseId - ) - .map(({ project_phase_id }) => project_phase_id); - -/** - * Replace all object properties which are empty strings "" with null - */ -const replaceEmptyStrings = (obj) => { - Object.keys(obj).forEach((key) => { - if (obj[key] === "") { - obj[key] = null; - } - }); -}; +import ProjectPhaseDialog from "./ProjectPhaseDialog"; +import ProjectPhaseDateConfirmationPopover from "./ProjectPhaseDateConfirmationPopover"; +import { DELETE_PROJECT_PHASE } from "src/queries/project"; +import { + useCurrentProjectPhaseIDs, + usePhaseNameLookup, + useSubphaseNameLookup, +} from "./ProjectPhase/helpers"; + +/** Hook that provides memoized column settings */ +const useColumns = ({ deleteInProgress, onDeletePhase, setEditPhase }) => + useMemo(() => { + return [ + { + headerName: "Phase", + field: "moped_phase", + minWidth: 200, + valueGetter: ({ row }) => row.moped_phase?.phase_name, + }, + { + headerName: "Subphase", + field: "moped_subphase", + minWidth: 200, + valueGetter: ({ row }) => row.moped_subphase?.subphase_name, + }, + { + headerName: "Start", + field: "phase_start", + type: "date", + /** valueGetter is used by the date sort function inherently used by the `date` type column */ + valueGetter: ({ row }) => + row.phase_start ? new Date(row.phase_start) : null, + /** the renderCell function controls the react node rendered for this cell */ + renderCell: ({ row }) => { + let strToRender = row.phase_start + ? new Date(row.phase_start).toLocaleDateString() + : ""; + + const showNotConfirmedIndicator = + !row.is_phase_start_confirmed && strToRender; + + strToRender = showNotConfirmedIndicator + ? `${strToRender}*` + : strToRender; + return ( + + {strToRender} + + ); + }, + minWidth: 150, + }, + { + headerName: "End", + field: "phase_end", + type: "date", + /** valueGetter is used by the date sort function inherently used by the `date` type column */ + valueGetter: ({ row }) => + row.phase_end ? new Date(row.phase_end) : null, + /** the renderCell function controls the react node rendered for this cell */ + renderCell: ({ row }) => { + let strToRender = row.phase_end + ? new Date(row.phase_end).toLocaleDateString() + : ""; + + const showNotConfirmedIndicator = + !row.is_phase_end_confirmed && strToRender; + + strToRender = showNotConfirmedIndicator + ? `${strToRender}*` + : strToRender; + return ( + + {strToRender} + + ); + }, + minWidth: 150, + }, + { + headerName: "Description", + field: "phase_description", + minWidth: 350, + }, + { + headerName: "Current", + field: "is_current_phase", + minWidth: 50, + renderCell: ({ row }) => + row.is_current_phase ? ( + + + + ) : ( + "" + ), + }, + { + headerName: "", + field: "_edit", + sortable: false, + renderCell: ({ row }) => { + return deleteInProgress ? ( + + ) : ( +
+ setEditPhase(row)} + > + + + + onDeletePhase({ project_phase_id: row.project_phase_id }) + } + > + + +
+ ); + }, + }, + ]; + }, [deleteInProgress, onDeletePhase, setEditPhase]); /** * ProjectPhases Component - renders Project Phase table * @return {JSX.Element} * @constructor */ -const ProjectPhases = ({ - projectId, - loading, - data, - refetch, - projectViewRefetch, -}) => { - const [isMutating, setIsMutating] = useState(false); - const [isDialogOpen, setIsDialogOpen] = useState(false); - - // Mutations - const [updateProjectPhase] = useMutation(UPDATE_PROJECT_PHASES_MUTATION); - const [clearCurrentProjectPhases] = useMutation( - CLEAR_CURRENT_PROJECT_PHASES_MUTATION +const ProjectPhases = ({ projectId, data, refetch }) => { + const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); + const [editPhase, setEditPhase] = useState(null); + + const [deletePhase, { loading: deleteInProgress }] = + useMutation(DELETE_PROJECT_PHASE); + + const onClickAddPhase = () => setEditPhase({ project_id: projectId }); + + const onDeletePhase = useCallback( + ({ project_phase_id }) => { + window.confirm("Are you sure you want to delete this phase?") && + deletePhase({ + variables: { project_phase_id }, + refetchQueries: ["ProjectSummary"], + }).then(() => { + refetch(); + }); + }, + [deletePhase, refetch] ); - const [deleteProjectPhase] = useMutation(DELETE_PROJECT_PHASE); - const [addProjectPhase] = useMutation(ADD_PROJECT_PHASE); - - // Dropdown options - const phaseOptions = data?.moped_phases || []; - // Hide Phase template dialog - const handleTemplateModalClose = () => { - setIsDialogOpen(false); - }; - - // If the query is loading or data object is undefined, - // stop here and just render the spinner. - if (loading || !data) return ; - - /** - * Set is_current_phase of all proj phases except currentPhaseId to false - * to ensure there is only one active phase - */ - const updateExistingPhases = async (projPhasesIdsToUpdate) => { - // Execute update mutation - // We bundle all phases into this single mutation so that it can be cleanly awaited - await clearCurrentProjectPhases({ - variables: { ids: projPhasesIdsToUpdate }, - }).catch((err) => { - console.error(err); - }); - }; + const columns = useColumns({ + setEditPhase, + deleteInProgress, + onDeletePhase, + }); - const phaseNameLookup = data.moped_phases.reduce( - (obj, item) => - Object.assign(obj, { - [item.phase_id]: item.phase_name, - }), - {} + const currentProjectPhaseIds = useCurrentProjectPhaseIDs( + data?.moped_proj_phases ); - const subphaseNameLookup = data.moped_subphases.reduce( - (obj, item) => - Object.assign(obj, { - [item.subphase_id]: item.subphase_name, - }), - {} - ); + const phaseNameLookup = usePhaseNameLookup(data?.moped_phases || []); - /** - * Column configuration for Phases table - */ - const phasesColumns = [ - { - title: "Phase", - field: "moped_phase", - validate: (row) => !!row.moped_phase?.phase_id, - render: (row) => row.moped_phase?.phase_name, - editComponent: (props) => ( - - phase.phase_name} - isOptionEqualToValue={(option, value) => - option.phase_id === value.phase_id - } - value={props.value || null} - onChange={(event, value) => { - return props.onChange(value); - }} - renderInput={(params) => ( - - )} - /> - Required - - ), - width: "25%", - }, - { - title: "Subphase", - field: "subphase_id", - render: (rowData) => rowData.moped_subphase?.subphase_name, - editComponent: (props) => ( - - ), - width: "20%", - }, - { - title: "Description", - field: "phase_description", - width: "25%", - }, - { - title: "Start", - field: "phase_start", - render: (rowData) => - rowData.phase_start - ? format(parseISO(rowData.phase_start), "MM/dd/yyyy") - : undefined, - editComponent: (props) => , - width: "10%", - }, - { - title: "End", - field: "phase_end", - render: (rowData) => - rowData.phase_end - ? format(parseISO(rowData.phase_end), "MM/dd/yyyy") - : undefined, - editComponent: (props) => , - width: "10%", - }, - { - title: "Current", - field: "is_current_phase", - lookup: { true: "Yes", false: "No" }, - render: (rowData) => - rowData.is_current_phase ? ( - - - Yes - - ) : ( - "" - ), - editComponent: (props) => ( - - ), - width: "10%", - }, - ]; + const subphaseNameLookup = useSubphaseNameLookup(data?.moped_subphases || []); + + const onSubmitCallback = () => { + refetch().then(() => setEditPhase(null)); + }; return ( <> - ( - { - if (e.keyCode === 13) { - // Bypass default MaterialTable behavior of submitting the entire form when a user hits enter - // See https://github.com/mbrn/material-table/pull/2008#issuecomment-662529834 - } - }} - /> - ), - Action: (props) => { - // If isn't the add action - if ( - typeof props.action === typeof Function || - props.action.tooltip !== "Add" - ) { - return ; - } else { - return ( - - ); - } - }, - Toolbar: (props) => ( - // to have it align with table content -
- -
- ), - }} - editable={{ - onRowAdd: async (newData) => { - setIsMutating(true); - const { moped_phase, ...rest } = newData; - - const newPhasePayload = { - project_id: projectId, - phase_id: moped_phase.phase_id, - ...rest, - }; - - replaceEmptyStrings(newPhasePayload); - - // if necessary, updates existing phases in table to ensure only one is marked "current" - if (newPhasePayload.is_current_phase) { - const projPhasesIdsToUpdate = getCurrentPhaseIDs( - null, - data.moped_proj_phases - ); - if (projPhasesIdsToUpdate.length > 0) { - await updateExistingPhases(projPhasesIdsToUpdate); - } - } - - // Execute insert mutation, returns promise - await addProjectPhase({ - variables: { - objects: [newPhasePayload], - }, - }).catch((err) => { - console.error(err); - }); - // Refetch data - await refetch(); - if (!!projectViewRefetch) { - await projectViewRefetch(); - } - setIsMutating(false); - }, - onRowUpdate: async (newData, oldData) => { - setIsMutating(true); - const { - project_phase_id, - moped_phase, - moped_subphase, - __typename, - ...updatedPhasePayload - } = newData; - // extract phase_id from moped_phase object - updatedPhasePayload.phase_id = moped_phase.phase_id; - - // Remove extraneous fields given by MaterialTable that - // Hasura doesn't need - delete updatedPhasePayload.tableData; - - replaceEmptyStrings(updatedPhasePayload); - - // if necessary, updates existing phases in table to ensure only one is marked "current" - if (updatedPhasePayload.is_current_phase) { - const projPhasesIdsToUpdate = getCurrentPhaseIDs( - project_phase_id, - data.moped_proj_phases - ); - if (projPhasesIdsToUpdate.length > 0) { - await updateExistingPhases(projPhasesIdsToUpdate); - } - } - - // Execute update mutation - await updateProjectPhase({ - variables: { project_phase_id, object: updatedPhasePayload }, - }).catch((err) => { - console.error(err); - }); - // Refetch data - await refetch(); - - if (!!projectViewRefetch) { - await projectViewRefetch(); - } - setIsMutating(false); - }, - onRowDelete: async (oldData) => { - // Execute delete mutation - setIsMutating(true); - await deleteProjectPhase({ - variables: { - project_phase_id: oldData.project_phase_id, - }, - }).catch((err) => { - console.error(err); - }); - await refetch(); - - if (!!projectViewRefetch) { - await projectViewRefetch(); - } - setIsMutating(false); - }, + row.project_phase_id} + disableRowSelectionOnClick + disableColumnMenu + getRowHeight={() => "auto"} + hideFooterPagination={true} + localeText={{ noRowsLabel: "No phases" }} + rows={data?.moped_proj_phases || []} + slots={{ + toolbar: ProjectPhaseToolbar, }} - title={ - - Phases - - } - options={{ - ...(data.moped_proj_phases.length < PAGING_DEFAULT_COUNT + 1 && { - paging: false, - }), - search: false, - rowStyle: { fontFamily: typography.fontFamily }, - actionsColumnIndex: -1, - addRowPosition: "first", - idSynonym: "project_phase_id", - }} - localization={{ - header: { - actions: "", - }, - body: { - emptyDataSourceMessage: ( - - No project phases to display - - ), + slotProps={{ + toolbar: { + addAction: onClickAddPhase, + setIsDialogOpen: setIsTemplateDialogOpen, }, }} /> + {editPhase && ( + setEditPhase(null)} + onSubmitCallback={onSubmitCallback} + phases={data?.moped_phases} + currentProjectPhaseIds={currentProjectPhaseIds} + projectId={projectId} + /> + )} setIsTemplateDialogOpen(false)} selectedPhases={data.moped_proj_phases} phaseNameLookup={phaseNameLookup} subphaseNameLookup={subphaseNameLookup} diff --git a/moped-editor/src/views/projects/projectView/ProjectTimeline.js b/moped-editor/src/views/projects/projectView/ProjectTimeline.js index b93fc20b9e..761ed812af 100644 --- a/moped-editor/src/views/projects/projectView/ProjectTimeline.js +++ b/moped-editor/src/views/projects/projectView/ProjectTimeline.js @@ -18,7 +18,7 @@ import ProjectMilestones from "./ProjectMilestones"; * @return {JSX.Element} * @constructor */ -const ProjectTimeline = (props) => { +const ProjectTimeline = () => { /** Params Hook * @type {integer} projectId * */ @@ -36,8 +36,6 @@ const ProjectTimeline = (props) => { fetchPolicy: "no-cache", }); - const projectViewRefetch = props.refetch; - // If the query is loading or data object is undefined, // stop here and just render the spinner. if (loading || !data) return ; @@ -50,10 +48,8 @@ const ProjectTimeline = (props) => { diff --git a/moped-etl/arcgis/settings.py b/moped-etl/arcgis/settings.py index 6ce300b452..e1a4484e7c 100644 --- a/moped-etl/arcgis/settings.py +++ b/moped-etl/arcgis/settings.py @@ -56,6 +56,7 @@ signal_ids srts_id subcomponents + substantial_completion_date task_order_names type_name updated_at