diff --git a/admin/src/App.js b/admin/src/App.js index a586634..a5789cb 100644 --- a/admin/src/App.js +++ b/admin/src/App.js @@ -6,6 +6,7 @@ import { HttpClient } from "./configs/HttpClient"; import { BASE_URL } from "./configs/BaseUrl"; import { OpenGardenAdminLayout } from './themes/Layout'; +import Dashboard from "./Dashboard"; import { TbSeeding, TbTree, TbPlant2, TbStar } from 'react-icons/tb'; @@ -20,7 +21,9 @@ const App = () => ( requireAuth={true} layout={OpenGardenAdminLayout} authProvider={AuthProvider} - dataProvider={Provider(BASE_URL, HttpClient)}> + dataProvider={Provider(BASE_URL, HttpClient)} + dashboard={Dashboard} + disableTelemetry> {permissions => ( <> { + this.setState({ stats: JSON.parse(res.body), loading: false }) + }); + }; + + render() { + return ( + <> + + + + + } + title="Users" + subheader={this.state.loading ? : this.state?.stats?.estimatedCount?.users}> + + + + + } + title="Plants" + subheader={this.state.loading ? : this.state?.stats?.estimatedCount?.plants}> + + + + + } + title="Varieties" + subheader={this.state.loading ? : this.state?.stats?.estimatedCount?.varieties}> + + + + + } + title="Floors" + subheader={this.state.loading ? : this.state?.stats?.estimatedCount?.floors}> + + + + + + ); + } +} diff --git a/core/src/controllers/controllers.module.ts b/core/src/controllers/controllers.module.ts index 2ce3f0f..87c168b 100644 --- a/core/src/controllers/controllers.module.ts +++ b/core/src/controllers/controllers.module.ts @@ -22,6 +22,7 @@ import { FavoritesVarietiesController } from './favorites/favorites.varieties.co import { VarietyExistsRule } from './varieties/constraint/variety.exists.rule'; import { FavoriteVarietiesMapperProfiles } from './favorites/models/mapper.profiles'; import { RootController } from './root.controller'; +import { StatsController } from './stats/stats.controller'; @Module({ imports: [ @@ -53,6 +54,7 @@ import { RootController } from './root.controller'; VarietiesController, FloorsController, FavoritesVarietiesController, + StatsController, ], }) export class ControllersModule {} diff --git a/core/src/controllers/stats/models/stats.global.response.body.ts b/core/src/controllers/stats/models/stats.global.response.body.ts new file mode 100644 index 0000000..ef3741e --- /dev/null +++ b/core/src/controllers/stats/models/stats.global.response.body.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StatsGlobalCountResponseBody { + @ApiProperty() + users: number; + + @ApiProperty() + plants: number; + + @ApiProperty() + varieties: number; + + @ApiProperty() + floors: number; +} + +export class StatsGlobalResponseBody { + @ApiProperty({ type: StatsGlobalCountResponseBody }) + estimatedCount: StatsGlobalCountResponseBody; +} diff --git a/core/src/controllers/stats/stats.controller.ts b/core/src/controllers/stats/stats.controller.ts new file mode 100644 index 0000000..b21dc8f --- /dev/null +++ b/core/src/controllers/stats/stats.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiResponse } from '@nestjs/swagger'; +import { VarietiesService } from '../../entities/varieties/varieties.service'; +import { UsersService } from '../../users/users.service'; +import { PlantsService } from '../../entities/plants/plants.service'; +import { FloorsService } from '../../entities/floors/floors.service'; +import { StatsGlobalResponseBody } from './models/stats.global.response.body'; + +@ApiBearerAuth() +@ApiTags('Stats') +@ApiResponse({ status: 401, description: 'Unauthorized' }) +@ApiResponse({ status: 429, description: 'Too Many Requests' }) +@Controller('stats') +export class StatsController { + constructor( + private usersService: UsersService, + private plantsService: PlantsService, + private varietiesService: VarietiesService, + private floorsService: FloorsService, + ) {} + + @Get('global') + @ApiResponse({ status: 200, type: StatsGlobalResponseBody }) + async getGlobal(): Promise { + return { + estimatedCount: { + users: await this.usersService.count(), + plants: await this.plantsService.count(), + varieties: await this.varietiesService.count(), + floors: await this.floorsService.count(), + }, + }; + } +} diff --git a/core/src/entities/favorites/models/favorite.variety.entity.ts b/core/src/entities/favorites/models/favorite.variety.entity.ts index 7c0c2f4..2465e30 100644 --- a/core/src/entities/favorites/models/favorite.variety.entity.ts +++ b/core/src/entities/favorites/models/favorite.variety.entity.ts @@ -1,13 +1,16 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import mongoose, { Document } from 'mongoose'; -import { User } from '../../../users/models/user.entity'; +import { Variety, VarietySchema } from '../../../entities/varieties/models/variety.entity'; import { BaseEntity } from '../../base.entity'; export type FavoriteVarietyDocument = FavoriteVariety & Document; @Schema() export class FavoriteVariety extends BaseEntity { - @Prop({ required: true, type: [{ type: mongoose.Schema.Types.ObjectId, ref: User.name }] }) + @Prop({ + required: true, + type: [{ type: mongoose.Schema.Types.ObjectId, ref: Variety.name, childSchemas: VarietySchema }], + }) variety: mongoose.Types.ObjectId; @Prop({ required: true, unique: true, index: true }) diff --git a/core/src/entities/floors/floors.service.ts b/core/src/entities/floors/floors.service.ts index 8febcdc..8c11465 100644 --- a/core/src/entities/floors/floors.service.ts +++ b/core/src/entities/floors/floors.service.ts @@ -12,14 +12,14 @@ export class FloorSearchParams extends BaseSearchParams { @Injectable() export class FloorsService extends BaseEntityService { - constructor(@InjectModel(Floor.name) private FloorModel: Model) { + constructor(@InjectModel(Floor.name) private floorModel: Model) { super(); } async create(Floor: Floor): Promise { try { Floor._id = new mongoose.Types.ObjectId(); - return await this.FloorModel.create(Floor); + return await this.floorModel.create(Floor); } catch (exception) { if (exception.code === 11000) { throw new ConflictException(); @@ -30,22 +30,26 @@ export class FloorsService extends BaseEntityService { async deleteFloor(floorId: string): Promise { try { - return await this.FloorModel.findByIdAndDelete(floorId); + return await this.floorModel.findByIdAndDelete(floorId); } catch { return null; } } async findOneById(id: string): Promise { - return await this.FloorModel.findById(id); + return await this.floorModel.findById(id); } async search(params: FloorSearchParams): Promise<[Floor[], number]> { const { pagination, ...filters } = params; const findParam = this._generateFilters(filters); - const elements = await this.FloorModel.find(findParam).skip(pagination.offset).limit(pagination.limit); - const count = await this.FloorModel.count(findParam); + const elements = await this.floorModel.find(findParam).skip(pagination.offset).limit(pagination.limit); + const count = await this.floorModel.count(findParam); return [elements, count]; } + + async count(): Promise { + return await this.floorModel.estimatedDocumentCount(); + } } diff --git a/core/src/entities/plants/plants.service.ts b/core/src/entities/plants/plants.service.ts index ff00d9e..074432c 100644 --- a/core/src/entities/plants/plants.service.ts +++ b/core/src/entities/plants/plants.service.ts @@ -18,14 +18,14 @@ export class PlantSearchParams extends BaseSearchParams { @Injectable() export class PlantsService extends BaseEntityService { - constructor(@InjectModel(Plant.name) private PlantModel: Model) { + constructor(@InjectModel(Plant.name) private plantModel: Model) { super(); } async create(Plant: Plant): Promise { try { Plant._id = new mongoose.Types.ObjectId(); - return await this.PlantModel.create(Plant); + return await this.plantModel.create(Plant); } catch (exception) { if (exception.code === 11000) { throw new ConflictException(); @@ -36,14 +36,14 @@ export class PlantsService extends BaseEntityService { async deletePlant(plantId: string): Promise { try { - return await this.PlantModel.findByIdAndDelete(plantId); + return await this.plantModel.findByIdAndDelete(plantId); } catch { return null; } } async findOneById(id: string): Promise { - return await this.PlantModel.findById(id); + return await this.plantModel.findById(id); } async search(params: PlantSearchParams): Promise<[Plant[], number]> { @@ -56,8 +56,12 @@ export class PlantsService extends BaseEntityService { findParam = this._generateFilter(findParam, 'classification.species', species); findParam = this._generateFilter(findParam, 'classification.binomialName', binomialName); - const elements = await this.PlantModel.find(findParam).skip(pagination.offset).limit(pagination.limit); - const count = await this.PlantModel.count(findParam); + const elements = await this.plantModel.find(findParam).skip(pagination.offset).limit(pagination.limit); + const count = await this.plantModel.count(findParam); return [elements, count]; } + + async count(): Promise { + return await this.plantModel.estimatedDocumentCount(); + } } diff --git a/core/src/entities/varieties/models/requirement.entity.ts b/core/src/entities/varieties/models/requirement.entity.ts index 6626ee7..9cb07bb 100644 --- a/core/src/entities/varieties/models/requirement.entity.ts +++ b/core/src/entities/varieties/models/requirement.entity.ts @@ -1,6 +1,6 @@ import { Prop } from '@nestjs/mongoose'; import mongoose from 'mongoose'; -import { Floor } from '../../floors/models/floor.entity'; +import { Floor, FloorSchema } from '../../floors/models/floor.entity'; export enum VarietyRequirementWaterNeed { LOW = 'LOW', @@ -37,6 +37,6 @@ export class VarietyRequirement { @Prop({ required: true }) sun: VarietyRequirementSun; - @Prop({ required: true, type: [{ type: mongoose.Schema.Types.ObjectId, ref: Floor.name }] }) + @Prop({ required: true, type: [{ type: Array, ref: Floor.name, childSchemas: FloorSchema }] }) floors: mongoose.Types.ObjectId[]; } diff --git a/core/src/entities/varieties/models/variety.entity.ts b/core/src/entities/varieties/models/variety.entity.ts index 32c0e25..2cbc655 100644 --- a/core/src/entities/varieties/models/variety.entity.ts +++ b/core/src/entities/varieties/models/variety.entity.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import mongoose, { Document } from 'mongoose'; -import { Plant } from '../../plants/models/plant.entity'; +import { Plant, PlantSchema } from '../../plants/models/plant.entity'; import { BasePublishedEntity } from '../../base.published.entity'; import { VarietyRequirement } from './requirement.entity'; import { VarietyCulture } from './culture.entity'; @@ -15,7 +15,10 @@ export enum VarietyPrecocity { @Schema() export class Variety extends BasePublishedEntity { - @Prop({ required: true, type: [{ type: mongoose.Schema.Types.ObjectId, ref: Plant.name }] }) + @Prop({ + required: true, + type: [{ type: mongoose.Schema.Types.ObjectId, ref: Plant.name, childSchemas: PlantSchema }], + }) plant: mongoose.Types.ObjectId; @Prop({ required: true, unique: true }) diff --git a/core/src/entities/varieties/varieties.service.ts b/core/src/entities/varieties/varieties.service.ts index 5061c08..2a76851 100644 --- a/core/src/entities/varieties/varieties.service.ts +++ b/core/src/entities/varieties/varieties.service.ts @@ -103,4 +103,8 @@ export class VarietiesService extends BaseEntityService { const count = await this.varietyModel.count(findParam); return [elements, count]; } + + async count(): Promise { + return await this.varietyModel.estimatedDocumentCount(); + } } diff --git a/core/src/users/users.service.ts b/core/src/users/users.service.ts index dd539b3..70b2fbf 100644 --- a/core/src/users/users.service.ts +++ b/core/src/users/users.service.ts @@ -27,4 +27,8 @@ export class UsersService { async findOneById(id: string): Promise { return await this.userModel.findOne({ id }); } + + async count(): Promise { + return await this.userModel.estimatedDocumentCount(); + } }