diff --git a/admin/datasource.ts b/admin/datasource.ts index fa59605c..161ef3d4 100644 --- a/admin/datasource.ts +++ b/admin/datasource.ts @@ -29,10 +29,16 @@ import { ImplementationLaborCost } from "@shared/entities/cost-inputs/implementa import { BaseSize } from "@shared/entities/base-size.entity.js"; import { BaseIncrease } from "@shared/entities/base-increase.entity.js"; import { ModelAssumptions } from "@shared/entities/model-assumptions.entity.js"; +import { UserUploadCostInputs } from "@shared/entities/users/user-upload-cost-inputs.entity.js"; +import { UserUploadRestorationInputs } from "@shared/entities/users/user-upload-restoration-inputs.entity.js"; +import { UserUploadConservationInputs } from "@shared/entities/users/user-upload-conservation-inputs.entity.js"; // TODO: If we import the COMMON_DATABASE_ENTITIES from shared, we get an error where DataSouce is not set for a given entity export const ADMINJS_ENTITIES = [ User, + UserUploadCostInputs, + UserUploadRestorationInputs, + UserUploadConservationInputs, ApiEventsEntity, Country, ProjectSize, diff --git a/admin/index.ts b/admin/index.ts index 5e0e41a0..25a33b38 100644 --- a/admin/index.ts +++ b/admin/index.ts @@ -33,6 +33,9 @@ import { ImplementationLaborCostResource } from "./resources/implementation-labo import { BaseSizeResource } from "./resources/base-size/base-size.resource.js"; import { BaseIncreaseResource } from "./resources/base-increase/base-increase.resource.js"; import { ModelAssumptionResource } from "./resources/model-assumptions/model-assumptions.resource.js"; +import { UserUploadCostInputs } from "@shared/entities/users/user-upload-cost-inputs.entity.js"; +import { UserUploadConservationInputs } from "@shared/entities/users/user-upload-conservation-inputs.entity.js"; +import { UserUploadRestorationInputs } from "@shared/entities/users/user-upload-restoration-inputs.entity.js"; AdminJS.registerAdapter({ Database: AdminJSTypeorm.Database, @@ -59,6 +62,36 @@ const start = async () => { componentLoader, resources: [ UserResource, + { + resource: UserUploadCostInputs, + name: "UserUploadCostInputs", + options: { + navigation: { + name: "User Data", + icon: "File", + }, + }, + }, + { + resource: UserUploadConservationInputs, + name: "UserUploadConservationInputs", + options: { + navigation: { + name: "User Data", + icon: "File", + }, + }, + }, + { + resource: UserUploadRestorationInputs, + name: "UserUploadRestorationInputs", + options: { + navigation: { + name: "User Data", + icon: "File", + }, + }, + }, ProjectSizeResource, FeasibilityAnalysisResource, ConservationAndPlanningAdminResource, @@ -119,7 +152,7 @@ const start = async () => { app.listen(PORT, () => { console.log( - `AdminJS started on http://localhost:${PORT}${admin.options.rootPath}` + `AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`, ); }); }; diff --git a/api/src/modules/import/import.controller.ts b/api/src/modules/import/import.controller.ts index 9f205a7f..9a4292fd 100644 --- a/api/src/modules/import/import.controller.ts +++ b/api/src/modules/import/import.controller.ts @@ -1,5 +1,11 @@ -import { Controller, UseGuards, UseInterceptors } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; +import { + Controller, + HttpStatus, + UploadedFiles, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; import { JwtAuthGuard } from '@api/modules/auth/guards/jwt-auth.guard'; import { RolesGuard } from '@api/modules/auth/guards/roles.guard'; import { RequiredRoles } from '@api/modules/auth/decorators/roles.decorator'; @@ -13,6 +19,7 @@ import { ControllerResponse } from '@api/types/controller-response.type'; import { Multer } from 'multer'; import { GetUser } from '@api/modules/auth/decorators/get-user.decorator'; import { User } from '@shared/entities/users/user.entity'; +import { usersContract } from '@shared/contracts/users.contract'; @Controller() @UseGuards(JwtAuthGuard, RolesGuard) @@ -36,4 +43,22 @@ export class ImportController { }; }); } + + @UseInterceptors(FilesInterceptor('files', 2)) + @RequiredRoles(ROLES.PARTNER, ROLES.ADMIN) + @TsRestHandler(usersContract.uploadData) + async uploadData( + @GetUser() user: User, + @UploadedFiles() files: Array, + ): Promise { + return tsRestHandler(usersContract.uploadData, async () => { + const [file1, file2] = files; + const [file1Buffer, file2Buffer] = [file1.buffer, file2.buffer]; + const data = await this.service.importDataProvidedByPartner( + [file1Buffer, file2Buffer], + user.id, + ); + return { body: data, status: HttpStatus.OK }; + }); + } } diff --git a/api/src/modules/import/import.service.ts b/api/src/modules/import/import.service.ts index da4de929..b4f5c57f 100644 --- a/api/src/modules/import/import.service.ts +++ b/api/src/modules/import/import.service.ts @@ -8,6 +8,15 @@ import { ImportRepository } from '@api/modules/import/import.repostiory'; import { EventBus } from '@nestjs/cqrs'; import { API_EVENT_TYPES } from '@api/modules/api-events/events.enum'; import { ImportEvent } from '@api/modules/import/events/import.event'; +import { DataSource } from 'typeorm'; +import { + userDataConservationInputMapJsonToEntity, + userDataCostInputsMapJsonToEntity, + userDataRestorationInputMapJsonToEntity, +} from '@api/modules/import/services/user-data-parser'; +import { UserUploadCostInputs } from '@shared/entities/users/user-upload-cost-inputs.entity'; +import { UserUploadRestorationInputs } from '@shared/entities/users/user-upload-restoration-inputs.entity'; +import { UserUploadConservationInputs } from '@shared/entities/users/user-upload-conservation-inputs.entity'; @Injectable() export class ImportService { @@ -24,6 +33,7 @@ export class ImportService { private readonly importRepo: ImportRepository, private readonly preprocessor: EntityPreprocessor, private readonly eventBus: EventBus, + private readonly dataSource: DataSource, ) {} async import(fileBuffer: Buffer, userId: string) { @@ -44,4 +54,36 @@ export class ImportService { registerImportEvent(userId: string, eventType: typeof this.eventMap) { this.eventBus.publish(new ImportEvent(eventType, userId, {})); } + + async importDataProvidedByPartner(fileBuffers: Buffer[], userId: string) { + // TODO: Debt, add event handling + const { costInputs, carbonInputs } = + await this.excelParser.parseUserExcels(fileBuffers); + const mappedCostInputs = userDataCostInputsMapJsonToEntity( + costInputs, + userId, + ); + const mappedRestorationInputs = userDataRestorationInputMapJsonToEntity( + carbonInputs.restoration, + userId, + ); + const mappedConservationInputs = userDataConservationInputMapJsonToEntity( + carbonInputs.conservation, + userId, + ); + await this.dataSource.transaction(async (manager) => { + const userCostInputsRepo = manager.getRepository(UserUploadCostInputs); + const userRestorationInputsRepo = manager.getRepository( + UserUploadRestorationInputs, + ); + const userConservationInputsRepo = manager.getRepository( + UserUploadConservationInputs, + ); + await userCostInputsRepo.save(mappedCostInputs); + await userRestorationInputsRepo.save(mappedRestorationInputs); + await userConservationInputsRepo.save(mappedConservationInputs); + }); + // + return carbonInputs; + } } diff --git a/api/src/modules/import/services/excel-parser.interface.ts b/api/src/modules/import/services/excel-parser.interface.ts index 0f9bb1d6..315b8e94 100644 --- a/api/src/modules/import/services/excel-parser.interface.ts +++ b/api/src/modules/import/services/excel-parser.interface.ts @@ -32,4 +32,6 @@ export const SHEETS_TO_PARSE = [ export interface ExcelParserInterface { parseExcel(data: Buffer): Promise; + + parseUserExcels(data: Buffer[]): Promise; } diff --git a/api/src/modules/import/services/user-data-parser.ts b/api/src/modules/import/services/user-data-parser.ts new file mode 100644 index 00000000..edc711fa --- /dev/null +++ b/api/src/modules/import/services/user-data-parser.ts @@ -0,0 +1,150 @@ +// I feel dirty doing this... + +import { UserUploadCostInputs } from '@shared/entities/users/user-upload-cost-inputs.entity'; +import { UserUploadRestorationInputs } from '@shared/entities/users/user-upload-restoration-inputs.entity'; +import { UserUploadConservationInputs } from '@shared/entities/users/user-upload-conservation-inputs.entity'; + +export function userDataCostInputsMapJsonToEntity( + inputJson: Record, + userId: string, +): Partial { + return { + programName: inputJson['Program name (if willing to share)'], + intendedLengthOfProject: inputJson['Intended length of project'], + country: inputJson['Country'], + currency: inputJson['Currency'], + projectStartYear: inputJson['Project start year'], + projectActivity: inputJson['Project activity'], + ecosystem: inputJson['Ecosystem'], + projectSize: inputJson['Project size'], + validationStandard: + inputJson['Validation standard / accrediting organization'], + numberOfLocalIndividuals: + inputJson[ + 'Number of local individuals who own, work, and/or live in the project site (e.g., land tenure)' + ], + cityOrRegion: inputJson['City / province / region / state of project'], + intendedAlternativeUseOfLand: inputJson['Intended alternative use of land'], + landOwnershipBeforeProject: inputJson['Land ownership before project'], + sdgsBenefitted: inputJson['SDGs benefitted / co-benefitted'], + projectEligibleForCarbonCredits: + inputJson['Project eligible for voluntary carbon credits?'] === 'yes', + willingToSpeakAboutPricing: + inputJson[ + 'Are you willing to speak with us about your carbon credit pricing?' + ] === 'yes', + ableToProvideDetailedCostDocumentation: + inputJson[ + 'Are you able to provide additional detailed cost documentation for the project?' + ] === 'yes', + costCategories: inputJson['Cost categories '], + establishingCommunityEngagement: + inputJson['Establishing community engagement / buy-in'], + conservationProjectPlanning: + inputJson['Conservation project planning & administration '], + carbonProjectPlanning: + inputJson['Carbon project planning & administration'], + landCost: inputJson['Land cost'], + financingCost: inputJson['Financing cost'], + materialsSeedsFertilizer: + inputJson['Materials (e.g., seeds, fertilizer, seedlings)'], + materialsMachineryEquipment: + inputJson['Materials (e.g., machinery, equipment, etc.)'], + projectLaborActivity: inputJson['Project labor / activity'], + engineeringIntervention: + inputJson['Engineering / construction intervention'], + ongoingCommunityEngagement: inputJson['Ongoing community engagement'], + otherProjectRunningCost: inputJson['Other project running cost'], + projectMonitoring: inputJson['Project monitoring'], + otherCost1: inputJson['1) Other cost (please specify activities)'], + otherCost2: inputJson['2) Other cost (please specify activities)'], + otherCost3: inputJson['3) Other cost (please specify activities)'], + projectCumulativeSequestration: + inputJson['Project site cumulative sequestration / carbon stock'], + detailedProjectActivity: + inputJson[ + 'Please describe in detail the project activity (e.g., planted mangrove seedlings, set up perimeter around conservation area)' + ], + communityEngagementSpending: + inputJson[ + 'When you kicked off the project, how did you spend to engage the community...' + ], + landRightsAndEasements: + inputJson[ + 'How did you acquire the rights to establish the project on the land?...' + ], + hourlyWageRate: + inputJson[ + 'What was the hourly wage rate paid for labor? How many hours worked for each activity?' + ], + ongoingCommunityCompensation: + inputJson[ + 'Please describe the ongoing community engagement for your project.' + ], + engineeringDetails: + inputJson[ + 'Did you undertake any engineering / construction interventions for your project?' + ], + user: { id: userId } as any, + }; +} + +export function userDataRestorationInputMapJsonToEntity( + data: Record, + userId: string, +): Partial { + return { + projectName: data['Project name'], + country: data['Country'], + cityOrRegion: data['City / province / region / state of project'], + projectStartYear: data['Project start year'], + mostRecentYearOfData: data['Most recent year of data collection'], + ecosystem: data['Ecosystem'], + projectActivity: data['Project activity']?.toString(), // Convertir a string si es numérico + projectSizeAreaStudied: data['Project size / area studied'], + categories: data['Categories'], + projectArea: data['Project area '], + abovegroundBiomassStock: data['Aboveground biomass stock'], + belowgroundBiomassStock: data['Belowground biomass stock '], + soilOrganicCarbonStock: data['Soil organic carbon stock '], + methaneEmissions: data['Methane emissions '], + nitrousOxideEmissions: data['Nitrous oxide emissions '], + abovegroundBiomassEmissionsFactor: + data['Aboveground biomass emissions factor'], + belowgroundBiomassEmissionsFactor: + data['Belowground biomass emissions factor'], + soilOrganicCarbonEmissionsFactor: + data['Soil organic carbon emissions factor '], + user: { id: userId } as any, + }; +} + +export function userDataConservationInputMapJsonToEntity( + data: Record, + userId: string, +): Partial { + return { + projectName: data['Project name'], + country: data['Country'], + cityOrRegion: data['City / province / region / state of project'], + projectStartYear: data['Project start year'], + mostRecentYearOfData: data['Most recent year of data collection'], + ecosystem: data['Ecosystem'], + projectActivity: data['Project activity']?.toString(), // Convertir a string si es numérico + projectSizeAreaStudied: data['Project size / area studied'], + categories: data['Categories'], + projectArea: data['Project area '], + abovegroundBiomassStock: data['Aboveground biomass stock'], + belowgroundBiomassStock: data['Belowground biomass stock '], + soilOrganicCarbonStock: data['Soil organic carbon stock '], + methaneEmissions: data['Methane emissions '], + nitrousOxideEmissions: data['Nitrous oxide emissions '], + abovegroundBiomassEmissionsFactor: + data['Aboveground biomass emissions factor'], + belowgroundBiomassEmissionsFactor: + data['Belowground biomass emissions factor'], + soilOrganicCarbonEmissionsFactor: + data['Soil organic carbon emissions factor '], + user: { id: userId } as any, + }; +} diff --git a/api/src/modules/import/services/xlsx.parser.ts b/api/src/modules/import/services/xlsx.parser.ts index d69ba019..caf2d698 100644 --- a/api/src/modules/import/services/xlsx.parser.ts +++ b/api/src/modules/import/services/xlsx.parser.ts @@ -21,4 +21,99 @@ export class XlsxParser implements ExcelParserInterface { return parsedData; } + + async parseUserExcels(data: Buffer[]) { + const carbonInputs: WorkBook = read(data[0]); + const costInputs: WorkBook = read(data[1]); + + const restorationSheet: WorkSheet = carbonInputs.Sheets['Restoration']; + const conservationSheet: WorkSheet = carbonInputs.Sheets['Conservation']; + const restoration = parseRestorationSheet(restorationSheet); + const costInputSheet = costInputs.Sheets['Input']; + const conservation = parseConservationSheet(conservationSheet); + + const costInput: Record = {}; + const keysToIgnore = [ + 'Input data into blue shade cells', + 'General information', + 'Project information', + ]; + + Object.keys(costInputSheet).forEach((cellKey) => { + if (!cellKey.startsWith('B')) return; // Ignore cells that are not in column B + + const questionCell = costInputSheet[cellKey]; + const question = questionCell?.v; + + if (question && !keysToIgnore.includes(question)) { + // Answer is in the column C or D of the same row + const rowIndex = cellKey.match(/\d+/)?.[0]; // extract row number from cell key + const answerCellKey = `C${rowIndex}`; + const answerCell = + costInputSheet[answerCellKey] || costInputSheet[`D${rowIndex}`]; + + const answer = answerCell?.v || 'No value provided'; + costInput[question] = answer; + } + }); + + return { + carbonInputs: { restoration, conservation }, + costInputs: costInput, + }; + } +} + +function parseRestorationSheet(sheet: WorkSheet): Record { + const result: Record = {}; + + const keysToIgnore = [ + 'Sub-national / project sequestration information', + 'General information', + ]; + + Object.keys(sheet).forEach((cellKey) => { + if (!cellKey.startsWith('B')) return; + + const questionCell = sheet[cellKey]; + const question = questionCell?.v; + + if (question && !keysToIgnore.includes(question)) { + const rowIndex = cellKey.match(/\d+/)?.[0]; + const answerCellKey = `C${rowIndex}`; + const answerCell = sheet[answerCellKey]; + + const answer = answerCell?.v || 'No value provided'; + result[question] = answer; + } + }); + + return result; +} + +function parseConservationSheet(sheet: WorkSheet): Record { + const result: Record = {}; + + const keysToIgnore = [ + 'Sub-national / project sequestration information', + 'General information', + ]; + + Object.keys(sheet).forEach((cellKey) => { + if (!cellKey.startsWith('B')) return; + + const questionCell = sheet[cellKey]; + const question = questionCell?.v; + + if (question && !keysToIgnore.includes(question)) { + const rowIndex = cellKey.match(/\d+/)?.[0]; + const answerCellKey = `C${rowIndex}`; + const answerCell = sheet[answerCellKey]; + + const answer = answerCell?.v || 'No value provided'; + result[question] = answer; + } + }); + + return result; } diff --git a/api/src/modules/users/users.module.ts b/api/src/modules/users/users.module.ts index 64a02288..9810333c 100644 --- a/api/src/modules/users/users.module.ts +++ b/api/src/modules/users/users.module.ts @@ -4,9 +4,20 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '@shared/entities/users/user.entity'; import { UsersController } from '@api/modules/users/users.controller'; import { AuthModule } from '@api/modules/auth/auth.module'; +import { UserUploadCostInputs } from '@shared/entities/users/user-upload-cost-inputs.entity'; +import { UserUploadRestorationInputs } from '@shared/entities/users/user-upload-restoration-inputs.entity'; +import { UserUploadConservationInputs } from '@shared/entities/users/user-upload-conservation-inputs.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User]), forwardRef(() => AuthModule)], + imports: [ + TypeOrmModule.forFeature([ + User, + UserUploadCostInputs, + UserUploadRestorationInputs, + UserUploadConservationInputs, + ]), + forwardRef(() => AuthModule), + ], providers: [UsersService], exports: [UsersService], controllers: [UsersController], diff --git a/client/src/public/forms/Carbon input template.xlsx b/client/src/public/forms/Carbon input template.xlsx new file mode 100644 index 00000000..1f8fb432 Binary files /dev/null and b/client/src/public/forms/Carbon input template.xlsx differ diff --git a/client/src/public/forms/Cost input template.xlsx b/client/src/public/forms/Cost input template.xlsx new file mode 100644 index 00000000..b47afea2 Binary files /dev/null and b/client/src/public/forms/Cost input template.xlsx differ diff --git a/shared/contracts/users.contract.ts b/shared/contracts/users.contract.ts index 58585e60..1de44175 100644 --- a/shared/contracts/users.contract.ts +++ b/shared/contracts/users.contract.ts @@ -3,7 +3,7 @@ import { generateEntityQuerySchema } from "@shared/schemas/query-param.schema"; import { User } from "@shared/entities/users/user.entity"; import { UserDto } from "@shared/dtos/users/user.dto"; import { z } from "zod"; -import { JSONAPIError } from '@shared/dtos/json-api.error'; +import { JSONAPIError } from "@shared/dtos/json-api.error"; import { ApiResponse } from "@shared/dtos/global/api-response.dto"; import { UpdateUserPasswordSchema } from "@shared/schemas/users/update-password.schema"; @@ -13,8 +13,8 @@ import { UpdateUserSchema } from "@shared/schemas/users/update-user.schema"; const contract = initContract(); export const usersContract = contract.router({ findMe: { - method: 'GET', - path: '/users/me', + method: "GET", + path: "/users/me", responses: { 200: contract.type>(), 401: contract.type(), @@ -58,4 +58,13 @@ export const usersContract = contract.router({ }, body: null, }, + + uploadData: { + method: "POST", + path: "/users/upload-data", + responses: { + 201: contract.type(), + }, + body: contract.type<{ thumbnail: File }>(), + }, }); diff --git a/shared/entities/users/user-upload-conservation-inputs.entity.ts b/shared/entities/users/user-upload-conservation-inputs.entity.ts new file mode 100644 index 00000000..429065bd --- /dev/null +++ b/shared/entities/users/user-upload-conservation-inputs.entity.ts @@ -0,0 +1,73 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + BaseEntity, + ManyToOne, + JoinColumn, +} from "typeorm"; +import { User } from "@shared/entities/users/user.entity"; + +@Entity("user_upload_conservation_inputs") +export class UserUploadConservationInputs extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => User, (user) => user.userUploadConservationInputs) + @JoinColumn({ name: "user_id" }) + user: User; + + @Column({ type: "varchar", length: 255, nullable: true }) + projectName: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + country: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + cityOrRegion: string; + + @Column({ type: "int", nullable: true }) + projectStartYear: number; + + @Column({ type: "int", nullable: true }) + mostRecentYearOfData: number; + + @Column({ type: "varchar", length: 255, nullable: true }) + ecosystem: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + projectActivity: string; + + @Column({ type: "int", nullable: true }) + projectSizeAreaStudied: number; + + @Column({ type: "varchar", length: 255, nullable: true }) + categories: string; + + @Column({ type: "int", nullable: true }) + projectArea: number; + + @Column({ type: "int", nullable: true }) + abovegroundBiomassStock: number; + + @Column({ type: "int", nullable: true }) + belowgroundBiomassStock: number; + + @Column({ type: "int", nullable: true }) + soilOrganicCarbonStock: number; + + @Column({ type: "int", nullable: true }) + methaneEmissions: number; + + @Column({ type: "int", nullable: true }) + nitrousOxideEmissions: number; + + @Column({ type: "int", nullable: true }) + abovegroundBiomassEmissionsFactor: number; + + @Column({ type: "int", nullable: true }) + belowgroundBiomassEmissionsFactor: number; + + @Column({ type: "int", nullable: true }) + soilOrganicCarbonEmissionsFactor: number; +} diff --git a/shared/entities/users/user-upload-cost-inputs.entity.ts b/shared/entities/users/user-upload-cost-inputs.entity.ts new file mode 100644 index 00000000..16e97e66 --- /dev/null +++ b/shared/entities/users/user-upload-cost-inputs.entity.ts @@ -0,0 +1,139 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + BaseEntity, +} from "typeorm"; +import { User } from "@shared/entities/users/user.entity"; + +@Entity("user_upload_cost_inputs") +export class UserUploadCostInputs extends BaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string; + + @ManyToOne(() => User, (user) => user.uploadedCostInputs) + @JoinColumn({ name: "user_id" }) + user: User; + + @Column({ type: "varchar", length: 255, nullable: true }) + programName: string; + + @Column({ type: "int", nullable: true }) + intendedLengthOfProject: number; + + @Column({ type: "varchar", length: 255, nullable: true }) + country: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + currency: string; + + @Column({ type: "int", nullable: true }) + projectStartYear: number; + + @Column({ type: "varchar", length: 255, nullable: true }) + projectActivity: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + ecosystem: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + projectSize: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + validationStandard: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + numberOfLocalIndividuals: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + cityOrRegion: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + intendedAlternativeUseOfLand: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + landOwnershipBeforeProject: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + sdgsBenefitted: string; + + @Column({ type: "boolean", nullable: true }) + projectEligibleForCarbonCredits: boolean; + + @Column({ type: "boolean", nullable: true }) + willingToSpeakAboutPricing: boolean; + + @Column({ type: "boolean", nullable: true }) + ableToProvideDetailedCostDocumentation: boolean; + + @Column({ type: "text", nullable: true }) + costCategories: string; + + @Column({ type: "int", nullable: true }) + establishingCommunityEngagement: number; + + @Column({ type: "int", nullable: true }) + conservationProjectPlanning: number; + + @Column({ type: "int", nullable: true }) + carbonProjectPlanning: number; + + @Column({ type: "int", nullable: true }) + landCost: number; + + @Column({ type: "int", nullable: true }) + financingCost: number; + + @Column({ type: "int", nullable: true }) + materialsSeedsFertilizer: number; + + @Column({ type: "int", nullable: true }) + materialsMachineryEquipment: number; + + @Column({ type: "int", nullable: true }) + projectLaborActivity: number; + + @Column({ type: "int", nullable: true }) + engineeringIntervention: number; + + @Column({ type: "int", nullable: true }) + ongoingCommunityEngagement: number; + + @Column({ type: "int", nullable: true }) + otherProjectRunningCost: number; + + @Column({ type: "int", nullable: true }) + projectMonitoring: number; + + @Column({ type: "int", nullable: true }) + otherCost1: number; + + @Column({ type: "int", nullable: true }) + otherCost2: number; + + @Column({ type: "int", nullable: true }) + otherCost3: number; + + @Column({ type: "text", nullable: true }) + projectCumulativeSequestration: string; + + @Column({ type: "text", nullable: true }) + detailedProjectActivity: string; + + @Column({ type: "text", nullable: true }) + communityEngagementSpending: string; + + @Column({ type: "text", nullable: true }) + landRightsAndEasements: string; + + @Column({ type: "text", nullable: true }) + hourlyWageRate: string; + + @Column({ type: "text", nullable: true }) + ongoingCommunityCompensation: string; + + @Column({ type: "text", nullable: true }) + engineeringDetails: string; +} diff --git a/shared/entities/users/user-upload-restoration-inputs.entity.ts b/shared/entities/users/user-upload-restoration-inputs.entity.ts new file mode 100644 index 00000000..be43501a --- /dev/null +++ b/shared/entities/users/user-upload-restoration-inputs.entity.ts @@ -0,0 +1,73 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + BaseEntity, + ManyToOne, + JoinColumn, +} from "typeorm"; +import { User } from "@shared/entities/users/user.entity"; + +@Entity("user_upload_restoration_inputs") +export class UserUploadRestorationInputs extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => User, (user) => user.userUploadRestorationInputs) + @JoinColumn({ name: "user_id" }) + user: User; + + @Column({ type: "varchar", length: 255, nullable: true }) + projectName: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + country: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + cityOrRegion: string; + + @Column({ type: "int", nullable: true }) + projectStartYear: number; + + @Column({ type: "int", nullable: true }) + mostRecentYearOfData: number; + + @Column({ type: "varchar", length: 255, nullable: true }) + ecosystem: string; + + @Column({ type: "varchar", length: 255, nullable: true }) + projectActivity: string; + + @Column({ type: "int", nullable: true }) + projectSizeAreaStudied: number; + + @Column({ type: "varchar", length: 255, nullable: true }) + categories: string; + + @Column({ type: "int", nullable: true }) + projectArea: number; + + @Column({ type: "int", nullable: true }) + abovegroundBiomassStock: number; + + @Column({ type: "int", nullable: true }) + belowgroundBiomassStock: number; + + @Column({ type: "int", nullable: true }) + soilOrganicCarbonStock: number; + + @Column({ type: "int", nullable: true }) + methaneEmissions: number; + + @Column({ type: "int", nullable: true }) + nitrousOxideEmissions: number; + + @Column({ type: "int", nullable: true }) + abovegroundBiomassEmissionsFactor: number; + + @Column({ type: "int", nullable: true }) + belowgroundBiomassEmissionsFactor: number; + + @Column({ type: "int", nullable: true }) + soilOrganicCarbonEmissionsFactor: number; +} diff --git a/shared/entities/users/user.entity.ts b/shared/entities/users/user.entity.ts index ec4ffcac..16727524 100644 --- a/shared/entities/users/user.entity.ts +++ b/shared/entities/users/user.entity.ts @@ -4,9 +4,13 @@ import { Entity, PrimaryGeneratedColumn, BaseEntity, + OneToMany, } from "typeorm"; import { Exclude } from "class-transformer"; import { ROLES } from "@shared/entities/users/roles.enum"; +import { UserUploadCostInputs } from "@shared/entities/users/user-upload-cost-inputs.entity"; +import { UserUploadRestorationInputs } from "@shared/entities/users/user-upload-restoration-inputs.entity"; +import { UserUploadConservationInputs } from "@shared/entities/users/user-upload-conservation-inputs.entity"; // TODO: For future reference: // https://github.com/typeorm/typeorm/issues/2897 @@ -42,4 +46,13 @@ export class User extends BaseEntity { @CreateDateColumn({ name: "created_at", type: "timestamp" }) createdAt: Date; + + @OneToMany("UserUploadCostInputs", "user") + uploadedCostInputs: UserUploadCostInputs[]; + + @OneToMany("UserUploadRestorationInputs", "user") + userUploadRestorationInputs: UserUploadRestorationInputs[]; + + @OneToMany("UserUploadConservationInputs", "user") + userUploadConservationInputs: UserUploadConservationInputs[]; } diff --git a/shared/lib/db-entities.ts b/shared/lib/db-entities.ts index a19a0af1..dbc802fc 100644 --- a/shared/lib/db-entities.ts +++ b/shared/lib/db-entities.ts @@ -30,6 +30,9 @@ import { BaseSize } from "@shared/entities/base-size.entity"; import { BaseIncrease } from "@shared/entities/base-increase.entity"; import { ModelAssumptions } from "@shared/entities/model-assumptions.entity"; import { CustomProject } from "@shared/entities/custom-project.entity"; +import { UserUploadCostInputs } from "@shared/entities/users/user-upload-cost-inputs.entity"; +import { UserUploadRestorationInputs } from "@shared/entities/users/user-upload-restoration-inputs.entity"; +import { UserUploadConservationInputs } from "@shared/entities/users/user-upload-conservation-inputs.entity"; export const COMMON_DATABASE_ENTITIES = [ User, @@ -64,4 +67,7 @@ export const COMMON_DATABASE_ENTITIES = [ BaseIncrease, ModelAssumptions, CustomProject, + UserUploadCostInputs, + UserUploadRestorationInputs, + UserUploadConservationInputs, ];