import getAuthorizationHeaders from "../utilities/get_authorization_headers"; import Http from "../utilities/http"; import makeErrorsPretty from "../utilities/make_errors_pretty"; import { DateConverter } from "../converters"; import type { ApiClassificationCode, ApiCloudFile, ApiGcpSignedUrl, ApiMasterformatProgress, ApiProject, ApiProjectCostAnalysisProgress, ApiProjectCostAnalysisProgressValidationResult, ApiProjectDeviationSummary, ApiProjectListing, ApiProjectProgressSummary, ApiProjectWorkPackage, ApiProjectWorkPackageCost, ApiRunningProcess, ProgressType, ProjectWorkPackageType, User } from "../models"; import type { AssociationIds, DateLike } from "type_aliases"; import ApiMasterFormatWithUniformat from "../models/api/api_masterformat_with_uniformat"; import ApiCssContactSummary from "../models/api/api_css_contact_summary"; import ApiSupportRequest from "../models/api/api_support_request"; export default class ProjectApi { static listProjectsForOrganization(organizationId: string, user: User, filterArchived: boolean = false): Promise { const query = filterArchived ? "?filterArchived=true" : ""; let url = `${Http.baseUrl()}/client-accounts/${organizationId}/projects${query}`; return Http.get(url, user) as unknown as Promise; } static listAllProjectsForUser(user: User): Promise { let url = `${Http.baseUrl()}/projects/list-user-projects`; return Http.get(url, user) as unknown as Promise; } static listProjectsForUser(user: User): Promise { let url = `${Http.baseUrl()}/projectListings`; return Http.get(url, user) as unknown as Promise; } /** * @deprecated Call getProject and listProjectsForOrganization instead */ static listProjects(projectId: string, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/list`; return Http.get(url, user) as unknown as Promise; } static getProject(projectId: string, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}`; return Http.get(url, user) as unknown as Promise; } static getProjectFloorTradeDeviationMagnitudes({ projectId }: AssociationIds, currentScan: boolean, user: User): Promise<{[floorFirebaseId: string]: {[tradeCode: string]: number}}> { let url = `${Http.baseUrl()}/projects/${projectId}/deviations-by-floor`; if (currentScan) { url += "?currentScan=true"; } return Http.get(url, user) as unknown as Promise<{[floorFirebaseId: string]: {[tradeCode: string]: number}}>; } static getProjectFloorBuiltElementCounts({ projectId }: AssociationIds, user: User): Promise<{[floorFirebaseId: string]: number}> { let url = `${Http.baseUrl()}/projects/${projectId}/built-elements-by-floor`; return Http.get(url, user) as unknown as Promise<{[floorFirebaseId: string]: number}>; } static getProjectFloorTradeBuiltElementCounts({ projectId }: AssociationIds, user: User): Promise<{[floorFirebaseId: string]: {[tradeCode: string]: number}}> { let url = `${Http.baseUrl()}/projects/${projectId}/built-elements-by-floor-and-trade`; return Http.get(url, user) as unknown as Promise<{[floorFirebaseId: string]: {[tradeCode: string]: number}}>; } static getProjectDeviationSummary({ projectId }: AssociationIds, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/deviation-summary`; return Http.get(url, user) as unknown as Promise; } static getProjectProgressSummary({ projectId }: AssociationIds, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/progress-summary`; return Http.get(url, user) as unknown as Promise; } static createProject(organizationId: string, project: ApiProject, user: User): Promise<{ firebaseId: string }> { let url = `${Http.baseUrl()}/client-accounts/${organizationId}/projects`; return Http.post(url, user, project) as unknown as Promise<{ firebaseId: string }>; } static updateProject(projectId: string, project: ApiProject, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}`; return Http.patch(url, user, project) as unknown as Promise; } static archiveProject(projectId: string, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/archive`; return Http.post(url, user, null) as unknown as Promise; } static unarchiveProject(projectId: string, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/unarchive`; return Http.post(url, user, null) as unknown as Promise; } static saveProjectCostAnalysisProgress({ projectId }: AssociationIds, progress, user: User): Promise { const url = `${Http.baseUrl()}/projects/${projectId}/cost-analysis-progress`; return Http.post(url, user, progress) as unknown as Promise; } static saveScannedProjectMasterformatProgress({ projectId }: AssociationIds, progress: ApiMasterformatProgress, user: User): Promise { const url = `${Http.baseUrl()}/projects/${projectId}/scanned-masterformat-progress`; return Http.post(url, user, progress) as unknown as Promise; } // TODO: Make schedule type an enum? static saveScheduledProjectMasterformatProgress({ projectId }: AssociationIds, progress: ApiMasterformatProgress, scheduleType: "current" | "baseline", user: User): Promise { const url = `${Http.baseUrl()}/projects/${projectId}/scheduled-masterformat-progress/${scheduleType}`; return Http.post(url, user, progress) as unknown as Promise; } static getProjectCostAnalysisProgress({ projectId }: AssociationIds, user: User): Promise { const url = `${Http.baseUrl()}/projects/${projectId}/cost-analysis-progress`; return Http.get(url, user) as unknown as Promise; } static getScannedProjectMasterformatProgress({ projectId }: AssociationIds, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/scanned-masterformat-progress`; return Http.get(url, user) as unknown as Promise; } static getMasterformatsWithPbesInProject(projectId: string, version: number, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/masterformats-with-pbes?masterformatVersion=${version}`; return Http.get(url, user) as unknown as Promise; } static uploadAiAssistedProgressMasterFormatScheduleTsv(projectId, tsvContent: string, isCorrectedTsvFile:string, user: User) { let url = `${Http.baseUrl()}/projects/${projectId}/hephaestus`; let multipartFormData = new FormData(); let file = new Blob([tsvContent], { type: "text/tab-separated-values" }); multipartFormData.append("tsvFile", file, "file.tsv"); multipartFormData.append("isCorrectedTsvFile", isCorrectedTsvFile); return Http.fetch(url, { method: "POST", headers: { ...getAuthorizationHeaders(user) }, body: multipartFormData }) as unknown as Promise; } static DownloadAiAssistedMasterFormatScheduleTsv(projectId, user: User) { let url = `${Http.baseUrl()}/projects/${projectId}/hephaestus`; return (fetch(url, { method: "GET", headers: { ...getAuthorizationHeaders(user) }, })) as unknown as Promise } static getCssContactsForProject(projectId: string, user: User): Promise { let url = `${Http.baseUrl()}/support-center/css-contacts?projectId=${encodeURIComponent(projectId)}`; return Http.get(url, user) as unknown as Promise; } static submitSupportRequest(projectId: string, { description, sourcePage, featureOfInterest, files }: { description: string, sourcePage?: string, featureOfInterest?: string, files?: File[] }, user: User): Promise { let url = `${Http.baseUrl()}/support-center/support-requests?projectId=${encodeURIComponent(projectId)}`; let multipartFormData = new FormData(); multipartFormData.append("description", description); if (sourcePage != null) { multipartFormData.append("sourcePage", sourcePage); } if (featureOfInterest != null) { multipartFormData.append("featureOfInterest", featureOfInterest); } (files || []).forEach((file) => { multipartFormData.append("attachments", file, file.name); }); return Http.fetch(url, { method: "POST", headers: { ...getAuthorizationHeaders(user) }, body: multipartFormData }) as unknown as Promise; } /** * @deprecated use getProjectMasterformatProgress */ static getScheduledProjectMasterformatProgress({ projectId }: AssociationIds, scheduleType: "current" | "baseline", user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/scheduled-masterformat-progress/${scheduleType}`; return Http.get(url, user) as unknown as Promise; } static getProjectMasterformatProgress({ projectId }: AssociationIds, scheduleType: ProgressType, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/scheduled-masterformat-progress/${scheduleType}`; return Http.get(url, user) as unknown as Promise; } static getAllFloorsMasterformatProgress({ projectId }: AssociationIds, progressType: ProgressType, user: User): Promise<{[floorFirebaseId: string]: ApiMasterformatProgress[]}> { const query = progressType == null ? "" : "?progressType=" + progressType; const url = `${Http.baseUrl()}/projects/${projectId}/masterformat-progresses${query}`; return Http.get(url, user) as unknown as Promise<{[floorFirebaseId: string]: ApiMasterformatProgress[]}>; } static listProjectFloorFiles({ projectId }: AssociationIds, user: User): Promise { let url = `${Http.baseUrl()}/projects/${projectId}/floor-files`; return Http.get(url, user) as unknown as Promise; } static getProjectDeviationsReportTsvUrl({ projectId }: AssociationIds, fileName: string, user: User): string { const baseUrl = `${Http.baseUrl()}/projects/${projectId}/${fileName}_deviation-report.tsv`; return Http.addAuthToDownloadUrl(baseUrl, user); } static getProjectDeviationsReportTsv({ projectId }: AssociationIds, user: User): Promise { const baseUrl = `${Http.baseUrl()}/projects/${projectId}/project_deviation-report.tsv`; const url = Http.addAuthToDownloadUrl(baseUrl, user); return Http.get(url, user, "text/tab-separated-values; charset=utf-8") as unknown as Promise; } static getTradeBreakdownTsvUrl({ projectId }: AssociationIds, fileName: string, user: User): string { const baseUrl = `${Http.baseUrl()}/projects/${projectId}/trade-breakdown/${fileName}_trade-breakdown.tsv`; return Http.addAuthToDownloadUrl(baseUrl, user); } static saveTradeBreakdownTsv({ projectId }: AssociationIds, tsvContent: string, user: User) { let multipartFormData = new FormData(); let file = new Blob([tsvContent], { type: "text/tab-separated-values" }); multipartFormData.append("file", file, "file.tsv"); const url = `${Http.baseUrl()}/projects/${projectId}/trade-breakdown`; return Http.fetch(url, { method: "POST", headers: { ...getAuthorizationHeaders(user) }, body: multipartFormData }) as unknown as Promise; } static getWorkPackages(projectId: string, user: User) { let url = `${Http.baseUrl()}/projects/${projectId}/work-packages`; return Http.get(url, user) as unknown as Promise; } static saveWorkPackages(projectId: string, user: User, workPackages: ApiProjectWorkPackage[]) { let url = `${Http.baseUrl()}/projects/${projectId}/work-packages`; return Http.put(url, user, workPackages) as unknown as Promise; } static generateMasterformatProgress({ projectId }: AssociationIds, reportDate: DateLike, masterformatVersion: number, user: User) { const formattedDate = DateConverter.dateToISO(reportDate); const url = `${Http.baseUrl()}/projects/${projectId}/masterformat-progress-report?masterformatVersion=${masterformatVersion}&reportDate=${formattedDate}`; return Http.post(url, user) as unknown as Promise; } static getFiltered5dTsv({ projectId }: AssociationIds, projectName: string, fileName: string, filter, user: User) { const url = `${Http.baseUrl()}/projects/${projectId}/cost-analysis-progress/line-items/${fileName}`; return Http.put(url, user, filter, "application/json", "text/tab-separated-values") as unknown as Promise; } static getClassificationCodes({ projectId }: AssociationIds, user: User) { const url = `${Http.baseUrl()}/projects/${projectId}/classification-codes`; return Http.get(url, user) as unknown as Promise; } static checkWbsStatus({ projectId }: AssociationIds, user: User) { const url = `${Http.baseUrl()}/projects/${projectId}/cost-analysis-progress/validations`; return Http.get(url, user) as unknown as Promise; }; static updateMasterformatSelection({ projectId, floorId }: AssociationIds, masterformatTree, user: User) { const url = `${Http.baseUrl()}/projects/${projectId}/floors/${floorId}/cost-analysis-progress-modeled-total-cost`; return Http.put(url, user, masterformatTree) as unknown as Promise; } static getWorkPackageCosts({ projectId }: AssociationIds, workPackageType: ProjectWorkPackageType, user: User) { const url = `${Http.baseUrl()}/projects/${projectId}/work-packages/${workPackageType}/costs`; return Http.get(url, user) as unknown as Promise; } static generateAllMasterformatProgress({ projectId }: AssociationIds, reportDate: DateLike, masterformatVersion: number, user: User): Promise { const formattedDate = DateConverter.dateToISO(reportDate); let url = `${Http.baseUrl()}/projects/${projectId}/generate-all-masterformat-progress?masterformatVersion=${masterformatVersion}&reportDate=${formattedDate}`; return Http.post(url, user) as unknown as Promise; } static importProjectData({ projectId }: AssociationIds, tsvContent: string, user: User) { let multipartFormData = new FormData(); let file = new Blob([tsvContent], { type: "text/tab-separated-values" }); multipartFormData.append("file", file, "file.tsv"); const url = `${Http.baseUrl()}/projects/${projectId}/import-project-data`; return Http.fetch(url, { method: "POST", headers: { ...getAuthorizationHeaders(user) }, body: multipartFormData }) as unknown as Promise; } // Direct-to-storage import: the browser uploads the TSV straight to GCS (Firebase SDK or a signed URL from // getSignedUploadUrl), then hands the gateway just the storage path. Returns the running process to poll, // like importProjectData — but the file bytes never go through the gateway request (no 32MB / Heroku limit). static importProjectDataFromStorage({ projectId }: AssociationIds, storagePath: string, user: User): Promise { const url = `${Http.baseUrl()}/projects/${projectId}/import-project-data-from-storage`; return Http.post(url, user, { storagePath }) as unknown as Promise; } // Get a short-lived signed PUT URL for uploading a file to a storage path in this project (mechanism B of // the direct-to-storage upload, for environments where the Firebase Storage SDK path isn't used). static getSignedUploadUrl({ projectId }: AssociationIds, storagePath: string, user: User): Promise { const url = `${Http.baseUrl()}/projects/${projectId}/gcpSignedUploadUrl`; return Http.post(url, user, { storagePath }) as unknown as Promise; } } makeErrorsPretty(ProjectApi, { exclude: ["getProjectDeviationsReportTsvUrl", "getTradeBreakdownTsvUrl"], overrideErrorMessage: ["getFiltered5dTsv", "saveTradeBreakdownTsv"] });