Skip to content

Commit

Permalink
upload user cost forms and list in backoffice
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Nov 20, 2024
1 parent 0ca9331 commit 534217d
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 100 deletions.
6 changes: 5 additions & 1 deletion admin/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +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/user-project-data.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,
Expand Down
25 changes: 23 additions & 2 deletions admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +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/user-project-data.entity.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,
Expand Down Expand Up @@ -70,6 +72,26 @@ const start = async () => {
},
},
},
{
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,
Expand Down Expand Up @@ -100,7 +122,6 @@ const start = async () => {
resource: Country,
name: "Country",
options: {
id: "Countries",
parent: databaseNavigation,
icon: "Globe",
},
Expand Down
3 changes: 0 additions & 3 deletions api/src/modules/import/import.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ 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';
import { Public } from '@api/modules/auth/decorators/is-public.decorator';

@Controller()
@UseGuards(JwtAuthGuard, RolesGuard)
Expand All @@ -45,7 +44,6 @@ export class ImportController {
});
}

//@Public()
@UseInterceptors(FilesInterceptor('files', 2))
@RequiredRoles(ROLES.PARTNER, ROLES.ADMIN)
@TsRestHandler(usersContract.uploadData)
Expand All @@ -54,7 +52,6 @@ export class ImportController {
@UploadedFiles() files: Array<Express.Multer.File>,
): Promise<any> {
return tsRestHandler(usersContract.uploadData, async () => {
console.log(files);
const [file1, file2] = files;
const [file1Buffer, file2Buffer] = [file1.buffer, file2.buffer];
const data = await this.service.importDataProvidedByPartner(
Expand Down
44 changes: 35 additions & 9 deletions api/src/modules/import/import.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ 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 {
userDataInputJson,
userDataMapJsonToEntity,
userDataConservationInputMapJsonToEntity,
userDataCostInputsMapJsonToEntity,
userDataRestorationInputMapJsonToEntity,
} from '@api/modules/import/services/user-data-parser';
import { UserUploadCostInputs } from '@shared/entities/user-project-data.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';

@Injectable()
export class ImportService {
Expand Down Expand Up @@ -53,11 +56,34 @@ export class ImportService {
}

async importDataProvidedByPartner(fileBuffers: Buffer[], userId: string) {
const { costInputs } = await this.excelParser.parseUserExcels(fileBuffers);
const mapped = userDataMapJsonToEntity(userDataInputJson, userId);
const savedData = await this.dataSource
.getRepository(UserUploadCostInputs)
.save(mapped);
return savedData;
// 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;
}
}
118 changes: 64 additions & 54 deletions api/src/modules/import/services/user-data-parser.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,10 @@
// I feel dirty doing this...

import { UserUploadCostInputs } from '@shared/entities/user-project-data.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 userDataInputJson = {
'Program name (if willing to share)': 'test name',
'Intended length of project': 2000,
Country: 'Basque Country',
Currency: 'Euro',
'Project start year': 1,
'Project activity': 'Conservation',
Ecosystem: 'Mangrove',
'Project size': 'something',
'Validation standard / accrediting organization': 'Verra',
'Number of local individuals who own, work, and/or live in the project site (e.g., land tenure)':
'land tenure',
'City / province / region / state of project': 'Azpeiti',
'Intended alternative use of land': 'commercial',
'Land ownership before project': 'private',
'SDGs benefitted / co-benefitted': 'sdg',
'Project eligible for voluntary carbon credits?': 'no',
'Are you willing to speak with us about your carbon credit pricing?': 'yes',
'Are you able to provide additional detailed cost documentation for the project?':
'yes',
'Cost categories ': 'Detailed activities...',
'Establishing community engagement / buy-in': 1,
'Conservation project planning & administration ': 2,
'Carbon project planning & administration': 3,
'Land cost': 4,
'Financing cost': 5,
'Materials (e.g., seeds, fertilizer, seedlings)': 6,
'Materials (e.g., machinery, equipment, etc.)': 7,
'Project labor / activity': 8,
'Engineering / construction intervention': 9,
'Ongoing community engagement': 10,
'Other project running cost': 11,
'Project monitoring': 12,
'1) Other cost (please specify activities)': 13,
'2) Other cost (please specify activities)': 14,
'3) Other cost (please specify activities)': 15,
'Project site cumulative sequestration / carbon stock': 'something',
'Please describe in detail the project activity (e.g., planted mangrove seedlings, set up perimeter around conservation area)':
'question 1',
'When you kicked off the project, how did you spend to engage the community...':
'question 2',
'How did you acquire the rights to establish the project on the land?...':
'question 3',
'What was the hourly wage rate paid for labor? How many hours worked for each activity?':
'question 4',
'Please describe the ongoing community engagement for your project.':
'question 5',
'Did you undertake any engineering / construction interventions for your project?':
'question 6',
};

// Función para transformar el JSON
export function userDataMapJsonToEntity(
export function userDataCostInputsMapJsonToEntity(
inputJson: Record<string, any>,
userId: string,
): Partial<UserUploadCostInputs> {
Expand Down Expand Up @@ -138,3 +88,63 @@ export function userDataMapJsonToEntity(
user: { id: userId } as any,
};
}

export function userDataRestorationInputMapJsonToEntity(
data: Record<string, any>,
userId: string,
): Partial<UserUploadRestorationInputs> {
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<string, any>,
userId: string,
): Partial<UserUploadConservationInputs> {
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,
};
}
93 changes: 68 additions & 25 deletions api/src/modules/import/services/xlsx.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,95 @@ export class XlsxParser implements ExcelParserInterface {
async parseUserExcels(data: Buffer[]) {
const carbonInputs: WorkBook = read(data[0]);
const costInputs: WorkBook = read(data[1]);
const CARBON_INPUTS_SHEETS = ['Restoration', 'Conservation'];
const COST_INPUTS_SHEETS = ['Input'];
const parsedCarbonInputs: any = {};
let parsedCostInputs: WorkSheet;
//
// for (const sheetName of CARBON_INPUTS_SHEETS) {
// const sheet: WorkSheet = carbonInputs.Sheets[sheetName];
// const parsedSheet = utils.sheet_to_json(sheet, {
// raw: true,
// });
// parsedCarbonInputs[sheetName] = parsedSheet;
// }

for (const sheetName of COST_INPUTS_SHEETS) {
parsedCostInputs = costInputs.Sheets[sheetName];
//= utils.sheet_to_json(sheet, { header: 4 });
//parsedCostInputs[sheetName] = parsedSheet;
}
const result: Record<string, string> = {};

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<string, string> = {};
const keysToIgnore = [
'Input data into blue shade cells',
'General information',
'Project information',
];

Object.keys(parsedCostInputs).forEach((cellKey) => {
Object.keys(costInputSheet).forEach((cellKey) => {
if (!cellKey.startsWith('B')) return; // Ignore cells that are not in column B

const questionCell = parsedCostInputs[cellKey];
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 =
parsedCostInputs[answerCellKey] || parsedCostInputs[`D${rowIndex}`];
costInputSheet[answerCellKey] || costInputSheet[`D${rowIndex}`];

const answer = answerCell?.v || 'No value provided';
result[question] = answer;
costInput[question] = answer;
}
});

return {
//carbonInputs: parsedCarbonInputs,
costInputs: result,
carbonInputs: { restoration, conservation },
costInputs: costInput,
};
}
}

function parseRestorationSheet(sheet: WorkSheet): Record<string, any> {
const result: Record<string, any> = {};

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<string, any> {
const result: Record<string, any> = {};

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;
}
Loading

0 comments on commit 534217d

Please sign in to comment.