Compare commits
25 Commits
924b538559
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dd8740171 | |||
| 4e6774a15b | |||
| e4f6afbc93 | |||
| 61529f7d94 | |||
| 9543d136aa | |||
| 456203f5cf | |||
| 82b1d4825b | |||
| 4ec9031005 | |||
| 7a5521648c | |||
| ca98adc338 | |||
| 405ba2e95d | |||
| 7452e05a92 | |||
| bd12fe02ae | |||
| a00c2942f5 | |||
| 768f0a5daf | |||
| c2fd263f27 | |||
| ac1980566a | |||
| db0ae34948 | |||
| 81184a8acc | |||
| a830cc079d | |||
| a986ddbb95 | |||
| 9232da69d3 | |||
| aa8f0b8f31 | |||
| 1b87dee60d | |||
| b54bbe2db2 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -54,3 +54,11 @@ pids
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
env
|
||||
.env
|
||||
**/.prisma-cache
|
||||
|
||||
venv/
|
||||
.vscode/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
@@ -20,6 +20,9 @@ ENV NODE_ENV=production
|
||||
|
||||
COPY --from=builder /usr/src/app/dist ./dist
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/app/package.json ./package.json
|
||||
|
||||
RUN npm prune --production
|
||||
|
||||
USER node
|
||||
|
||||
|
||||
1478
ServicesApi/package-lock.json
generated
1478
ServicesApi/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"ioredis": "^5.6.1",
|
||||
"mongodb": "^6.18.0",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"redis": "^5.6.1",
|
||||
@@ -46,7 +47,7 @@
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
|
||||
@@ -403,6 +403,7 @@ model account_records {
|
||||
accounting_receipt_number Int @default(0)
|
||||
status_id Int @default(0) @db.SmallInt
|
||||
approved_record Boolean @default(false)
|
||||
is_predicted Boolean @default(false)
|
||||
import_file_name String? @db.VarChar
|
||||
receive_debit Int?
|
||||
receive_debit_uu_id String? @db.VarChar
|
||||
@@ -1932,6 +1933,20 @@ model build_ibans {
|
||||
@@index([updated_at], map: "ix_build_ibans_updated_at")
|
||||
}
|
||||
|
||||
model user_types {
|
||||
id Int @id @default(autoincrement())
|
||||
uu_id String @unique(map: "ix_user_types_uu_id") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
type String @db.VarChar
|
||||
description String @default("") @db.VarChar
|
||||
type_token String @default("") @db.VarChar
|
||||
token String @default("") @db.VarChar
|
||||
occupant_types occupant_types[]
|
||||
staff staff[]
|
||||
|
||||
@@index([type], map: "ix_user_types_type")
|
||||
@@index([token], map: "ix_user_types_token")
|
||||
}
|
||||
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model build_living_space {
|
||||
fix_value Decimal @default(0) @db.Decimal(20, 6)
|
||||
@@ -3080,7 +3095,9 @@ model occupant_types {
|
||||
occupant_code String @default("") @db.VarChar
|
||||
occupant_category String @default("") @db.VarChar
|
||||
occupant_category_type String @default("") @db.VarChar
|
||||
function_retriever String @default("") @db.VarChar
|
||||
// function_retriever String @default("") @db.VarChar
|
||||
user_type_id Int?
|
||||
user_type_uu_id String? @db.VarChar
|
||||
occupant_is_unique Boolean @default(false)
|
||||
ref_id String? @db.VarChar(100)
|
||||
replication_id Int @default(0) @db.SmallInt
|
||||
@@ -3103,6 +3120,7 @@ model occupant_types {
|
||||
build_decision_book_person_occupants build_decision_book_person_occupants[]
|
||||
build_living_space build_living_space[]
|
||||
build_management build_management[]
|
||||
user_types user_types? @relation(fields: [user_type_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@index([created_at], map: "ix_occupant_types_created_at")
|
||||
@@index([cryp_uu_id], map: "ix_occupant_types_cryp_uu_id")
|
||||
@@ -3475,7 +3493,9 @@ model staff {
|
||||
staff_code String @db.VarChar
|
||||
duties_id Int
|
||||
duties_uu_id String @db.VarChar
|
||||
function_retriever String @default("") @db.VarChar
|
||||
// function_retriever String @default("") @db.VarChar
|
||||
user_type_id Int?
|
||||
user_type_uu_id String? @db.VarChar
|
||||
ref_id String? @db.VarChar(100)
|
||||
replication_id Int @default(0) @db.SmallInt
|
||||
cryp_uu_id String? @db.VarChar
|
||||
@@ -3497,6 +3517,7 @@ model staff {
|
||||
employee_history employee_history[]
|
||||
employees employees[]
|
||||
duties duties @relation(fields: [duties_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
user_types user_types? @relation(fields: [user_type_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
|
||||
@@index([created_at], map: "ix_staff_created_at")
|
||||
@@index([cryp_uu_id], map: "ix_staff_cryp_uu_id")
|
||||
|
||||
@@ -8,35 +8,34 @@ import {
|
||||
Body,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
Req,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { AuthControlGuard, EndpointControlGuard } from '../middleware/access-control.guard';
|
||||
import { AuthControlGuard, EndpointControlGuard } from '@/src/middleware/access-control.guard';
|
||||
import { Navigator } from '@/src/utils/navigator/navigator';
|
||||
|
||||
@Controller('accounts')
|
||||
export class AccountsController {
|
||||
constructor(private accountsService: AccountsService) {}
|
||||
|
||||
constructor(
|
||||
private accountsService: AccountsService,
|
||||
private navigator: Navigator
|
||||
) { }
|
||||
|
||||
@Get('events')
|
||||
@HttpCode(200)
|
||||
@UseGuards(AuthControlGuard)
|
||||
async getEvents(@Query() query: any) {
|
||||
const { userToken } = query;
|
||||
if (!userToken) { throw new NotFoundException('User token is missing or null') }
|
||||
const events = await this.navigator.getInfos(this.accountsService, userToken)
|
||||
return { events, message: "Events fetched successfully" };
|
||||
}
|
||||
|
||||
@Post('filter')
|
||||
@HttpCode(200)
|
||||
@UseGuards(AuthControlGuard, EndpointControlGuard)
|
||||
async filterAccounts(@Body() query: any) {
|
||||
const result = await this.accountsService.findWithPagination(query);
|
||||
const { pagination, data } = result;
|
||||
|
||||
if (data.length === 0) {
|
||||
return { pagination, data: [] };
|
||||
}
|
||||
|
||||
const resultRefined = data.map((rec: any) => ({
|
||||
...rec,
|
||||
build_decision_book_payments: rec.build_decision_book_payments?.map(
|
||||
(pmt: any) => ({
|
||||
...pmt,
|
||||
ratePercent:
|
||||
((pmt.payment_amount / rec.currency_value) * 100).toFixed(2) + '%',
|
||||
}),
|
||||
),
|
||||
}));
|
||||
return { pagination, data: resultRefined };
|
||||
}
|
||||
async filterAccounts(@Body() query: any, @Req() req: any) { return await this.navigator.getFunction(req, this.accountsService.mapper, query) }
|
||||
}
|
||||
|
||||
@@ -2,21 +2,31 @@ import { Module } from '@nestjs/common';
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { AccountsController } from './accounts.controller';
|
||||
import { PrismaModule } from '@/prisma/prisma.module';
|
||||
import { CacheService } from '../cache.service';
|
||||
import { CacheService } from '../database/redis/redis.service';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { RedisModule } from '../database/redis/redis.module';
|
||||
import {
|
||||
AuthControlGuard,
|
||||
EndpointControlGuard,
|
||||
} from '@/src/middleware/access-control.guard';
|
||||
import { SuperUsersService } from './superusers/superusers.service';
|
||||
import { UrlHandler } from '../utils/navigator/urlHandler';
|
||||
import { Navigator } from '@/src/utils/navigator/navigator';
|
||||
import { NavigatorModule } from '../navigator/navigator.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UtilsModule],
|
||||
imports: [PrismaModule, UtilsModule, RedisModule, NavigatorModule],
|
||||
providers: [
|
||||
AccountsService,
|
||||
CacheService,
|
||||
AuthControlGuard,
|
||||
EndpointControlGuard,
|
||||
SuperUsersService,
|
||||
UrlHandler,
|
||||
Navigator,
|
||||
],
|
||||
controllers: [AccountsController],
|
||||
})
|
||||
export class AccountsModule {}
|
||||
export class AccountsModule {
|
||||
constructor() { }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { Prisma, account_records } from '@prisma/client';
|
||||
import { CacheService } from '../cache.service';
|
||||
import { PaginationHelper, PaginationInfo } from '../utils/pagination-helper';
|
||||
import { SuperUsersService } from './superusers/superusers.service';
|
||||
import { EventsService } from '../navigator/events/events.service';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsService {
|
||||
mapper: any
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private cacheService: CacheService,
|
||||
private paginationHelper: PaginationHelper,
|
||||
) {}
|
||||
|
||||
async findAll(filter: any): Promise<Partial<account_records>[]> {
|
||||
return this.prisma.account_records.findMany({
|
||||
where: { ...filter },
|
||||
});
|
||||
private superUsersService: SuperUsersService,
|
||||
private eventService: EventsService,
|
||||
) {
|
||||
this.mapper = {
|
||||
"j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA": this.superUsersService,
|
||||
}
|
||||
}
|
||||
|
||||
async findDynamic(
|
||||
query: Prisma.account_recordsFindManyArgs,
|
||||
): Promise<{ totalCount: number; result: Partial<account_records>[] }> {
|
||||
const totalCount = await this.prisma.account_records.count({
|
||||
where: query.where,
|
||||
});
|
||||
const result = await this.prisma.account_records.findMany(query);
|
||||
return { totalCount, result };
|
||||
}
|
||||
async onModuleInit() {
|
||||
Object.entries(this.mapper).map(async ([key, value]) => {
|
||||
const service = value as any
|
||||
await this.eventService.setEvents(service.events, "AccountsService")
|
||||
})
|
||||
// const accountPages = await fetch(
|
||||
// "http://localhost:3000/pages",
|
||||
// {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// token: 'j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA',
|
||||
// pages: {
|
||||
|
||||
async findWithPagination(
|
||||
query: any & { page?: number; pageSize?: number },
|
||||
): Promise<{ data: any[]; pagination: PaginationInfo }> {
|
||||
return this.paginationHelper.paginate(this.prisma.account_records, query);
|
||||
}
|
||||
|
||||
async findOne(uuid: string): Promise<Partial<account_records> | null> {
|
||||
return this.prisma.account_records.findUnique({
|
||||
where: { uu_id: uuid },
|
||||
});
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SuperusersService } from './superusers.service';
|
||||
|
||||
describe('SuperusersService', () => {
|
||||
let service: SuperusersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [SuperusersService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SuperusersService>(SuperusersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
54
ServicesApi/src/accounts/superusers/superusers.service.ts
Normal file
54
ServicesApi/src/accounts/superusers/superusers.service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { PaginationHelper } from '@/src/utils/pagination-helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PaginationInfo } from '@/src/utils/pagination-helper';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
|
||||
|
||||
@Injectable()
|
||||
export class SuperUsersService {
|
||||
userToken: string = "j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA"
|
||||
constructor(
|
||||
private paginationHelper: PaginationHelper,
|
||||
private prisma: PrismaService,
|
||||
private urlHandler: UrlHandler,
|
||||
) { }
|
||||
|
||||
events = {
|
||||
"e6hewIe7YqbQZHO3:j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA": [
|
||||
{
|
||||
"key": "qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx",
|
||||
"endpoint": "/accounts/filter:POST",
|
||||
"eToken": "e6hewIe7YqbQZHO3",
|
||||
"token": "j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA",
|
||||
"description": "Super Users Account Filter",
|
||||
"isDefault": true,
|
||||
"query": { "query": true, "page": false, "pageSize": false },
|
||||
"pages": []
|
||||
}
|
||||
]
|
||||
};
|
||||
mapper = {
|
||||
"e6hewIe7YqbQZHO3:j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA:qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx": (query: any) => this.filter(query),
|
||||
}
|
||||
|
||||
async getEvents() { return this.urlHandler.getEvents(this.events, this.mapper) }
|
||||
|
||||
async infoEvents(userToken: string) { return Object.entries(this.events).filter(([key]) => key.endsWith(userToken)) }
|
||||
|
||||
async filter(query: any & { page?: number; pageSize?: number }): Promise<{ pagination: PaginationInfo; data: any[] }> {
|
||||
const result = await this.paginationHelper.findWithPagination(query, this.prisma.account_records);
|
||||
const { pagination, data } = result;
|
||||
|
||||
if (data.length === 0) { return { pagination, data: [] } }
|
||||
const resultRefined = data.map((rec: any) => ({
|
||||
...rec,
|
||||
build_decision_book_payments: rec.build_decision_book_payments?.map(
|
||||
(pmt: any) => ({
|
||||
...pmt,
|
||||
ratePercent: ((pmt.payment_amount / rec.currency_value) * 100).toFixed(2) + '%',
|
||||
}),
|
||||
),
|
||||
}));
|
||||
return { pagination, data: resultRefined };
|
||||
}
|
||||
}
|
||||
@@ -7,34 +7,26 @@ import {
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { PrismaModule } from '@/prisma/prisma.module';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { AccountsModule } from './accounts/accounts.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
import { CacheService } from './cache.service';
|
||||
import { RedisModule } from './database/redis/redis.module';
|
||||
import { LoggerMiddleware } from '@/src/middleware/logger.middleware';
|
||||
import { DiscoveryModule } from '@nestjs/core';
|
||||
|
||||
const redisConfig = {
|
||||
host: '10.10.2.15',
|
||||
port: 6379,
|
||||
password: 'your_strong_password_here',
|
||||
};
|
||||
import { NavigatorModule } from './navigator/navigator.module';
|
||||
|
||||
const modulesList = [UsersModule, AccountsModule, AuthModule];
|
||||
const serviceModuleList = [
|
||||
PrismaModule,
|
||||
RedisModule.forRoot({
|
||||
config: redisConfig,
|
||||
}),
|
||||
RedisModule.forRootWithConfig(true),
|
||||
DiscoveryModule,
|
||||
];
|
||||
const controllersList = [AppController];
|
||||
const providersList = [AppService, CacheService];
|
||||
const exportsList = [CacheService];
|
||||
const providersList = [AppService];
|
||||
const exportsList = [];
|
||||
|
||||
@Module({
|
||||
imports: [...serviceModuleList, ...modulesList],
|
||||
imports: [...serviceModuleList, ...modulesList, NavigatorModule],
|
||||
controllers: controllersList,
|
||||
providers: providersList,
|
||||
exports: exportsList,
|
||||
|
||||
@@ -11,9 +11,12 @@ import { ResetPasswordService } from './password/reset/reset.service';
|
||||
import { ChangePasswordService } from './password/change/change.service';
|
||||
import { VerifyOtpService } from './password/verify-otp/verify-otp.service';
|
||||
import { DisconnectService } from './disconnect/disconnect.service';
|
||||
import { MongoModule } from '@/src/database/mongo/mongo.module';
|
||||
import { MongoService } from '@/src/database/mongo/mongo.service';
|
||||
import { NavigatorModule } from '../navigator/navigator.module';
|
||||
|
||||
@Module({
|
||||
imports: [UtilsModule],
|
||||
imports: [UtilsModule, MongoModule, NavigatorModule],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
@@ -25,8 +28,9 @@ import { DisconnectService } from './disconnect/disconnect.service';
|
||||
ResetPasswordService,
|
||||
VerifyOtpService,
|
||||
DisconnectService,
|
||||
MongoService,
|
||||
PrismaService,
|
||||
],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule { }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { userLoginValidator } from '@/src/auth/login/dtoValidator';
|
||||
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
|
||||
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
|
||||
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
|
||||
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { AuthTokenSchema } from '@/src/types/auth/token';
|
||||
|
||||
@@ -11,7 +11,7 @@ export class LoginService {
|
||||
private readonly redis: RedisHandlers,
|
||||
private readonly passHandlers: PasswordHandlers,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async run(dto: userLoginValidator) {
|
||||
const foundUser = await this.prisma.users.findFirstOrThrow({
|
||||
@@ -58,7 +58,7 @@ export class LoginService {
|
||||
uu_id: true,
|
||||
occupant_code: true,
|
||||
occupant_type: true,
|
||||
function_retriever: true,
|
||||
// function_retriever: true,
|
||||
},
|
||||
},
|
||||
build_parts: {
|
||||
@@ -106,7 +106,7 @@ export class LoginService {
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff_code: true,
|
||||
function_retriever: true,
|
||||
// function_retriever: true,
|
||||
duties: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
@@ -137,14 +137,16 @@ export class LoginService {
|
||||
});
|
||||
selectList = employees;
|
||||
}
|
||||
|
||||
let fullName = `${foundPerson.firstname}`;
|
||||
if (foundPerson.middle_name) fullName += ` ${foundPerson.middle_name}`;
|
||||
if (foundPerson.birthname) fullName += ` ${foundPerson.birthname}`;
|
||||
fullName += ` ${foundPerson.surname}`;
|
||||
const redisData = AuthTokenSchema.parse({
|
||||
people: foundPerson,
|
||||
users: foundUser,
|
||||
credentials: {
|
||||
person_uu_id: foundPerson.uu_id,
|
||||
person_name: foundPerson.firstname,
|
||||
person_full_name: `${foundPerson.firstname} ${foundPerson.middle_name || ''} | ${foundPerson.birthname || ''} | ${foundPerson.surname}`,
|
||||
uuid: foundPerson.uu_id,
|
||||
fullName: fullName,
|
||||
},
|
||||
selectionList: {
|
||||
type: foundUser.user_type,
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { userChangePasswordValidator } from './dtoValidator';
|
||||
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
|
||||
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
|
||||
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
|
||||
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
|
||||
|
||||
@Injectable()
|
||||
export class ChangePasswordService {
|
||||
@@ -14,7 +14,7 @@ export class ChangePasswordService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly redis: RedisHandlers,
|
||||
private readonly passHandlers: PasswordHandlers,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
private async syncPasswordHistory(
|
||||
foundUser: any,
|
||||
@@ -84,18 +84,18 @@ export class ChangePasswordService {
|
||||
password: hashPassword,
|
||||
...(oldestIndex === 'first'
|
||||
? {
|
||||
old_password_first: dto.password,
|
||||
old_password_first_modified_at: new Date(),
|
||||
}
|
||||
old_password_first: dto.password,
|
||||
old_password_first_modified_at: new Date(),
|
||||
}
|
||||
: oldestIndex === 'second'
|
||||
? {
|
||||
old_password_second: dto.password,
|
||||
old_password_second_modified_at: new Date(),
|
||||
}
|
||||
old_password_second: dto.password,
|
||||
old_password_second_modified_at: new Date(),
|
||||
}
|
||||
: {
|
||||
old_password_third: dto.password,
|
||||
old_password_third_modified_at: new Date(),
|
||||
}),
|
||||
old_password_third: dto.password,
|
||||
old_password_third_modified_at: new Date(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { userCreatePasswordValidator } from './dtoValidator';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
|
||||
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
@@ -8,7 +8,7 @@ export class CreatePasswordService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly passHandlers: PasswordHandlers,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async run(dto: userCreatePasswordValidator) {
|
||||
if (dto.password !== dto.rePassword) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { userResetPasswordValidator } from './dtoValidator';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { PasswordHandlers } from '@/src/utils/auth/login_handler';
|
||||
import { PasswordHandlers } from '@/src/utils/store/loginHandler';
|
||||
|
||||
@Injectable()
|
||||
export class ResetPasswordService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly passHandlers: PasswordHandlers,
|
||||
) {}
|
||||
) { }
|
||||
async run(dto: userResetPasswordValidator) {
|
||||
const foundUser = await this.prisma.users.findFirstOrThrow({
|
||||
where: {
|
||||
|
||||
@@ -1,83 +1,89 @@
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
NotAcceptableException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable, UnauthorizedException, NotAcceptableException } from '@nestjs/common';
|
||||
import { userSelectValidator } from '@/src/auth/select/dtoValidator';
|
||||
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
|
||||
import {
|
||||
EmployeeTokenSchema,
|
||||
OccupantTokenSchema,
|
||||
TokenDictInterface,
|
||||
UserType,
|
||||
} from '@/src/types/auth/token';
|
||||
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
|
||||
import { EmployeeTokenSchema, OccupantTokenSchema, UserType } from '@/src/types/auth/token';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { EventsService } from '@/src/navigator/events/events.service';
|
||||
import { PagesService } from '@/src/navigator/pages/pages.service';
|
||||
import { MenusService } from '@/src/navigator/menus/menu.service';
|
||||
import { includes } from 'zod';
|
||||
|
||||
@Injectable()
|
||||
export class SelectService {
|
||||
constructor(
|
||||
private readonly redis: RedisHandlers,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
private readonly pagesService: PagesService,
|
||||
private readonly eventService: EventsService,
|
||||
private readonly menusService: MenusService,
|
||||
) { }
|
||||
|
||||
async run(dto: userSelectValidator, req: Request) {
|
||||
const accessObject = await this.redis.getLoginFromRedis(req);
|
||||
if (!accessObject) {
|
||||
throw new UnauthorizedException(
|
||||
'Authorization failed. Please login to continue',
|
||||
);
|
||||
}
|
||||
if (!accessObject) { throw new UnauthorizedException('Authorization failed. Please login to continue') }
|
||||
const accessToken = accessObject.key.split(':')[1];
|
||||
const existingSelectToken = await this.redis.callExistingSelectToken(
|
||||
accessObject.value.users.uu_id,
|
||||
dto.uuid,
|
||||
);
|
||||
if (existingSelectToken) {
|
||||
return {
|
||||
message: 'Select successful',
|
||||
token: existingSelectToken,
|
||||
};
|
||||
}
|
||||
const existingSelectToken = await this.redis.callExistingSelectToken(accessObject.value.users.uu_id, dto.uuid);
|
||||
if (existingSelectToken) { return { message: 'Select successful', token: existingSelectToken } }
|
||||
const userType = accessObject.value.users.user_type;
|
||||
if (userType === 'employee') {
|
||||
const employee = await this.prisma.employees.findFirstOrThrow({
|
||||
where: { uu_id: dto.uuid },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const employee = await this.prisma.employees.findFirstOrThrow({ where: { uu_id: dto.uuid }, omit: { id: true } });
|
||||
const staff = await this.prisma.staff.findFirstOrThrow({
|
||||
where: { id: employee.staff_id },
|
||||
omit: {
|
||||
id: true,
|
||||
where: { uu_id: employee.staff_uu_id },
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff_code: true,
|
||||
user_type_id: true,
|
||||
duties_id: true,
|
||||
staff_name: true,
|
||||
staff_description: true,
|
||||
duties_uu_id: true,
|
||||
created_credentials_token: true,
|
||||
updated_credentials_token: true,
|
||||
confirmed_credentials_token: true,
|
||||
is_confirmed: true,
|
||||
deleted: true,
|
||||
active: true,
|
||||
is_notification_send: true,
|
||||
is_email_send: true,
|
||||
expiry_starts: true,
|
||||
expiry_ends: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
ref_int: true,
|
||||
user_types: {
|
||||
select: {
|
||||
token: true,
|
||||
type_token: true
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const duties = await this.prisma.duties.findFirstOrThrow({
|
||||
where: { id: staff.duties_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
omit: { id: true },
|
||||
});
|
||||
const department = await this.prisma.departments.findFirstOrThrow({
|
||||
where: { id: duties.department_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
omit: { id: true },
|
||||
});
|
||||
const duty = await this.prisma.duty.findFirstOrThrow({
|
||||
where: { id: duties.duties_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
omit: { id: true },
|
||||
});
|
||||
const company = await this.prisma.companies.findFirstOrThrow({
|
||||
where: { id: duties.company_id },
|
||||
omit: {
|
||||
id: true,
|
||||
},
|
||||
omit: { id: true },
|
||||
});
|
||||
|
||||
const staffUserType = staff.user_type_id ?
|
||||
await this.prisma.user_types.findFirst({
|
||||
where: { id: staff.user_type_id },
|
||||
select: {
|
||||
token: true,
|
||||
type_token: true
|
||||
}
|
||||
}) : null;
|
||||
const employeeToken = EmployeeTokenSchema.parse({
|
||||
uuid: dto.uuid,
|
||||
company: company,
|
||||
department: department,
|
||||
duty: duty,
|
||||
@@ -85,6 +91,7 @@ export class SelectService {
|
||||
staff: staff,
|
||||
menu: null,
|
||||
pages: null,
|
||||
events: null,
|
||||
selection: await this.prisma.employees.findFirstOrThrow({
|
||||
where: { uu_id: dto.uuid },
|
||||
select: {
|
||||
@@ -93,7 +100,12 @@ export class SelectService {
|
||||
select: {
|
||||
uu_id: true,
|
||||
staff_code: true,
|
||||
function_retriever: true,
|
||||
user_types: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
duties: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
@@ -122,9 +134,14 @@ export class SelectService {
|
||||
},
|
||||
},
|
||||
}),
|
||||
functionsRetriever: staff.function_retriever,
|
||||
typeToken: staffUserType?.type_token,
|
||||
functionsRetriever: staffUserType?.token,
|
||||
kind: UserType.employee,
|
||||
});
|
||||
// Render page and menu
|
||||
// const collection = this.mongoService.getDb(`Events/${company.uu_id}`)
|
||||
const events = ""
|
||||
|
||||
|
||||
const tokenSelect = await this.redis.setSelectToRedis(
|
||||
accessToken,
|
||||
@@ -132,63 +149,79 @@ export class SelectService {
|
||||
accessObject.value.users.uu_id,
|
||||
dto.uuid,
|
||||
);
|
||||
|
||||
return {
|
||||
message: 'Select successful',
|
||||
token: tokenSelect,
|
||||
};
|
||||
return { message: 'Select successful', token: tokenSelect };
|
||||
} else if (userType === 'occupant') {
|
||||
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow(
|
||||
{
|
||||
where: { uu_id: dto.uuid },
|
||||
omit: {
|
||||
id: true,
|
||||
person_id: true,
|
||||
build_parts_id: true,
|
||||
occupant_type_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
cryp_uu_id: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
const occupantType = await this.prisma.occupant_types.findFirstOrThrow({
|
||||
where: { uu_id: livingSpace.occupant_type_uu_id },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow({
|
||||
where: { uu_id: dto.uuid },
|
||||
select: {
|
||||
uu_id: true,
|
||||
build_parts_uu_id: true,
|
||||
occupant_type_uu_id: true,
|
||||
occupant_types: {
|
||||
select: {
|
||||
user_types: {
|
||||
select: {
|
||||
token: true,
|
||||
type_token: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const occupantType = await this.prisma.occupant_types.findFirstOrThrow({
|
||||
where: { uu_id: livingSpace.occupant_type_uu_id }
|
||||
});
|
||||
const userTypeInfo = occupantType.user_type_uu_id ?
|
||||
await this.prisma.user_types.findFirst({
|
||||
where: { uu_id: occupantType.user_type_uu_id },
|
||||
select: {
|
||||
uu_id: true,
|
||||
type: true,
|
||||
description: true,
|
||||
type_token: true,
|
||||
token: true
|
||||
}
|
||||
}) : null;
|
||||
const part = await this.prisma.build_parts.findFirstOrThrow({
|
||||
where: { uu_id: livingSpace.build_parts_uu_id },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
select: {
|
||||
uu_id: true,
|
||||
part_code: true,
|
||||
part_no: true,
|
||||
part_level: true,
|
||||
human_livable: true,
|
||||
build_uu_id: true,
|
||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
enum_class: true,
|
||||
value: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const build = await this.prisma.build.findFirstOrThrow({
|
||||
where: { uu_id: part.build_uu_id },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
select: {
|
||||
uu_id: true,
|
||||
build_name: true
|
||||
}
|
||||
});
|
||||
const company = await this.prisma.companies.findFirstOrThrow({
|
||||
where: { uu_id: accessObject.value.users.related_company },
|
||||
omit: {
|
||||
id: true,
|
||||
cryp_uu_id: true,
|
||||
ref_id: true,
|
||||
replication_id: true,
|
||||
},
|
||||
select: {
|
||||
uu_id: true,
|
||||
is_confirmed: true,
|
||||
deleted: true,
|
||||
active: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
ref_int: true
|
||||
}
|
||||
});
|
||||
const occupantToken = OccupantTokenSchema.parse({
|
||||
uuid: dto.uuid,
|
||||
livingSpace: livingSpace,
|
||||
occupant: occupantType,
|
||||
build: build,
|
||||
@@ -196,59 +229,40 @@ export class SelectService {
|
||||
company: company,
|
||||
menu: null,
|
||||
pages: null,
|
||||
config: null,
|
||||
caches: null,
|
||||
selection: await this.prisma.build_living_space.findFirstOrThrow({
|
||||
where: { uu_id: dto.uuid },
|
||||
select: {
|
||||
uu_id: true,
|
||||
occupant_types: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
occupant_code: true,
|
||||
occupant_type: true,
|
||||
function_retriever: true,
|
||||
},
|
||||
},
|
||||
build_parts: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
part_code: true,
|
||||
part_no: true,
|
||||
part_level: true,
|
||||
human_livable: true,
|
||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
enum_class: true,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
select: {
|
||||
uu_id: true,
|
||||
build_name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
events: null,
|
||||
selection: {
|
||||
occupant_types: {
|
||||
uu_id: occupantType.uu_id,
|
||||
occupant_code: occupantType.occupant_code,
|
||||
occupant_type: occupantType.occupant_type
|
||||
},
|
||||
}),
|
||||
functionsRetriever: occupantType.function_retriever,
|
||||
kind: UserType.occupant,
|
||||
build_parts: {
|
||||
uu_id: part.uu_id,
|
||||
part_code: part.part_code,
|
||||
part_no: part.part_no,
|
||||
part_level: part.part_level,
|
||||
human_livable: part.human_livable,
|
||||
api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown: {
|
||||
uu_id: part.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown.uu_id,
|
||||
enum_class: part.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown.enum_class,
|
||||
value: part.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown.value
|
||||
},
|
||||
build: {
|
||||
uu_id: build.uu_id, build_name: build.build_name
|
||||
}
|
||||
}
|
||||
},
|
||||
typeToken: userTypeInfo?.type_token,
|
||||
functionsRetriever: userTypeInfo?.token,
|
||||
kind: UserType.occupant
|
||||
});
|
||||
const tokenSelect = await this.redis.setSelectToRedis(
|
||||
accessToken,
|
||||
occupantToken,
|
||||
accessObject.value.users.uu_id,
|
||||
dto.uuid,
|
||||
);
|
||||
return {
|
||||
message: 'Select successful',
|
||||
token: tokenSelect,
|
||||
};
|
||||
} else {
|
||||
throw new NotAcceptableException('Invalid user type');
|
||||
}
|
||||
|
||||
occupantToken.events = await this.eventService.getEventsOccupants(livingSpace.uu_id);
|
||||
occupantToken.pages = await this.pagesService.getPagesOccupants(accessObject.value.users.uu_id, livingSpace.occupant_types.user_types?.token || '');
|
||||
occupantToken.menu = await this.menusService.renderOccupantMenu(occupantToken.pages);
|
||||
|
||||
const tokenSelect = await this.redis.setSelectToRedis(accessToken, occupantToken, accessObject.value.users.uu_id, dto.uuid);
|
||||
return { message: 'Select successful', token: tokenSelect };
|
||||
} else { throw new NotAcceptableException('Invalid user type') }
|
||||
}
|
||||
}
|
||||
|
||||
9
ServicesApi/src/database/mongo/mongo.module.ts
Normal file
9
ServicesApi/src/database/mongo/mongo.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongoService } from './mongo.service';
|
||||
import { MongoProvider } from './mongo.provider';
|
||||
|
||||
@Module({
|
||||
providers: [MongoProvider, MongoService],
|
||||
exports: [MongoService, MongoProvider, 'MONGO_DB']
|
||||
})
|
||||
export class MongoModule {}
|
||||
11
ServicesApi/src/database/mongo/mongo.provider.ts
Normal file
11
ServicesApi/src/database/mongo/mongo.provider.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MongoClient, Db } from 'mongodb';
|
||||
|
||||
export const MongoProvider = {
|
||||
provide: 'MONGO_DB',
|
||||
useFactory: async (): Promise<Db> => {
|
||||
const uri = 'mongodb://appuser:apppassword@10.10.2.13:27017/appdb';
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
return client.db('appdb');
|
||||
},
|
||||
};
|
||||
18
ServicesApi/src/database/mongo/mongo.service.spec.ts
Normal file
18
ServicesApi/src/database/mongo/mongo.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MongoService } from './mongo.service';
|
||||
|
||||
describe('MongoService', () => {
|
||||
let service: MongoService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [MongoService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MongoService>(MongoService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
294
ServicesApi/src/database/mongo/mongo.service.ts
Normal file
294
ServicesApi/src/database/mongo/mongo.service.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Db, Document, Collection, Filter, ObjectId, UpdateResult, Sort, SortDirection } from 'mongodb';
|
||||
|
||||
@Injectable()
|
||||
export class MongoService {
|
||||
|
||||
private collection: Collection<Document>;
|
||||
|
||||
constructor(@Inject('MONGO_DB') private readonly db: Db) { this.collection = this.db.collection('mongoCache') }
|
||||
|
||||
async set(collectionName: string) { this.collection = this.db.collection(collectionName) }
|
||||
|
||||
async getDb() { return this.collection }
|
||||
|
||||
async create(data: Record<string, any>): Promise<Document> {
|
||||
const insertResult = await this.collection.insertOne(data);
|
||||
if (!insertResult.acknowledged) { throw new Error('Failed to insert document') }
|
||||
return await this.getOne(insertResult.insertedId);
|
||||
}
|
||||
async createMany(data: Record<string, any>[]): Promise<number> {
|
||||
const insertResult = await this.collection.insertMany(data);
|
||||
if (!insertResult.acknowledged) { throw new Error('Failed to insert documents') }
|
||||
return insertResult.insertedCount;
|
||||
}
|
||||
|
||||
async findManyKeyWhere(value: string, limit?: number, skip?: number): Promise<Document[]> {
|
||||
const docs = await this.collection.find({
|
||||
$where: function () {
|
||||
return Object.keys(this).some(key => key.includes(value));
|
||||
}
|
||||
}).toArray();
|
||||
return docs;
|
||||
}
|
||||
|
||||
async findManyKeyWhereValue(value: string, limit?: number, skip?: number): Promise<Document[]> {
|
||||
const docs = await this.collection.find({
|
||||
$where: function () {
|
||||
return Object.keys(this).some(key => this[key] === value);
|
||||
}
|
||||
}).toArray();
|
||||
return docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a document by UUID or create it if it doesn't exist
|
||||
* @param data Document data with UUID field
|
||||
* @returns The found or created document
|
||||
*
|
||||
* @example
|
||||
* Create a new user or retrieve existing one
|
||||
* const userData = { uuid: 'TOKEN:12345:user', name: 'John Doe', email: 'john@example.com' };
|
||||
* const user = await mongoService.findOrCreate(userData);
|
||||
*/
|
||||
async findOrCreate(data: Record<string, any>): Promise<{ data: Document, isCreated: boolean }> {
|
||||
if (!data.uuid) { throw new Error('UUID is required for findOrCreate operation') }
|
||||
// Use direct UUID lookup instead of regex for exact match
|
||||
const existingDoc = await this.collection.findOne({ uuid: data.uuid } as Filter<Document>);
|
||||
if (existingDoc) { return { data: existingDoc, isCreated: false } }
|
||||
const insertResult = await this.collection.insertOne(data);
|
||||
if (!insertResult.acknowledged) { throw new Error('Failed to insert document') }
|
||||
return { data: await this.getOne(insertResult.insertedId), isCreated: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all documents from the collection
|
||||
* @returns Array of all documents
|
||||
*
|
||||
* @example
|
||||
* Get all users in the collection
|
||||
* const allUsers = await mongoService.getAll();
|
||||
*/
|
||||
async getAll(): Promise<Document[]> { return await this.collection.find().toArray() }
|
||||
|
||||
/**
|
||||
* Find a document by ID key using regex pattern
|
||||
* @param idKey ID key to search for
|
||||
* @returns The found document
|
||||
* @throws Error if document is not found
|
||||
*
|
||||
* @example
|
||||
* Find a user by ID key
|
||||
* const user = await mongoService.findOne('12345');
|
||||
* This will search for documents with uuid matching pattern ^TOKEN:12345:
|
||||
*/
|
||||
async findOne(filter: Filter<Document>): Promise<Document | null> {
|
||||
const result = await this.collection.findOne(filter);
|
||||
return result ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple documents using a filter
|
||||
* @param filter MongoDB filter
|
||||
* @param limit Optional limit of results (default: no limit)
|
||||
* @param skip Optional number of documents to skip (default: 0)
|
||||
* @returns Array of matching documents
|
||||
*
|
||||
* @example
|
||||
* Find active users with pagination
|
||||
* const filter = { active: true } as Filter<Document>;
|
||||
* const activeUsers = await mongoService.findMany(filter, 10, 20); // limit 10, skip 20
|
||||
*
|
||||
* @example
|
||||
* Find users by role
|
||||
* const admins = await mongoService.findMany({ role: 'admin' } as Filter<Document>);
|
||||
* const documents = await mongoService.findMany(filter, limit, skip, ['name', 'createdAt'], ['asc', 'desc']);
|
||||
*/
|
||||
async findMany(filter: Filter<Document>, limit?: number, skip?: number, sortBys?: string[], sortDirections?: SortDirection[]): Promise<Document[]> {
|
||||
let query = this.collection.find(filter);
|
||||
if (typeof skip === 'number') { query = query.skip(skip) }
|
||||
if (typeof limit === 'number') { query = query.limit(limit) }
|
||||
if (sortBys && sortDirections && sortBys.length === sortDirections.length) {
|
||||
const sortOptions = sortBys.reduce<Record<string, SortDirection>>((acc, sortBy, index) => ({
|
||||
...acc,
|
||||
[sortBy]: sortDirections[index]
|
||||
}), {});
|
||||
query = query.sort(sortOptions);
|
||||
}
|
||||
return await query.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a document by its MongoDB ObjectId
|
||||
* @param id MongoDB ObjectId
|
||||
* @returns The found document
|
||||
* @throws Error if document is not found
|
||||
*
|
||||
* @example
|
||||
* Get a user by ObjectId
|
||||
* const userId = new ObjectId('507f1f77bcf86cd799439011');
|
||||
* const user = await mongoService.getOne(userId);
|
||||
*/
|
||||
async getOne(id: ObjectId): Promise<Document> {
|
||||
const result = await this.collection.findOne({ _id: id });
|
||||
if (!result) { throw new Error(`Document with ID ${id.toString()} not found`) }
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find documents by regex pattern on UUID field
|
||||
* @param idKey ID key to search for
|
||||
* @returns Array of matching documents
|
||||
*
|
||||
* @example
|
||||
* Find all users with a specific ID key pattern
|
||||
* const users = await mongoService.findByRegex('12345');
|
||||
* This will return all documents with uuid matching pattern ^TOKEN:12345:
|
||||
*/
|
||||
async findByRegex(idKey: string): Promise<Document[]> {
|
||||
if (!idKey) { throw new Error('ID key is required for regex search') }
|
||||
const pattern = `^${idKey}`;
|
||||
return await this.collection.find({ uuid: { $regex: pattern } } as Filter<Document>).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single document by its MongoDB ObjectId
|
||||
* @param id MongoDB ObjectId
|
||||
* @param data Data to update
|
||||
* @returns The updated document
|
||||
* @throws Error if document is not found or update fails
|
||||
*
|
||||
* @example
|
||||
* Update a user's profile
|
||||
* const userId = new ObjectId('507f1f77bcf86cd799439011');
|
||||
* const updates = { name: 'Jane Doe', lastLogin: new Date() };
|
||||
* const updatedUser = await mongoService.updateOne(userId, updates);
|
||||
*/
|
||||
async updateOne(id: ObjectId, data: Record<string, any>): Promise<Document> {
|
||||
const updateResult = await this.collection.updateOne(
|
||||
{ _id: id },
|
||||
{ $set: data }
|
||||
);
|
||||
if (!updateResult.acknowledged) { throw new Error('Update operation failed') }
|
||||
if (updateResult.matchedCount === 0) { throw new Error(`Document with ID ${id.toString()} not found`) }
|
||||
return await this.getOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple documents matching a filter
|
||||
* @param filter MongoDB filter
|
||||
* @param data Data to update
|
||||
* @returns Update result with count of modified documents
|
||||
* @throws Error if update fails
|
||||
*
|
||||
* @example
|
||||
* Mark all inactive users as archived
|
||||
* const filter = { active: false } as Filter<Document>;
|
||||
* const updates = { status: 'archived', archivedAt: new Date() };
|
||||
* const result = await mongoService.updateMany(filter, updates);
|
||||
* console.log(`${result.modifiedCount} users archived`);
|
||||
*/
|
||||
async updateMany(filter: Filter<Document>, data: Record<string, any>): Promise<UpdateResult> {
|
||||
const updateResult = await this.collection.updateMany(filter, { $set: data });
|
||||
if (!updateResult.acknowledged) { throw new Error('Update operation failed') }
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document by its MongoDB ObjectId
|
||||
* @param id MongoDB ObjectId
|
||||
* @returns True if document was deleted, false otherwise
|
||||
*
|
||||
* @example
|
||||
* Delete a user account
|
||||
* const userId = new ObjectId('507f1f77bcf86cd799439011');
|
||||
* const deleted = await mongoService.deleteOne(userId);
|
||||
* if (deleted) console.log('User successfully deleted');
|
||||
*/
|
||||
async deleteOne(id: ObjectId): Promise<boolean> {
|
||||
const deleteResult = await this.collection.deleteOne({ _id: id });
|
||||
return deleteResult.acknowledged && deleteResult.deletedCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple documents matching a filter
|
||||
* @param filter MongoDB filter
|
||||
* @returns Number of deleted documents
|
||||
*
|
||||
* @example
|
||||
* Delete all expired sessions
|
||||
* const filter = { expiresAt: { $lt: new Date() } } as Filter<Document>;
|
||||
* const count = await mongoService.deleteMany(filter);
|
||||
* console.log(`${count} expired sessions deleted`);
|
||||
*/
|
||||
async deleteMany(filter: Filter<Document>): Promise<number> {
|
||||
const deleteResult = await this.collection.deleteMany(filter);
|
||||
return deleteResult.acknowledged ? deleteResult.deletedCount : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find documents by regex pattern on any specified field
|
||||
* @param field The field name to apply the regex filter on
|
||||
* @param value The value to search for in the field
|
||||
* @param options Optional regex options (e.g., 'i' for case-insensitive)
|
||||
* @param prefix Optional prefix to add before the value (default: '')
|
||||
* @param suffix Optional suffix to add after the value (default: '')
|
||||
* @returns Array of matching documents
|
||||
*
|
||||
* @example
|
||||
* Find users with email from a specific domain (case-insensitive)
|
||||
* const gmailUsers = await mongoService.findByFieldRegex('email', 'gmail.com', 'i');
|
||||
*
|
||||
* @example
|
||||
* Find users with names starting with 'J'
|
||||
* const usersStartingWithJ = await mongoService.findByFieldRegex('name', 'J', 'i', '^');
|
||||
*
|
||||
* @example
|
||||
* Find users with phone numbers ending in specific digits
|
||||
* const specificPhoneUsers = await mongoService.findByFieldRegex('phone', '5555', '', '', '$');
|
||||
*/
|
||||
async findByFieldRegex(field: string, value: string, options?: string, prefix: string = '', suffix: string = ''): Promise<Document[]> {
|
||||
if (!field || !value) { throw new Error('Field name and value are required for regex search') }
|
||||
const pattern = `${prefix}${value}${suffix}`;
|
||||
const query: Record<string, any> = {};
|
||||
query[field] = { $regex: pattern };
|
||||
if (options) { query[field].$options = options; }
|
||||
return await this.collection.find(query as unknown as Filter<Document>).toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find documents by regex pattern across all fields (including nested)
|
||||
* @param value The value to search for
|
||||
* @param options Optional regex options (e.g., 'i' for case-insensitive)
|
||||
* @returns Array of matching documents
|
||||
*
|
||||
* @example
|
||||
* Find any document containing a specific value anywhere
|
||||
* const docs = await mongoService.findByRegexAcrossFields('someValue', 'i');
|
||||
*/
|
||||
async findByRegexAcrossFields(value: string, options?: string, searchType: 'value' | 'key' | 'both' = 'value'): Promise<Document[]> {
|
||||
if (!value) { throw new Error('Search value is required') }
|
||||
const query: any = { $or: [] };
|
||||
if (searchType === 'value' || searchType === 'both') {
|
||||
query.$or.push(
|
||||
{ '$expr': { $regexMatch: { input: { $toString: '$$ROOT' }, regex: value, options } } },
|
||||
{ 'data': { $type: 'object', $regex: value, $options: options } }
|
||||
);
|
||||
}
|
||||
|
||||
if (searchType === 'key' || searchType === 'both') {
|
||||
query.$where = function () {
|
||||
const searchRegex = new RegExp(value, options);
|
||||
function checkKeys(obj: Record<string, any>) {
|
||||
for (const key in obj) {
|
||||
if (searchRegex.test(key)) return true; if (obj[key] && typeof obj[key] === 'object') { if (checkKeys(obj[key])) return true }
|
||||
}
|
||||
return false;
|
||||
} return checkKeys(this)
|
||||
}.toString();
|
||||
}
|
||||
|
||||
return await this.collection.find(query as unknown as Filter<Document>).toArray();
|
||||
}
|
||||
|
||||
}
|
||||
2
ServicesApi/src/database/redis/redis.constants.ts
Normal file
2
ServicesApi/src/database/redis/redis.constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const REDIS_CLIENT = 'REDIS_CLIENT';
|
||||
export const REDIS_OPTIONS = 'REDIS_OPTIONS';
|
||||
23
ServicesApi/src/database/redis/redis.interfaces.ts
Normal file
23
ServicesApi/src/database/redis/redis.interfaces.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ModuleMetadata, Type } from '@nestjs/common';
|
||||
|
||||
export interface RedisModuleOptions {
|
||||
config?: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
password?: string;
|
||||
db?: number;
|
||||
keyPrefix?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RedisOptionsFactory {
|
||||
createRedisOptions(): Promise<RedisModuleOptions> | RedisModuleOptions;
|
||||
}
|
||||
|
||||
export interface RedisModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
||||
useExisting?: Type<RedisOptionsFactory>;
|
||||
useClass?: Type<RedisOptionsFactory>;
|
||||
useFactory?: (...args: any[]) => Promise<RedisModuleOptions> | RedisModuleOptions;
|
||||
inject?: any[];
|
||||
}
|
||||
91
ServicesApi/src/database/redis/redis.module.ts
Normal file
91
ServicesApi/src/database/redis/redis.module.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import Redis from 'ioredis';
|
||||
|
||||
import { DynamicModule, Module, OnApplicationShutdown, Provider } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { CacheService } from './redis.service';
|
||||
import { REDIS_CLIENT, REDIS_OPTIONS } from './redis.constants';
|
||||
import { RedisModuleOptions, RedisModuleAsyncOptions } from './redis.interfaces';
|
||||
|
||||
@Module({})
|
||||
export class RedisModule implements OnApplicationShutdown {
|
||||
constructor(private moduleRef: ModuleRef) { }
|
||||
|
||||
/**
|
||||
* Registers the module with default configuration.
|
||||
*
|
||||
* @param isGlobal - Register in the global scope
|
||||
* @returns A DynamicModule
|
||||
*/
|
||||
static forRootWithConfig(isGlobal = false): DynamicModule {
|
||||
const redisConfig = { host: '10.10.2.15', port: 6379, password: 'your_strong_password_here' };
|
||||
return RedisModule.forRoot({ config: redisConfig }, isGlobal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the module synchronously.
|
||||
*
|
||||
* @param options - The module options
|
||||
* @param isGlobal - Register in the global scope
|
||||
* @returns A DynamicModule
|
||||
*/
|
||||
static forRoot(options?: RedisModuleOptions, isGlobal = false): DynamicModule {
|
||||
const redisOptionsProvider: Provider = {
|
||||
provide: REDIS_OPTIONS,
|
||||
useValue: options || {},
|
||||
};
|
||||
|
||||
const redisClientProvider: Provider = {
|
||||
provide: REDIS_CLIENT,
|
||||
useFactory: () => {
|
||||
if (!options || !options.config) { return new Redis() }
|
||||
const { host, port, password } = options.config;
|
||||
return new Redis({ host, port, password });
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
module: RedisModule,
|
||||
providers: [redisOptionsProvider, redisClientProvider, CacheService],
|
||||
exports: [CacheService],
|
||||
global: isGlobal,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the module asynchronously.
|
||||
*
|
||||
* @param options - The async module options
|
||||
* @param isGlobal - Register in the global scope
|
||||
* @returns A DynamicModule
|
||||
*/
|
||||
static forRootAsync(options: RedisModuleAsyncOptions, isGlobal = false): DynamicModule {
|
||||
const redisOptionsProvider: Provider = {
|
||||
provide: REDIS_OPTIONS,
|
||||
useFactory: options.useFactory || (() => ({})),
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
const redisClientProvider: Provider = {
|
||||
provide: REDIS_CLIENT,
|
||||
useFactory: (redisOptions: RedisModuleOptions) => {
|
||||
if (!redisOptions || !redisOptions.config) { return new Redis() }
|
||||
const { host, port, password } = redisOptions.config;
|
||||
return new Redis({ host, port, password })
|
||||
},
|
||||
inject: [REDIS_OPTIONS],
|
||||
};
|
||||
|
||||
return {
|
||||
module: RedisModule,
|
||||
imports: options.imports || [],
|
||||
providers: [redisOptionsProvider, redisClientProvider, CacheService],
|
||||
exports: [CacheService],
|
||||
global: isGlobal,
|
||||
};
|
||||
}
|
||||
|
||||
async onApplicationShutdown() {
|
||||
const client = this.moduleRef.get(REDIS_CLIENT, { strict: false });
|
||||
if (client && typeof client.quit === 'function') { await client.quit() }
|
||||
}
|
||||
}
|
||||
30
ServicesApi/src/database/redis/redis.service.spec.ts
Normal file
30
ServicesApi/src/database/redis/redis.service.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CacheService } from './redis.service';
|
||||
import Redis from 'ioredis';
|
||||
import { REDIS_CLIENT } from './redis.constants';
|
||||
|
||||
describe('CacheService', () => {
|
||||
let service: CacheService;
|
||||
let mockRedisClient: Redis;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a mock Redis client
|
||||
mockRedisClient = new Redis();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CacheService,
|
||||
{
|
||||
provide: REDIS_CLIENT,
|
||||
useValue: mockRedisClient,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CacheService>(CacheService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RedisService } from '@liaoliaots/nestjs-redis';
|
||||
import Redis from 'ioredis';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { REDIS_CLIENT } from './redis.constants';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
private client: Redis;
|
||||
|
||||
constructor(private readonly redisService: RedisService) {
|
||||
this.client = this.redisService.getOrThrow();
|
||||
constructor(@Inject(REDIS_CLIENT) private readonly redisClient: Redis) {
|
||||
this.client = redisClient;
|
||||
}
|
||||
|
||||
async set(key: string, value: any) {
|
||||
@@ -16,10 +16,8 @@ export class CacheService {
|
||||
|
||||
async get(key: string): Promise<any | null> {
|
||||
const value = await this.client.get(key);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return { key, value: JSON.parse(value) };
|
||||
if (!value) { return null }
|
||||
return { key, value: JSON.parse(value) }
|
||||
}
|
||||
|
||||
async set_with_ttl(key: string, value: any, ttl: number) {
|
||||
@@ -32,10 +30,7 @@ export class CacheService {
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
const deleted = await this.client.del(key);
|
||||
if (deleted === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return deleted === 0 ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4,31 +4,39 @@ import {
|
||||
Injectable,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { RedisHandlers } from '@/src/utils/auth/redis_handlers';
|
||||
import { RedisHandlers } from '@/src/utils/store/redisHandlers';
|
||||
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
|
||||
|
||||
@Injectable()
|
||||
export class AuthControlGuard implements CanActivate {
|
||||
constructor(private cacheService: RedisHandlers) {}
|
||||
constructor(private cacheService: RedisHandlers) { }
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const accessToken = this.cacheService.mergeLoginKey(req);
|
||||
console.log('AuthControlGuard', accessToken);
|
||||
if (!accessToken) { throw new ForbiddenException('Send to Login') }
|
||||
this.cacheService.renewTtlLoginFromRedis(req);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EndpointControlGuard implements CanActivate {
|
||||
constructor(private cacheService: RedisHandlers) {}
|
||||
constructor(
|
||||
private cacheService: RedisHandlers,
|
||||
private urlHandler: UrlHandler,
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
// const selectToken = this.cacheService.mergeSelectKey(req);
|
||||
// const method = req.method;
|
||||
// const path = req.route?.path;
|
||||
const method = req.method;
|
||||
const path = req.route?.path;
|
||||
const keyUrl = `${path}:${method.toUpperCase()}`;
|
||||
const driveToken = await this.urlHandler.getSecureUrlToken(keyUrl);
|
||||
const accessObject = await this.cacheService.getSelectFromRedis(req);
|
||||
console.log('EndpointControlGuard', accessObject);
|
||||
if (!accessObject) { throw new ForbiddenException('Access denied') }
|
||||
req.driveToken = `${driveToken}:${accessObject?.value.functionsRetriever}`;
|
||||
this.cacheService.renewTtlSelectFromRedis(req);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
47
ServicesApi/src/navigator/dtoValidator.ts
Normal file
47
ServicesApi/src/navigator/dtoValidator.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { IsString, IsObject, IsOptional, IsNumber } from 'class-validator';
|
||||
|
||||
export class mongoSetValidator {
|
||||
@IsString()
|
||||
collectionName: string;
|
||||
|
||||
@IsObject()
|
||||
data: object;
|
||||
}
|
||||
|
||||
export class mongoGetValidator {
|
||||
@IsString()
|
||||
collectionName: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
regexKey: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter: object;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
limit: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
skip: number;
|
||||
}
|
||||
|
||||
|
||||
export class eventSetValidator {
|
||||
@IsString()
|
||||
usersUUID: string;
|
||||
|
||||
@IsObject()
|
||||
event: Record<string, Record<string, Record<string, string>>>;
|
||||
}
|
||||
|
||||
export class eventGetValidator {
|
||||
@IsString()
|
||||
usersUUID: string;
|
||||
|
||||
@IsString()
|
||||
typeToken: string;
|
||||
}
|
||||
41
ServicesApi/src/navigator/events/dtoValidator.ts
Normal file
41
ServicesApi/src/navigator/events/dtoValidator.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { IsString, IsObject, IsOptional, IsNumber, ValidateNested } from 'class-validator';
|
||||
|
||||
// { # collection Events:Build-UUIDv4 | Events:Company-UUIDv4 : "userUUID" : { "userTypeToken" : { "siteUrlToken" : "eventKey" } } }
|
||||
// const jsonData = { 'USER-UUID(V4)': { 'j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA': { 'e6hewIe7YqbQZHO3': 'qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx' } } };
|
||||
|
||||
export class EventsSetterValidator {
|
||||
|
||||
@IsObject()
|
||||
event: Record<string, Record<string, Record<string, string>>>;
|
||||
|
||||
@IsString()
|
||||
userUUID: string; // UUID of employee or occupant
|
||||
}
|
||||
|
||||
export class EventsGetterValidator {
|
||||
@IsString()
|
||||
collectionName: string;
|
||||
|
||||
@IsString()
|
||||
dutyUUID: string; // UUID of employee or occupant
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
regexKey?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
searchType?: 'value' | 'key' | 'both';
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
filter?: object = {};
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
limit?: number = 1;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
skip?: number = 0;
|
||||
}
|
||||
18
ServicesApi/src/navigator/events/events.service.spec.ts
Normal file
18
ServicesApi/src/navigator/events/events.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
describe('EventsService', () => {
|
||||
let service: EventsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [EventsService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EventsService>(EventsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
214
ServicesApi/src/navigator/events/events.service.ts
Normal file
214
ServicesApi/src/navigator/events/events.service.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { Body, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { MongoService } from '@/src/database/mongo/mongo.service';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { EventsGetterValidator, EventsSetterValidator } from '@/src/navigator/events/dtoValidator';
|
||||
import { Document } from 'mongodb';
|
||||
|
||||
type SearchType = 'value' | 'key' | 'both';
|
||||
|
||||
@Injectable()
|
||||
export class EventsService {
|
||||
|
||||
// const result = await eventsService.getEventsOccupants({
|
||||
// collectionName: 'Events:Build-UUIDv4',
|
||||
// regexKey: 'qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx'
|
||||
// });
|
||||
// const result = await eventsService.getEventsOccupants({
|
||||
// collectionName: 'Events:Build-UUIDv4',
|
||||
// regexKey: 'e6hewIe7YqbQZHO3',
|
||||
// searchType: 'key'
|
||||
// });
|
||||
// const result = await eventsService.getEventsOccupants({
|
||||
// collectionName: 'Events:Build-UUIDv4',
|
||||
// regexKey: 'e6hewIe7YqbQZHO3',
|
||||
// searchType: 'both'
|
||||
// });
|
||||
|
||||
constructor(private mongoService: MongoService, private prisma: PrismaService) { }
|
||||
seperator = "/"
|
||||
|
||||
private async getBuildUUID(uuid: string) {
|
||||
console.log('uuid', uuid)
|
||||
const livingSpace = await this.prisma.build_living_space.findFirstOrThrow({
|
||||
where: { uu_id: uuid },
|
||||
select: {
|
||||
people: {
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
uu_id: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
build_parts: {
|
||||
select: {
|
||||
build: {
|
||||
select: {
|
||||
uu_id: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const userUUID = livingSpace.people.users[0].uu_id
|
||||
const buildUUID = livingSpace.build_parts.build.uu_id
|
||||
return { userUUID, buildUUID }
|
||||
}
|
||||
|
||||
private async getCompanyUUID(uuid: string) {
|
||||
const employee = await this.prisma.employees.findFirstOrThrow({
|
||||
where: { uu_id: uuid },
|
||||
select: {
|
||||
people: {
|
||||
select: {
|
||||
users: {
|
||||
select: {
|
||||
uu_id: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
staff: {
|
||||
select: {
|
||||
duties: {
|
||||
select: {
|
||||
company_uu_id: true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const userUUID = employee.people?.users[0].uu_id
|
||||
const companyUUID = employee.staff?.duties.company_uu_id
|
||||
return { userUUID, companyUUID }
|
||||
}
|
||||
|
||||
private validateCollectionName(collectionName: string) {
|
||||
if (!collectionName) {
|
||||
throw new NotFoundException('Collection name is required')
|
||||
}
|
||||
}
|
||||
|
||||
private async setupMongoCollection(collectionName: string, buildUUID: string) {
|
||||
await this.mongoService.set(collectionName);
|
||||
await this.mongoService.set(`EVENTS${this.seperator}${buildUUID}`);
|
||||
}
|
||||
|
||||
private escapeRegex(text: string): string {
|
||||
return text.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
||||
}
|
||||
|
||||
async getAllEventsOccupants(userTypeToken: string): Promise<Document[] | null> {
|
||||
await this.mongoService.set(`Events`)
|
||||
return await this.mongoService.findManyKeyWhere(userTypeToken) || null;
|
||||
}
|
||||
|
||||
async getAllEventsEmployees(userTypeToken: string): Promise<Document[] | null> {
|
||||
await this.mongoService.set(`Events`)
|
||||
return await this.mongoService.findManyKeyWhere(userTypeToken) || null;
|
||||
}
|
||||
|
||||
async getEventsOccupants(livingSpaceUUID: string) {
|
||||
const eventsObject = {}
|
||||
const { userUUID, buildUUID } = await this.getBuildUUID(livingSpaceUUID);
|
||||
const collectionKey = `Events/${buildUUID}`
|
||||
this.validateCollectionName(collectionKey);
|
||||
await this.mongoService.set(collectionKey)
|
||||
const eventsResponse = await this.mongoService.findOne({ [userUUID]: { $exists: true } });
|
||||
if (eventsResponse && typeof eventsResponse === 'object') {
|
||||
const mapOfEvents = eventsResponse[userUUID];
|
||||
if (mapOfEvents && typeof mapOfEvents === 'object') {
|
||||
const userTypeTokenKey = Object.keys(mapOfEvents)[0];
|
||||
const userTypeTokenValue = mapOfEvents[userTypeTokenKey];
|
||||
if (userTypeTokenValue && typeof userTypeTokenValue === 'object') {
|
||||
for (const siteUrlTokenKey of Object.keys(userTypeTokenValue)) {
|
||||
const siteUrlTokenValue = userTypeTokenValue[siteUrlTokenKey];
|
||||
eventsObject[`${siteUrlTokenKey}:${userTypeTokenKey}`] = siteUrlTokenValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return eventsObject || null;
|
||||
}
|
||||
|
||||
async getEventsEmployees(employeeUUID: string) {
|
||||
const eventsObject = {}
|
||||
const companyUUID = await this.getCompanyUUID(employeeUUID);
|
||||
if (!companyUUID) { throw new NotFoundException('Company not found') }
|
||||
await this.mongoService.set(`EVENTS${this.seperator}${companyUUID}`);
|
||||
const eventsResponse = await this.mongoService.findOne({ [employeeUUID]: { $exists: true } });
|
||||
if (eventsResponse && typeof eventsResponse === 'object') {
|
||||
const mapOfEvents = eventsResponse[employeeUUID];
|
||||
if (mapOfEvents && typeof mapOfEvents === 'object') {
|
||||
const userTypeTokenKey = Object.keys(mapOfEvents)[0];
|
||||
const userTypeTokenValue = mapOfEvents[userTypeTokenKey];
|
||||
if (userTypeTokenValue && typeof userTypeTokenValue === 'object') {
|
||||
for (const siteUrlTokenKey of Object.keys(userTypeTokenValue)) {
|
||||
const siteUrlTokenValue = userTypeTokenValue[siteUrlTokenKey];
|
||||
eventsObject[`${siteUrlTokenKey}:${userTypeTokenKey}`] = siteUrlTokenValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return eventsObject || null;
|
||||
}
|
||||
|
||||
private async setSavedEventToMapper(data: any, useruuid: string) {
|
||||
await this.mongoService.set(`MAP${this.seperator}EVENTS`);
|
||||
const events = await this.mongoService.findOrCreate({ uuid: `EVENTS${this.seperator}${useruuid}:${data.uuid}`, data });
|
||||
}
|
||||
|
||||
private async deleteSavedEventFromMapper(data: any, useruuid: string) {
|
||||
await this.mongoService.set(`MAP${this.seperator}EVENTS`);
|
||||
const events = await this.mongoService.deleteMany({ uuid: `EVENTS${this.seperator}${useruuid}:${data.uuid}` });
|
||||
return events;
|
||||
}
|
||||
|
||||
|
||||
async setEventsEmployees(@Body() body: EventsSetterValidator) {
|
||||
const companyUUID = await this.getCompanyUUID(body.userUUID);
|
||||
if (!companyUUID) { throw new NotFoundException('Company not found') }
|
||||
await this.mongoService.set(`EVENTS${this.seperator}${companyUUID}`);
|
||||
const events = await this.mongoService.findOrCreate(body.event);
|
||||
// await this.setSavedEventToMapper(events, body.dutyUUID);
|
||||
return events;
|
||||
}
|
||||
|
||||
async setEventsOccupants(@Body() body: EventsSetterValidator) {
|
||||
const buildUUID = await this.getBuildUUID(body.userUUID);
|
||||
if (!buildUUID) { throw new NotFoundException('Build not found') }
|
||||
await this.mongoService.set(`EVENTS${this.seperator}${buildUUID}`);
|
||||
const events = await this.mongoService.findOrCreate(body.event);
|
||||
return events;
|
||||
}
|
||||
|
||||
async setEvents(events: any, serviceName: string) {
|
||||
await this.mongoService.set(`Events`);
|
||||
for (const [key, value] of Object.entries(events)) {
|
||||
const description = (value as Array<any>)[0].endpoint || "";
|
||||
console.log(`Setting events for ${serviceName} ${description} is carried to nosql database store.`)
|
||||
await this.mongoService.deleteMany({ [key]: { $exists: true } });
|
||||
await this.mongoService.create({ [key]: value });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEventsEmployees(@Body() body: EventsGetterValidator) {
|
||||
const companyUUID = await this.getCompanyUUID(body.dutyUUID);
|
||||
if (!companyUUID) { throw new NotFoundException('Company not found') }
|
||||
await this.mongoService.set(`EVENTS${this.seperator}${companyUUID}`);
|
||||
const events = await this.mongoService.deleteMany({ uuid: { $regex: body.regexKey, $options: 'i' } });
|
||||
return events;
|
||||
}
|
||||
|
||||
async deleteEventsOccupants(@Body() body: EventsGetterValidator) {
|
||||
const buildUUID = await this.getBuildUUID(body.dutyUUID);
|
||||
if (!buildUUID) { throw new NotFoundException('Build not found') }
|
||||
await this.mongoService.set(`EVENTS${this.seperator}${buildUUID}`);
|
||||
const events = await this.mongoService.deleteMany({ uuid: { $regex: body.regexKey, $options: 'i' } });
|
||||
return events;
|
||||
}
|
||||
}
|
||||
37
ServicesApi/src/navigator/events/validation-example.ts
Normal file
37
ServicesApi/src/navigator/events/validation-example.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { EventsSetterValidator } from './dtoValidator';
|
||||
|
||||
// Example JSON data to validate
|
||||
const jsonData = {
|
||||
collectionName: 'Events:Build-UUIDv4',
|
||||
data: {
|
||||
'USER-UUID(V4)': {
|
||||
'j0adQOsJBR0xq24dxLKdDU9EQRmt4gzE05CmhA': {
|
||||
'e6hewIe7YqbQZHO3': 'qt5P0xoeThjNT9EuWfwBgxsntHY5ydRtKFr1pgKGcgxx'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function validateEventData() {
|
||||
const eventData = plainToClass(EventsSetterValidator, jsonData);
|
||||
const errors = await validate(eventData, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
validationError: { target: false }
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log('Validation failed. Errors:', JSON.stringify(errors, null, 2));
|
||||
return false;
|
||||
} else {
|
||||
console.log('Validation successful!');
|
||||
console.log('Validated data:', JSON.stringify(eventData, null, 2));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
validateEventData()
|
||||
.then(isValid => console.log('Is valid:', isValid))
|
||||
.catch(err => console.error('Error during validation:', err));
|
||||
48
ServicesApi/src/navigator/menus/main.ts
Normal file
48
ServicesApi/src/navigator/menus/main.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { interfaceMenu, interfaceMapper, interfaceMenus } from "@/src/utils/types/menus";
|
||||
import { menuForEmployeeDefinition } from "./menuItems/employee";
|
||||
import { menuForOccupantDefinition } from "./menuItems/occupant";
|
||||
|
||||
const config = {
|
||||
FirstLayerColor: "#ebc334",
|
||||
SecondLayerColor: "#18910d",
|
||||
ThirdLayerColor: "#2825c4",
|
||||
employeePrefix: "/office",
|
||||
occupantPrefix: "/venue"
|
||||
}
|
||||
|
||||
function generateMapperKey(keys: string[]): string { return keys.join(':') + ':' }
|
||||
|
||||
function generateMapper(menu: interfaceMenu[], parentKeys: string[] = []): interfaceMapper {
|
||||
let mapper: interfaceMapper = {};
|
||||
for (const item of menu) {
|
||||
const currentKeys = [...parentKeys, item.key];
|
||||
if (item.page) { mapper[item.page] = generateMapperKey(currentKeys) }
|
||||
if (item.subs) { const subMapper = generateMapper(item.subs, currentKeys); mapper = { ...mapper, ...subMapper } }
|
||||
}
|
||||
return mapper;
|
||||
}
|
||||
|
||||
function generateDynamicMapper(menus: interfaceMenu[]): interfaceMapper { return generateMapper(menus) }
|
||||
|
||||
function applyColorsAndPrefixes(menu: interfaceMenu[], isEmployee: boolean, config: any, layer: number = 1): interfaceMenu[] {
|
||||
return menu.map(item => {
|
||||
const newItem = { ...item };
|
||||
if (layer === 1) newItem.color = config.FirstLayerColor;
|
||||
else if (layer === 2) newItem.color = config.SecondLayerColor;
|
||||
else if (layer >= 3) newItem.color = config.ThirdLayerColor;
|
||||
if (newItem.page) { newItem.page = `${isEmployee ? config.employeePrefix : config.occupantPrefix}${newItem.page}` }
|
||||
if (newItem.subs) { newItem.subs = applyColorsAndPrefixes(newItem.subs, isEmployee, layer + 1) }
|
||||
return newItem;
|
||||
});
|
||||
}
|
||||
|
||||
export const occupantMenus: interfaceMenus = {
|
||||
Menu: applyColorsAndPrefixes(menuForOccupantDefinition, false, config),
|
||||
Mapper: generateDynamicMapper(applyColorsAndPrefixes(menuForOccupantDefinition, false, config))
|
||||
};
|
||||
|
||||
export const employeeMenus: interfaceMenus = {
|
||||
Menu: applyColorsAndPrefixes(menuForEmployeeDefinition, true, config),
|
||||
Mapper: generateDynamicMapper(applyColorsAndPrefixes(menuForEmployeeDefinition, true, config))
|
||||
};
|
||||
|
||||
18
ServicesApi/src/navigator/menus/menu.service.spec.ts
Normal file
18
ServicesApi/src/navigator/menus/menu.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ServicesService } from './menu.service';
|
||||
|
||||
describe('ServicesService', () => {
|
||||
let service: ServicesService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ServicesService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ServicesService>(ServicesService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
56
ServicesApi/src/navigator/menus/menu.service.ts
Normal file
56
ServicesApi/src/navigator/menus/menu.service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { employeeMenus, occupantMenus } from "./main";
|
||||
import { interfaceMenu } from "@/src/utils/types/menus";
|
||||
|
||||
// backend_service | Occupant pages
|
||||
// backend_service | {
|
||||
// backend_service | IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE: 'IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE:hES1KfaPRZeadmmjdryShA'
|
||||
// backend_service | }
|
||||
// backend_service | Occupant Menu Structure
|
||||
// backend_service | Occupant pages
|
||||
// backend_service | [
|
||||
// backend_service | {
|
||||
// backend_service | key: 'dzFGPzZJRgmft4HrrTeBtQ',
|
||||
// backend_service | icon: '',
|
||||
// backend_service | text: { tr: 'Pano', en: 'Dashboard' },
|
||||
// backend_service | page: '/venue/dashboard',
|
||||
// backend_service | token: 'IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE',
|
||||
// backend_service | color: '#ebc334',
|
||||
// backend_service | subs: []
|
||||
// backend_service | }
|
||||
// backend_service | ]
|
||||
// backend_service | [
|
||||
// backend_service | {
|
||||
// backend_service | key: 'dzFGPzZJRgmft4HrrTeBtQ',
|
||||
// backend_service | icon: '',
|
||||
// backend_service | text: { tr: 'Pano', en: 'Dashboard' },
|
||||
// backend_service | page: '/venue/dashboard',
|
||||
// backend_service | token: 'IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE',
|
||||
// backend_service | color: '#ebc334',
|
||||
// backend_service | subs: []
|
||||
// backend_service | }
|
||||
// backend_service | ]
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class MenusService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
async renderOccupantMenu(pages: any) {
|
||||
const defaultMenu: interfaceMenu[] = occupantMenus.Menu;
|
||||
const userHasUrls = Object.keys(pages)
|
||||
const renderedMenu: interfaceMenu[] = [];
|
||||
defaultMenu.map((value) => { if (value.token && userHasUrls.includes(value.token)) { renderedMenu.push(value as interfaceMenu) } });
|
||||
return renderedMenu || [];
|
||||
}
|
||||
|
||||
async renderEmployeeMenu(pages: Record<string, string>) {
|
||||
const defaultMenu = employeeMenus.Menu;
|
||||
console.log('Employee pages');
|
||||
console.dir(pages);
|
||||
console.log('Employee Menu Structure');
|
||||
console.dir(defaultMenu);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
301
ServicesApi/src/navigator/menus/menuItems/employee.ts
Normal file
301
ServicesApi/src/navigator/menus/menuItems/employee.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
export const menuForEmployeeDefinition = [
|
||||
{
|
||||
key: "a6EoBlTPSgGbUQELbyRwMA",
|
||||
icon: "",
|
||||
text: { "tr": "Dashboard", "en": "Dashboard" },
|
||||
page: "/dashboard",
|
||||
token: "qY56XMEr08wJkNvOR6EYQZKMVdTQEfHdLXGzzxcKU24E",
|
||||
color: "",
|
||||
subs: null,
|
||||
},
|
||||
{
|
||||
key: "NV2kI8NERmqrNgIeiUYojQ",
|
||||
icon: "",
|
||||
text: { "tr": "Bireysel", "en": "Individual" },
|
||||
page: null,
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "xnhFAyi3Sp2qVWcVcR6m9w",
|
||||
icon: "",
|
||||
text: { "tr": "Birey", "en": "Person" },
|
||||
page: "/person",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "7wdsqwCQSmXRsRPC9GSgwx",
|
||||
icon: "",
|
||||
text: { "tr": "Oluştur", "en": "Create" },
|
||||
page: "/person/create",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "56O8WRP4TyC7F8bc1vjXgx",
|
||||
icon: "",
|
||||
text: { "tr": "Güncelle", "en": "Update" },
|
||||
page: "/person/update",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "RPaESp64SUmjNyEY1WUE8Q",
|
||||
icon: "",
|
||||
text: { "tr": "Sil", "en": "Delete" },
|
||||
page: "/person/delete",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "qcRK3EPQSoLSWkJFhtWOwx",
|
||||
icon: "",
|
||||
text: { "tr": "Kullanıcı", "en": "User" },
|
||||
page: "/users",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "PqNGe0SaQKeyUGyzJoSLwx",
|
||||
icon: "",
|
||||
text: { "tr": "Oluştur", "en": "Create" },
|
||||
page: "/users/create",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "ruvQlE7wQzqHqUvCNIoUnA",
|
||||
icon: "",
|
||||
text: { "tr": "Güncelle", "en": "Update" },
|
||||
page: "/users/update",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "DfDStf1dTBCRShNQeb5pZA",
|
||||
icon: "",
|
||||
text: { "tr": "Sil", "en": "Delete" },
|
||||
page: "/users/delete",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "ALV19bQ8S7q8LpOkdRDMwx",
|
||||
icon: "",
|
||||
text: { "tr": "Bina", "en": "Build" },
|
||||
page: null,
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "eToBYS4DTEKseVYMJLNZwx",
|
||||
icon: "",
|
||||
text: { "tr": "Binalar", "en": "Building" },
|
||||
page: null,
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "EkR7p6qmRN2Wb1GLsH5aEQ",
|
||||
icon: "",
|
||||
text: { "tr": "Oluştur", "en": "Create" },
|
||||
page: "/building/build/create",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "qcoHwABjSli04D7xeWGOHQ",
|
||||
icon: "",
|
||||
text: { "tr": "Güncelle", "en": "Update" },
|
||||
page: "/building/build/update",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "vC2oPkjRfudvBDlNReeRAx",
|
||||
icon: "",
|
||||
text: { "tr": "Sil", "en": "Delete" },
|
||||
page: "/building/build/delete",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "NFte61RnTHGPWlnoUItHAx",
|
||||
icon: "",
|
||||
text: { "tr": "Daireler", "en": "Parts" },
|
||||
page: null,
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "7o6QNpelSpmxpJxTedEj4w",
|
||||
icon: "",
|
||||
text: { "tr": "Oluştur", "en": "Create" },
|
||||
page: "/building/parts/create",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "rP6idRkyToLcxwpalCxgxx",
|
||||
icon: "OBKPalaMQwWhQmQ9Ni0y6Q",
|
||||
text: { "tr": "Güncelle", "en": "Update" },
|
||||
page: "/building/parts/update",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
},
|
||||
{
|
||||
key: "CBNaWzVqRaSpWaPTM54PbA",
|
||||
icon: "",
|
||||
text: { "tr": "Sil", "en": "Delete" },
|
||||
page: "/building/parts/delete",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "NFte61RnTHGPWlnoUItHAx",
|
||||
icon: "",
|
||||
text: { "tr": "Alanlar", "en": "Area" },
|
||||
page: null,
|
||||
token: null,
|
||||
color: "",
|
||||
subs: []
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "yzvyvqMhQ06TdC9paOw4Ax",
|
||||
icon: "",
|
||||
text: { "tr": "Yönetim", "en": "Management" },
|
||||
page: null,
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "DEumSZtaTSKiDsD1VJPQxx",
|
||||
icon: "",
|
||||
text: { "tr": "Bütçe", "en": "Budget" },
|
||||
page: "/management/budget",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "PIPD61aZRveFZ6GGfK3VYw",
|
||||
icon: "",
|
||||
text: { "tr": "Eylemler", "en": "Actions" },
|
||||
page: "/management/budget/actions",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null,
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
icon: "",
|
||||
text: { "tr": "Durum", "en": "Status" },
|
||||
page: "/management/budget/status",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null,
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "RHI0bthYRjWWf4tBaPBdgx",
|
||||
icon: "",
|
||||
text: { "tr": "Toplantılar", "en": "Meetings" },
|
||||
page: "/meetings",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "OESxDOI6S4eNcdeRCrKIjQ",
|
||||
icon: "",
|
||||
text: { "tr": "Yıllık", "en": "Annual" },
|
||||
page: "/meetings/annual",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "MhEHidsRWyHdCqtHJOcvAx",
|
||||
icon: "",
|
||||
text: { "tr": "Oluştur", "en": "Create" },
|
||||
page: "/meetings/annual/create",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null,
|
||||
},
|
||||
{
|
||||
key: "xhnSW4hWSDuJyREMjXOivA",
|
||||
icon: "",
|
||||
text: { "tr": "Kapat", "en": "Close" },
|
||||
page: "/meetings/annual/close",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "A4raUDNFTpZ7mPfqJBGSwx",
|
||||
icon: "",
|
||||
text: { "tr": "Acil", "en": "Emergency" },
|
||||
page: "/meetings/emergency",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [
|
||||
{
|
||||
key: "T3Fd0C5Tf2V1dZhiZuNQxx",
|
||||
icon: "",
|
||||
text: { "tr": "Oluştur", "en": "Create" },
|
||||
page: "/meetings/emergency/create",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null,
|
||||
},
|
||||
{
|
||||
key: "L1ogOYhSl6BDPstufiSwxx",
|
||||
icon: "",
|
||||
text: { "tr": "Kapat", "en": "Close" },
|
||||
page: "/meetings/emergency/close",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "vwzmxtBoQFW62YHes5OZAg",
|
||||
icon: "",
|
||||
text: { "tr": "Katılımlar", "en": "Participations" },
|
||||
page: "/meetings/participations",
|
||||
token: null,
|
||||
color: "",
|
||||
subs: [],
|
||||
}
|
||||
],
|
||||
}
|
||||
];
|
||||
13
ServicesApi/src/navigator/menus/menuItems/occupant.ts
Normal file
13
ServicesApi/src/navigator/menus/menuItems/occupant.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { interfaceMenu } from "@/src/utils/types/menus";
|
||||
|
||||
export const menuForOccupantDefinition: interfaceMenu[] = [
|
||||
{
|
||||
key: "dzFGPzZJRgmft4HrrTeBtQ",
|
||||
icon: "",
|
||||
text: { "tr": "Pano", "en": "Dashboard" },
|
||||
page: "/dashboard",
|
||||
token: "IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE",
|
||||
color: "",
|
||||
subs: [],
|
||||
},
|
||||
]
|
||||
13
ServicesApi/src/navigator/menus/test.ts
Normal file
13
ServicesApi/src/navigator/menus/test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { employeeMenus, occupantMenus } from './main';
|
||||
|
||||
console.log('Employee Menu Mapper:');
|
||||
console.log(JSON.stringify(employeeMenus.Mapper, null, 2));
|
||||
|
||||
console.log('\nEmployee Menu Structure (with colors):');
|
||||
console.log(JSON.stringify(employeeMenus.Menu, null, 2));
|
||||
|
||||
console.log('\nOccupant Menu Mapper:');
|
||||
console.log(JSON.stringify(occupantMenus.Mapper, null, 2));
|
||||
|
||||
console.log('\nOccupant Menu Structure (with colors):');
|
||||
console.log(JSON.stringify(occupantMenus.Menu, null, 2));
|
||||
18
ServicesApi/src/navigator/navigator.controller.spec.ts
Normal file
18
ServicesApi/src/navigator/navigator.controller.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NavigatorController } from './navigator.controller';
|
||||
|
||||
describe('NavigatorController', () => {
|
||||
let controller: NavigatorController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [NavigatorController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<NavigatorController>(NavigatorController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
73
ServicesApi/src/navigator/navigator.controller.ts
Normal file
73
ServicesApi/src/navigator/navigator.controller.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Controller, Post, NotFoundException, Body } from '@nestjs/common';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { EventsService } from '@/src/navigator/events/events.service';
|
||||
import { MongoService } from '@/src/database/mongo/mongo.service';
|
||||
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
|
||||
import { eventSetValidator, eventGetValidator } from './dtoValidator';
|
||||
import { PagesService } from './pages/pages.service';
|
||||
|
||||
const tokens = { employeeTypeToken: "L9wBdwV9OlxsLAgh", occupantTypeToken: "j0adQOsJBR0xq24d" }
|
||||
|
||||
@Controller('navigator')
|
||||
export class NavigatorController {
|
||||
constructor(
|
||||
private prismaService: PrismaService,
|
||||
private eventService: EventsService,
|
||||
private mongoService: MongoService,
|
||||
private urlHandler: UrlHandler,
|
||||
private pagesService: PagesService
|
||||
) { }
|
||||
|
||||
@Post('event/set')
|
||||
async setEvent(@Body() body: eventSetValidator) {
|
||||
const user = await this.prismaService.users.findFirst({ where: { uu_id: body.usersUUID }, include: { people: true } });
|
||||
if (!user) { throw new NotFoundException('User not found') }
|
||||
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: body.event.token } })
|
||||
const person = user.people[0]
|
||||
const people2userType = await this.prismaService.employees.findFirstOrThrow({ where: { uu_id: person.uu_id, staff: { user_type_uu_id: userType.uu_id } } })
|
||||
if (!people2userType) { throw new NotFoundException('User type not found') }
|
||||
if (userType.type_token == tokens.employeeTypeToken) { await this.eventService.setEventsEmployees({ event: body.event, userUUID: body.usersUUID }) }
|
||||
else if (userType.type_token == tokens.occupantTypeToken) { await this.eventService.setEventsOccupants({ event: body.event, userUUID: body.usersUUID }) }
|
||||
else { throw new NotFoundException('User type not found') }
|
||||
return body.event;
|
||||
}
|
||||
|
||||
@Post('event/get')
|
||||
async getEvent(@Body() body: eventGetValidator) {
|
||||
const { typeToken, usersUUID } = body
|
||||
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: typeToken } })
|
||||
if (userType.type_token == tokens.employeeTypeToken) {
|
||||
const allEvents = await this.eventService.getAllEventsEmployees(typeToken);
|
||||
if (!allEvents) { throw new NotFoundException('Events not found') }
|
||||
const selectedEvents = await this.eventService.getEventsEmployees(usersUUID);
|
||||
const selectedEventsKeys = Object.values(selectedEvents || {}).map((value: any) => value.key) || [];
|
||||
for (const event of allEvents) { if (selectedEventsKeys.includes(event.key)) { event.isSelected = true } else { event.isSelected = false } }
|
||||
return { events: allEvents }
|
||||
}
|
||||
else if (userType.type_token == tokens.occupantTypeToken) {
|
||||
const allEvents = await this.eventService.getAllEventsOccupants(typeToken);
|
||||
if (!allEvents) { throw new NotFoundException('Events not found') }
|
||||
const selectedEvents = await this.eventService.getEventsOccupants(usersUUID);
|
||||
const selectedEventsKeys = Object.values(selectedEvents || {}).map((value: any) => value.key) || [];
|
||||
for (const event of allEvents) { if (selectedEventsKeys.includes(event.key)) { event.isSelected = true } else { event.isSelected = false } }
|
||||
return { events: allEvents }
|
||||
} else { throw new NotFoundException('User type not found') }
|
||||
}
|
||||
|
||||
@Post('page/set')
|
||||
async setPage(@Body() body: { usersUUID: string, usersToken: string, url: string, page: Record<string, any> }) {
|
||||
return await this.pagesService.setPageViaToken(body.usersUUID, body.usersToken, body.url, body.page)
|
||||
}
|
||||
|
||||
@Post('page/get')
|
||||
async getPage(@Body() body: { usersUUID: string, token: string, url?: string, skip?: number, limit?: number }) {
|
||||
const pages = await this.pagesService.getPageViaToken(body.usersUUID, body.token, body.url, body.skip, body.limit)
|
||||
return { pages }
|
||||
}
|
||||
|
||||
@Post('page/configure')
|
||||
async setPages(@Body() body: { chunkIndex: number; chunkCount: number; data: Record<string, any> }) {
|
||||
return await this.pagesService.configurePages(body.data, body.chunkIndex, body.chunkCount)
|
||||
}
|
||||
|
||||
}
|
||||
18
ServicesApi/src/navigator/navigator.module.ts
Normal file
18
ServicesApi/src/navigator/navigator.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongoModule } from '@/src/database/mongo/mongo.module';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { MenusService } from '@/src/navigator/menus/menu.service';
|
||||
import { NavigatorController } from '@/src/navigator/navigator.controller';
|
||||
import { EventsService } from '@/src/navigator/events/events.service';
|
||||
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
|
||||
import { PagesService } from '@/src/navigator/pages/pages.service';
|
||||
|
||||
@Module({
|
||||
controllers: [NavigatorController],
|
||||
imports: [MongoModule],
|
||||
providers: [MenusService, EventsService, PagesService, PrismaService, UrlHandler],
|
||||
exports: [MenusService, EventsService, PagesService, PrismaService]
|
||||
})
|
||||
export class NavigatorModule {
|
||||
constructor() { }
|
||||
}
|
||||
18
ServicesApi/src/navigator/pages/pages.service.spec.ts
Normal file
18
ServicesApi/src/navigator/pages/pages.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PagesService } from './pages.service';
|
||||
|
||||
describe('PagesService', () => {
|
||||
let service: PagesService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PagesService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PagesService>(PagesService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
172
ServicesApi/src/navigator/pages/pages.service.ts
Normal file
172
ServicesApi/src/navigator/pages/pages.service.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { MongoService } from '@/src/database/mongo/mongo.service';
|
||||
import { UrlHandler } from '@/src/utils/navigator/urlHandler';
|
||||
|
||||
@Injectable()
|
||||
export class PagesService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly mongoService: MongoService,
|
||||
private readonly urlHandler: UrlHandler
|
||||
) { }
|
||||
|
||||
tokens = { employeeTypeToken: "L9wBdwV9OlxsLAgh", occupantTypeToken: "j0adQOsJBR0xq24d" }
|
||||
|
||||
private async saveChunkToDB(data: Record<string, any>, chunkIndex: number) {
|
||||
await this.mongoService.set("Pages");
|
||||
if (chunkIndex == 1) { await this.mongoService.deleteMany({}) }
|
||||
await this.mongoService.createMany(Object.values(data));
|
||||
}
|
||||
|
||||
async getPagesOccupants(userUUID: string, usersToken: string) {
|
||||
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: userUUID }, include: { people: true } });
|
||||
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: usersToken } })
|
||||
if (userType.type_token == this.tokens.occupantTypeToken) {
|
||||
const person = user.people
|
||||
const livingSpace = await this.prismaService.build_living_space.findFirstOrThrow({
|
||||
where: { person_id: person.id, occupant_types: { user_types: { id: userType.id } } },
|
||||
select: { build_parts_id: true }
|
||||
})
|
||||
const buildUUID = await this.prismaService.build.findFirstOrThrow({
|
||||
where: { build_parts: { some: { id: livingSpace.build_parts_id } } }, select: { uu_id: true }
|
||||
})
|
||||
this.mongoService.set(`Pages/${buildUUID.uu_id}`);
|
||||
console.log('usersToken', usersToken)
|
||||
console.log('user.uu_id', user.uu_id)
|
||||
const userPage = await this.mongoService.findOne({ [user.uu_id]: { $exists: true } })
|
||||
console.log('userPage', userPage)
|
||||
if (!userPage) { throw new NotFoundException('Users slot not found') }
|
||||
const userPageSlot = userPage[user.uu_id][usersToken]
|
||||
if (!userPageSlot) { throw new NotFoundException('Users slot not found') }
|
||||
console.log('userPageSlot', userPageSlot)
|
||||
const writeObject = {}
|
||||
for (const [key, value] of Object.entries(userPageSlot)) {
|
||||
writeObject[key] = `${key}:${value}`
|
||||
}
|
||||
console.log('writeObject', writeObject)
|
||||
return writeObject
|
||||
}
|
||||
else { throw new NotFoundException('User type not found') }
|
||||
}
|
||||
|
||||
async getPagesEmployee(userUUID: string, usersToken: string) {
|
||||
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: userUUID }, include: { people: true } });
|
||||
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: usersToken } })
|
||||
if (userType.type_token == this.tokens.employeeTypeToken) {
|
||||
const person = user.people[0]
|
||||
const employee = await this.prismaService.employees.findFirstOrThrow({ where: { people_id: person.id, staff: { user_type_id: userType.id } } })
|
||||
const companyUUID = await this.prismaService.companies.findFirstOrThrow({
|
||||
where: { departments: { some: { duties: { some: { staff: { some: { uu_id: employee.staff_uu_id } } } } } } }, select: { uu_id: true }
|
||||
})
|
||||
this.mongoService.set(`Pages/${companyUUID.uu_id}`);
|
||||
const userPage = await this.mongoService.findOne({ [user.uu_id]: { [usersToken]: { $exists: true } } })
|
||||
if (!userPage) { throw new NotFoundException('Users slot not found') }
|
||||
return userPage
|
||||
}
|
||||
else { throw new NotFoundException('User type not found') }
|
||||
}
|
||||
|
||||
async setPageViaToken(userUUID: string, usersToken: string, url: string, page: Record<string, any>) {
|
||||
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: userUUID }, include: { people: true } });
|
||||
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: usersToken } })
|
||||
const urlToken = await this.urlHandler.getSecureUrlToken(url)
|
||||
if (userType.type_token == this.tokens.employeeTypeToken) {
|
||||
const person = user.people[0]
|
||||
const employee = await this.prismaService.employees.findFirstOrThrow({ where: { people_id: person.id, staff: { user_type_id: userType.id } } })
|
||||
const companyUUID = await this.prismaService.companies.findFirstOrThrow({
|
||||
where: { departments: { some: { duties: { some: { staff: { some: { uu_id: employee.staff_uu_id } } } } } } }, select: { uu_id: true }
|
||||
})
|
||||
this.mongoService.set(`Pages/${companyUUID.uu_id}`);
|
||||
const userPage = await this.mongoService.findOne({ [employee.uu_id]: { $exists: true } });
|
||||
if (!userPage) {
|
||||
console.log('urlToken', urlToken)
|
||||
} else { console.log('urlToken', urlToken) }
|
||||
} else if (userType.type_token == this.tokens.occupantTypeToken) {
|
||||
const person = user.people
|
||||
const livingSpace = await this.prismaService.build_living_space.findFirstOrThrow({
|
||||
where: { person_id: person.id, occupant_types: { user_types: { id: userType.id } } },
|
||||
select: { uu_id: true, build_parts_id: true }
|
||||
})
|
||||
const buildUUID = await this.prismaService.build.findFirstOrThrow({
|
||||
where: { build_parts: { some: { id: livingSpace.build_parts_id } } }, select: { uu_id: true }
|
||||
})
|
||||
this.mongoService.set(`Pages/${buildUUID.uu_id}`);
|
||||
const userPage = await this.mongoService.findOne({ [user.uu_id]: { $exists: true } });
|
||||
if (!userPage) {
|
||||
const newUserPageSlot = await this.mongoService.create({ [user.uu_id]: { [`${usersToken}`]: { [`${urlToken}`]: `${page.key}` } } })
|
||||
return newUserPageSlot
|
||||
} else {
|
||||
const updatedUserPageSlot = await this.mongoService.updateOne(userPage._id, { [`${user.uu_id}.${usersToken}.${urlToken}`]: `${page.key}` })
|
||||
return updatedUserPageSlot ? { status: "success", data: updatedUserPageSlot } : { status: "error", data: null }
|
||||
}
|
||||
// console.log('urlToken', { [user.uu_id]: { [`${body.usersToken}`]: { [`${urlToken}`]: `${body.page.key}` } } })
|
||||
}
|
||||
else { throw new NotFoundException('User type not found') }
|
||||
}
|
||||
|
||||
async getPageViaToken(usersUUID: string, token: string, url?: string, skip?: number, limit?: number) {
|
||||
this.mongoService.set("Pages");
|
||||
const addUrlQuery = url ? { url: url } : {};
|
||||
const user = await this.prismaService.users.findFirstOrThrow({ where: { uu_id: usersUUID }, include: { people: true } });
|
||||
const userType = await this.prismaService.user_types.findFirstOrThrow({ where: { token: token } })
|
||||
if (userType.type_token == this.tokens.employeeTypeToken) {
|
||||
const person = user.people[0]
|
||||
const pages = await this.mongoService.findMany({
|
||||
$and: [
|
||||
{ $or: [{ includeTokens: { $in: ['*'] } }, { includeTokens: { $in: [token] } }] },
|
||||
{ $nor: [{ excludeTokens: { $in: ['*'] } }, { excludeTokens: { $in: [token] } }] },
|
||||
addUrlQuery,
|
||||
{ typeToken: this.tokens.employeeTypeToken },
|
||||
],
|
||||
}, limit || 50, skip || 0, ['url'], ['asc'])
|
||||
if (!pages) { throw new NotFoundException(`Pages not found. User type: ${userType.type_token}`) }
|
||||
const employee = await this.prismaService.employees.findFirstOrThrow({
|
||||
where: { people_id: person.id, staff: { user_type_id: userType.id } },
|
||||
select: { uu_id: true, staff_uu_id: true }
|
||||
})
|
||||
const companyUUID = await this.prismaService.companies.findFirstOrThrow({
|
||||
where: { departments: { some: { duties: { some: { staff: { some: { uu_id: employee.staff_uu_id } } } } } } }, select: { uu_id: true }
|
||||
})
|
||||
this.mongoService.set(`Pages/${companyUUID.uu_id}`);
|
||||
const usersPages = await this.mongoService.findMany({ [employee.uu_id]: { $exists: true } });
|
||||
return pages;
|
||||
}
|
||||
else if (userType.type_token == this.tokens.occupantTypeToken) {
|
||||
const person = user.people
|
||||
const pages = await this.mongoService.findMany({
|
||||
$and: [
|
||||
{ $or: [{ includeTokens: { $in: ['*'] } }, { includeTokens: { $in: [token] } }] },
|
||||
{ $nor: [{ excludeTokens: { $in: ['*'] } }, { excludeTokens: { $in: [token] } }] },
|
||||
addUrlQuery,
|
||||
{ typeToken: this.tokens.occupantTypeToken },
|
||||
],
|
||||
}, limit || 50, skip || 0, ['url'], ['asc'])
|
||||
console.log('pages', pages)
|
||||
if (!pages) { throw new NotFoundException(`Pages not found. User type: ${userType.type_token}`) }
|
||||
const livingSpace = await this.prismaService.build_living_space.findFirstOrThrow({
|
||||
where: { person_id: person.id, occupant_types: { user_types: { id: userType.id } } },
|
||||
select: { uu_id: true, build_parts_id: true }
|
||||
})
|
||||
console.log('livingSpace', livingSpace)
|
||||
const buildUUID = await this.prismaService.build.findFirstOrThrow({
|
||||
where: { build_parts: { some: { id: livingSpace.build_parts_id } } }, select: { uu_id: true }
|
||||
})
|
||||
this.mongoService.set(`Pages/${buildUUID.uu_id}`);
|
||||
const usersPages = await this.mongoService.findMany({ [livingSpace.uu_id]: { $exists: true } });
|
||||
console.log('usersPages', usersPages)
|
||||
return Object.entries(pages).map(([key, value]: [string, any]) => {
|
||||
if (usersPages.some((page: any) => page[key])) { value.isSelected = true } else { value.isSelected = false }
|
||||
return value;
|
||||
})
|
||||
}
|
||||
else { throw new NotFoundException('User type not found') }
|
||||
}
|
||||
|
||||
async configurePages(data: Record<string, any>, chunkIndex: number, chunkCount: number) {
|
||||
const count = Object.keys(data).length;
|
||||
console.log(`🧩 Chunk [${chunkIndex}/${chunkCount}] alındı. Kayıt sayısı: ${count}`);
|
||||
await this.saveChunkToDB(data, chunkIndex);
|
||||
return { message: 'Chunk işlendi', count };
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import { uuid, z } from 'zod';
|
||||
|
||||
// ENUM
|
||||
export const UserType = {
|
||||
@@ -9,9 +9,8 @@ export type UserType = (typeof UserType)[keyof typeof UserType];
|
||||
|
||||
// Credentials
|
||||
export const CredentialsSchema = z.object({
|
||||
person_uu_id: z.string(),
|
||||
person_name: z.string(),
|
||||
full_name: z.string(),
|
||||
uuid: z.string(),
|
||||
fullName: z.string(),
|
||||
});
|
||||
export type Credentials = z.infer<typeof CredentialsSchema>;
|
||||
|
||||
@@ -102,6 +101,7 @@ export const AuthTokenSchema = z.object({
|
||||
export type AuthToken = z.infer<typeof AuthTokenSchema>;
|
||||
|
||||
export const EmployeeTokenSchema = z.object({
|
||||
uuid: z.string(),
|
||||
company: z.object({
|
||||
// id: z.number(),
|
||||
uu_id: z.string(),
|
||||
@@ -219,7 +219,7 @@ export const EmployeeTokenSchema = z.object({
|
||||
staff_code: z.string(),
|
||||
// duties_id: z.number(),
|
||||
duties_uu_id: z.string(),
|
||||
function_retriever: z.string().nullable(),
|
||||
// function_retriever: z.string().nullable(),
|
||||
// ref_id: z.string().nullable(),
|
||||
// replication_id: z.number(),
|
||||
// cryp_uu_id: z.string().nullable(),
|
||||
@@ -240,30 +240,40 @@ export const EmployeeTokenSchema = z.object({
|
||||
|
||||
menu: z.array(z.object({})).nullable(),
|
||||
pages: z.array(z.string()).nullable(),
|
||||
events: z.record(z.string(), z.string()).nullable(),
|
||||
|
||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
||||
typeToken: z.string(),
|
||||
functionsRetriever: z.string(),
|
||||
kind: z.literal(UserType.employee),
|
||||
});
|
||||
|
||||
export const OccupantTokenSchema = z.object({
|
||||
uuid: z.string(),
|
||||
livingSpace: z.object({}),
|
||||
occupant: z.object({}),
|
||||
build: z.object({}),
|
||||
part: z.object({}),
|
||||
company: z.object({}).optional(),
|
||||
|
||||
menu: z.array(z.object({})).nullable(),
|
||||
pages: z.array(z.string()).nullable(),
|
||||
menu: z.array(z.object({
|
||||
key: z.string(),
|
||||
icon: z.string(),
|
||||
text: z.object({ tr: z.string(), en: z.string() }),
|
||||
page: z.string().nullable(),
|
||||
token: z.string().nullable(),
|
||||
color: z.string(),
|
||||
subs: z.array(z.lazy(() => OccupantTokenSchema.shape.menu.element)).nullable()
|
||||
})).nullable(),
|
||||
pages: z.record(z.string(), z.string()).nullable(),
|
||||
events: z.record(z.string(), z.string()).nullable(),
|
||||
|
||||
selection: z.record(z.string(), z.unknown()).nullable(),
|
||||
typeToken: z.string(),
|
||||
functionsRetriever: z.string(),
|
||||
kind: z.literal(UserType.occupant),
|
||||
});
|
||||
|
||||
export const TokenDictTypes = z.discriminatedUnion('kind', [
|
||||
EmployeeTokenSchema,
|
||||
OccupantTokenSchema,
|
||||
]);
|
||||
export const TokenDictTypes = z.discriminatedUnion('kind', [EmployeeTokenSchema, OccupantTokenSchema]);
|
||||
|
||||
export type TokenDictInterface = z.infer<typeof TokenDictTypes>;
|
||||
|
||||
@@ -2,13 +2,14 @@ import { Module } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { UsersController } from './users.controller';
|
||||
import { PrismaModule } from '@/prisma/prisma.module';
|
||||
import { CacheService } from '../cache.service';
|
||||
import { CacheService } from '../database/redis/redis.service';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { RedisModule } from '../database/redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, UtilsModule],
|
||||
providers: [UsersService, CacheService],
|
||||
imports: [PrismaModule, UtilsModule, RedisModule],
|
||||
providers: [UsersService],
|
||||
controllers: [UsersController],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
export class UsersModule { }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { Prisma, users } from '@prisma/client';
|
||||
import { CacheService } from '../cache.service';
|
||||
import { CacheService } from '../database/redis/redis.service';
|
||||
import { PaginationHelper, PaginationInfo } from '../utils/pagination-helper';
|
||||
|
||||
@Injectable()
|
||||
@@ -10,7 +10,7 @@ export class UsersService {
|
||||
private prisma: PrismaService,
|
||||
private cacheService: CacheService,
|
||||
private paginationHelper: PaginationHelper,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async findAll(filter: any): Promise<Partial<users>[]> {
|
||||
return this.prisma.users.findMany({
|
||||
|
||||
63
ServicesApi/src/utils/navigator/navigator.ts
Normal file
63
ServicesApi/src/utils/navigator/navigator.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ForbiddenException, MisdirectedException, Injectable } from "@nestjs/common";
|
||||
import { RedisHandlers } from "../store/redisHandlers";
|
||||
|
||||
@Injectable()
|
||||
export class Navigator {
|
||||
|
||||
constructor(private redisHandler: RedisHandlers) { }
|
||||
|
||||
async getAllInfos(mainService: any) {
|
||||
const mainServiceMapper = mainService?.mapper
|
||||
if (!mainServiceMapper) { throw new ForbiddenException(`Mapper in ${mainService.constructor.name} is missing or null`) }
|
||||
const allInfos = Object.entries(mainServiceMapper).map(([key, value]) => {
|
||||
return {
|
||||
key,
|
||||
value
|
||||
}
|
||||
})
|
||||
return allInfos
|
||||
}
|
||||
|
||||
async getInfos(mainService: any, userToken: string) {
|
||||
// Get asked service by userToken
|
||||
const mainServiceMapper = mainService?.mapper
|
||||
if (!mainServiceMapper) { throw new ForbiddenException(`Mapper in ${mainService.constructor.name} is missing or null`) }
|
||||
// Get related events from mainServiceMapper by userToken
|
||||
const relatedService = mainServiceMapper?.[userToken]
|
||||
if (!relatedService) { throw new MisdirectedException(`No service found for drive token: ${userToken}`) }
|
||||
// Call event infos from relatedService
|
||||
return await relatedService.infoEvents(userToken);
|
||||
}
|
||||
|
||||
async getService(request: any, mapper: any) {
|
||||
// Get request drive token from acess control guard and retrieve related Service
|
||||
const driveToken = request.driveToken
|
||||
if (!driveToken) { throw new Error('Drive token is missing or null') }
|
||||
// Get second part of drive token which is user type token
|
||||
const secondPartOfDriveToken = driveToken.split(":")[1]
|
||||
if (!secondPartOfDriveToken) { throw new Error('Drive token is missing or null') }
|
||||
// Get related service from mapper which function maps registered events to functions
|
||||
return mapper[secondPartOfDriveToken];
|
||||
}
|
||||
|
||||
async getFunction(request: any, mapper: any, query: any) {
|
||||
const relatedService = await this.getService(request, mapper)
|
||||
if (!relatedService) { throw new Error(`No service found for drive token: ${request.driveToken}`) }
|
||||
try {
|
||||
// Get function mapper from related service
|
||||
if (!relatedService.mapper) { throw new Error(`Mapper in ${relatedService.constructor.name} is missing or null`) }
|
||||
// Get redis select token object from redis
|
||||
const selectObject = await this.redisHandler.getSelectFromRedis(request);
|
||||
if (!selectObject) { throw new Error(`Select object is missing or null`) }
|
||||
if (!selectObject.value.events) { throw new Error(`Events in select object is missing or null`) }
|
||||
const eventKey = Object.entries(selectObject.value.events).filter((key) => key.includes(request.driveToken))[0]
|
||||
if (!eventKey) { throw new Error(`No event is registered for this user ${request.driveToken}`) }
|
||||
// Get function to call from related service mapper
|
||||
const functionToCall = relatedService.mapper[eventKey.join(":")];
|
||||
if (!functionToCall || typeof functionToCall !== 'function') {
|
||||
throw new Error(`No function found for drive token: ${request.driveToken}`);
|
||||
}
|
||||
return await functionToCall(query);
|
||||
} catch (error) { throw new ForbiddenException(`This user is not allowed to access this endpoint. Please contact your system administrator.`) }
|
||||
}
|
||||
}
|
||||
67
ServicesApi/src/utils/navigator/urlHandler.ts
Normal file
67
ServicesApi/src/utils/navigator/urlHandler.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Events, Mapper } from "@/src/utils/types/url";
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
@Injectable()
|
||||
export class UrlHandler {
|
||||
private createSecureKeyWithoutLib(url: string): string {
|
||||
const subString = createHash('sha256').update(url).digest().toString('base64').substring(0, 48)
|
||||
return subString.replace(/=/g, 'E').replace(/-/g, 'M').replace(/_/g, 'N').replace(/\+/g, 'P').replace(/\//g, 'Q')
|
||||
}
|
||||
|
||||
async getSecureUrlToken(url: string): Promise<string> {
|
||||
return this.createSecureKeyWithoutLib(url);
|
||||
}
|
||||
|
||||
async getEvents(events: Events, mapper: Mapper) {
|
||||
for (const keyUrl of Object.keys(mapper)) {
|
||||
const splittedMapper = keyUrl.split(':')
|
||||
const eToken = splittedMapper[0]
|
||||
const token = splittedMapper[1]
|
||||
const key = splittedMapper[2]
|
||||
const eventKey = `${eToken}:${token}`
|
||||
|
||||
if (Object.keys(events).includes(eventKey)) {
|
||||
// Check if the event contains an item with the matching key
|
||||
const eventArray = events[eventKey]
|
||||
const foundEvent = eventArray.find(item => item.key === key)
|
||||
|
||||
if (!foundEvent) {
|
||||
throw new Error(`Event key ${key} not found in event ${eventKey}`)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Event ${eventKey} not found in events`)
|
||||
}
|
||||
}
|
||||
return mapper;
|
||||
}
|
||||
|
||||
async infoEvents(events: Events, urlRetriever: string | null = null, functionRetriever: string | null = null) {
|
||||
if (urlRetriever && !functionRetriever) {
|
||||
console.log("urlRetriever", urlRetriever)
|
||||
if (events[urlRetriever]) {
|
||||
return [[urlRetriever, events[urlRetriever]]];
|
||||
}
|
||||
return [];
|
||||
} else if (urlRetriever && functionRetriever) {
|
||||
if (events[urlRetriever]) {
|
||||
const eventItem = events[urlRetriever].find(item => item.key === functionRetriever);
|
||||
if (eventItem) {
|
||||
return [[urlRetriever, { [functionRetriever]: eventItem }]];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
} else if (!urlRetriever && functionRetriever) {
|
||||
const filteredEvents: [string, any][] = [];
|
||||
Object.entries(events).forEach(([url, eventArray]) => {
|
||||
const eventItem = eventArray.find(item => item.key === functionRetriever);
|
||||
if (eventItem) {
|
||||
filteredEvents.push([url, { [functionRetriever]: eventItem }]);
|
||||
}
|
||||
});
|
||||
return filteredEvents;
|
||||
} else {
|
||||
return Object.entries(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type ModelDelegate = {
|
||||
|
||||
@Injectable()
|
||||
export class PaginationHelper {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(private prisma: PrismaService) { }
|
||||
|
||||
/**
|
||||
* Sayfalama destekli sorgu yapar
|
||||
@@ -55,4 +55,11 @@ export class PaginationHelper {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async findWithPagination(
|
||||
query: any & { page?: number; pageSize?: number },
|
||||
service: ModelDelegate,
|
||||
): Promise<{ data: any[]; pagination: PaginationInfo }> {
|
||||
return this.paginate(service, query);
|
||||
}
|
||||
}
|
||||
|
||||
6
ServicesApi/src/utils/store/mongoHandler.ts
Normal file
6
ServicesApi/src/utils/store/mongoHandler.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class MongoHandler {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
TokenDictInterface,
|
||||
AuthToken,
|
||||
AuthTokenSchema,
|
||||
} from '@/src/types/auth/token';
|
||||
import { CacheService } from '@/src/cache.service';
|
||||
import { PasswordHandlers } from './login_handler';
|
||||
} from '../../types/auth/token';
|
||||
import { CacheService } from '../../database/redis/redis.service';
|
||||
import { PasswordHandlers } from './loginHandler';
|
||||
import { Injectable, ForbiddenException } from '@nestjs/common';
|
||||
|
||||
interface LoginFromRedis {
|
||||
@@ -25,7 +25,7 @@ export class RedisHandlers {
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly passwordService: PasswordHandlers,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Validates that a Redis key follows the expected format
|
||||
@@ -125,7 +125,7 @@ export class RedisHandlers {
|
||||
if (parts[1] === parts[2]) {
|
||||
const value = await this.cacheService.get(key);
|
||||
if (value) {
|
||||
return { key, value };
|
||||
return { key: value.key, value: value.value };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export class RedisHandlers {
|
||||
for (const key of keys) {
|
||||
const value = await this.cacheService.get(key);
|
||||
if (value) {
|
||||
return { key, value };
|
||||
return { key: value.key, value: value.value };
|
||||
}
|
||||
}
|
||||
throw new ForbiddenException(
|
||||
@@ -224,15 +224,15 @@ export class RedisHandlers {
|
||||
}
|
||||
|
||||
async renewTtlLoginFromRedis(req: Request): Promise<any> {
|
||||
const mergedKey = this.mergeLoginKey(req);
|
||||
const value = await this.cacheService.get(mergedKey);
|
||||
return this.cacheService.set_with_ttl(mergedKey, value, 86400);
|
||||
const loginToken = await this.getLoginFromRedis(req);
|
||||
if (!loginToken) { throw new ForbiddenException('Login token not found') }
|
||||
return this.cacheService.set_with_ttl(loginToken.key, loginToken.value, 60 * 30);
|
||||
}
|
||||
|
||||
async renewTtlSelectFromRedis(req: Request): Promise<any> {
|
||||
const mergedKey = this.mergeSelectKey(req);
|
||||
const value = await this.cacheService.get(mergedKey);
|
||||
return this.cacheService.set_with_ttl(mergedKey, value, 60 * 30);
|
||||
const selectToken = await this.getSelectFromRedis(req);
|
||||
if (!selectToken) { throw new ForbiddenException('Select token not found') }
|
||||
return this.cacheService.set_with_ttl(selectToken.key, selectToken.value, 60 * 30);
|
||||
}
|
||||
|
||||
async setLoginToRedis(token: AuthToken, userUUID: string): Promise<any> {
|
||||
18
ServicesApi/src/utils/types/menus.ts
Normal file
18
ServicesApi/src/utils/types/menus.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface interfaceMenu {
|
||||
key: string;
|
||||
icon: string;
|
||||
text: { tr: string, en: string };
|
||||
page: string | null;
|
||||
token: string | null;
|
||||
color: string;
|
||||
subs: interfaceMenu[] | null;
|
||||
}
|
||||
|
||||
export interface interfaceMapper {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface interfaceMenus {
|
||||
Menu: interfaceMenu[];
|
||||
Mapper: interfaceMapper;
|
||||
}
|
||||
16
ServicesApi/src/utils/types/url.ts
Normal file
16
ServicesApi/src/utils/types/url.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface Events {
|
||||
[key: string]: Array<{
|
||||
endpoint: string;
|
||||
eToken: string;
|
||||
token: string;
|
||||
key: string;
|
||||
description: string;
|
||||
isDefault: boolean;
|
||||
query: Record<string, boolean>;
|
||||
pages: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Mapper {
|
||||
[key: string]: (query: any) => any;
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PaginationHelper } from './pagination-helper';
|
||||
import { PrismaService } from '@/src/prisma.service';
|
||||
import { RedisHandlers } from './auth/redis_handlers';
|
||||
import { PasswordHandlers } from './auth/login_handler';
|
||||
import { CacheService } from '@/src/cache.service';
|
||||
import { RedisHandlers } from './store/redisHandlers';
|
||||
import { PasswordHandlers } from './store/loginHandler';
|
||||
import { RedisModule } from '../database/redis/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule],
|
||||
providers: [
|
||||
PaginationHelper,
|
||||
PrismaService,
|
||||
RedisHandlers,
|
||||
PasswordHandlers,
|
||||
CacheService,
|
||||
],
|
||||
exports: [PaginationHelper, RedisHandlers, PasswordHandlers, CacheService],
|
||||
exports: [PaginationHelper, RedisHandlers, PasswordHandlers],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
export class UtilsModule { }
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"types": ["node", "jest"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
@@ -19,7 +19,6 @@
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
// "baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
}
|
||||
|
||||
416
ServicesFrontEnd/frontend/package-lock.json
generated
416
ServicesFrontEnd/frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"cookies-next": "^6.1.0",
|
||||
"glob": "^11.0.3",
|
||||
"ioredis": "^5.6.1",
|
||||
"lucide-react": "^0.533.0",
|
||||
"next": "15.4.4",
|
||||
@@ -550,6 +551,41 @@
|
||||
"integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
@@ -1057,6 +1093,28 @@
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001727",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
|
||||
@@ -1130,7 +1188,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@@ -1142,8 +1199,7 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
@@ -1178,6 +1234,19 @@
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -1237,6 +1306,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.2",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||
@@ -1251,6 +1330,43 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -1301,6 +1417,33 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
@@ -1562,6 +1705,14 @@
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.533.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.533.0.tgz",
|
||||
@@ -1581,11 +1732,24 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -1766,6 +1930,34 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1905,6 +2097,36 @@
|
||||
"@img/sharp-win32-x64": "0.34.3"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
@@ -1930,6 +2152,94 @@
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
@@ -2029,6 +2339,104 @@
|
||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"reset-sync": "rm ./.next/server/app/[locale]/sync.lock"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"cookies-next": "^6.1.0",
|
||||
"glob": "^11.0.3",
|
||||
"ioredis": "^5.6.1",
|
||||
"lucide-react": "^0.533.0",
|
||||
"next": "15.4.4",
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
'use client';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from '@/i18n/navigation';
|
||||
import { apiGetFetcher } from '@/lib/fetcher';
|
||||
|
||||
export default function PageSelect() {
|
||||
const t = useTranslations('Select');
|
||||
const router = useRouter();
|
||||
|
||||
const [selectionList, setSelectionList] = useState<{ type: string, list: any[] } | null>(null);
|
||||
const [selectedOption, setSelectedOption] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSelectionList = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
apiGetFetcher({ url: '/api/auth/selections', isNoCache: true }).then((res) => {
|
||||
if (res.success) {
|
||||
if (res.data && typeof res.data === 'object' && 'type' in res.data && 'list' in res.data) {
|
||||
setSelectionList(res.data as { type: string, list: any[] });
|
||||
}
|
||||
const fetchSelectionList = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
apiGetFetcher({ url: '/api/auth/selections', isNoCache: true }).then((res) => {
|
||||
if (res.success) {
|
||||
if (res.data && typeof res.data === 'object' && 'type' in res.data && 'list' in res.data) {
|
||||
setSelectionList(res.data as { type: string, list: any[] });
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching selection list:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchSelectionList();
|
||||
}, []);
|
||||
const handleSelection = (id: string) => { setSelectedOption(id) };
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching selection list:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => { fetchSelectionList() }, []);
|
||||
|
||||
const handleSelection = (uuid: string) => { setSelectedOption(uuid) };
|
||||
const handleContinue = async () => {
|
||||
if (!selectedOption) return;
|
||||
setIsLoading(true);
|
||||
@@ -41,7 +41,7 @@ export default function PageSelect() {
|
||||
const result = await response.json();
|
||||
if (response.ok && result.status === 200) {
|
||||
console.log('Selection successful, redirecting to venue page');
|
||||
router.push('/venue');
|
||||
if (selectionList?.type === 'employee') { router.push('/office') } else if (selectionList?.type === 'occupant') { router.push('/venue') }
|
||||
} else {
|
||||
console.error('Selection failed:', result.message);
|
||||
alert(`Selection failed: ${result.message || 'Unknown error'}`);
|
||||
@@ -52,231 +52,362 @@ export default function PageSelect() {
|
||||
} finally { setIsLoading(false) }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 sm:p-6 md:p-8">
|
||||
<div className="max-w-6xl mx-auto w-full h-full flex flex-col">
|
||||
<div className="text-center mb-8 sm:mb-10 mt-4 sm:mt-6">
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-800 mb-2">{t('title')}</h1>
|
||||
<p className="text-base sm:text-lg text-gray-600">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 flex-grow">
|
||||
{selectionList?.list?.map((item: any) => {
|
||||
if (selectionList.type === 'employee') {
|
||||
const staff = item.staff;
|
||||
const department = staff?.duties?.departments;
|
||||
const company = department?.companies;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.uu_id}
|
||||
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
|
||||
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
|
||||
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
|
||||
onClick={() => handleSelection(item.uu_id)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
|
||||
{staff?.staff_code?.charAt(0) || 'E'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{t('staff')}: {staff?.staff_code || t('employee')}</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
|
||||
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('department')}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-16">{t('name')}:</span>
|
||||
<span className="text-sm text-gray-600">{department?.department_name || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-16">{t('code')}:</span>
|
||||
<span className="text-sm text-gray-600">{department?.department_code || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('company')}:</span>
|
||||
<span className="ml-2 text-sm text-gray-600">{company?.public_name || company?.formal_name || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOption === item.uu_id && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectionList.type === 'occupant') {
|
||||
const occupantType = item.occupant_types;
|
||||
const buildPart = item.build_parts;
|
||||
const build = buildPart?.build;
|
||||
const enums = buildPart?.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown;
|
||||
return (
|
||||
<div
|
||||
key={item.uu_id}
|
||||
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
|
||||
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
|
||||
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
|
||||
onClick={() => handleSelection(item.uu_id)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
|
||||
{occupantType?.occupant_code?.charAt(0) || 'O'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{t('occupant_type')}: {occupantType?.occupant_type}</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
|
||||
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('occupant_code')}:</span>
|
||||
<span className="ml-2 font-semibold text-indigo-600">{occupantType?.occupant_code}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('building')}:</span>
|
||||
<span className="ml-2 text-gray-600">{build?.build_name || 'Building'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('type')}:</span>
|
||||
<span className="ml-2 text-gray-600">{enums?.value}</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('part_details')}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 ml-6 mt-1">
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('code')}:</span>
|
||||
<span className="text-sm text-gray-600">{buildPart?.part_code}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('no')}:</span>
|
||||
<span className="text-sm text-gray-600">{buildPart?.part_no}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('level')}:</span>
|
||||
<span className="text-sm text-gray-600">{buildPart?.part_level}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('status')}:</span>
|
||||
<span className={`text-sm font-medium ${buildPart?.human_livable ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{buildPart?.human_livable ? t('livable') : t('not_livable')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOption === item.uu_id && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.uu_id}
|
||||
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
|
||||
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
|
||||
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
|
||||
onClick={() => handleSelection(item.uu_id)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
|
||||
{item.uu_id?.charAt(0) || 'S'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{selectionList.type || t('selection')}</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{item.uu_id || t('id')}</p>
|
||||
</div>
|
||||
|
||||
{selectedOption === item.uu_id && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
if (selectionList) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 sm:p-6 md:p-8">
|
||||
<div className="max-w-6xl mx-auto w-full h-full flex flex-col">
|
||||
<div className="text-center mb-8 sm:mb-10 mt-4 sm:mt-6">
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-800 mb-2">{t('title')}</h1>
|
||||
<p className="text-base sm:text-lg text-gray-600">{t('description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mt-10 flex justify-center">
|
||||
<button
|
||||
className={`px-8 py-4 rounded-xl font-bold text-white transition-all duration-300 text-lg ${selectedOption
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transform hover:scale-105'
|
||||
: 'bg-gray-400 cursor-not-allowed'}`}
|
||||
disabled={!selectedOption || isLoading}
|
||||
onClick={handleContinue}
|
||||
>
|
||||
{isLoading ? t('processing') : selectedOption ? t('continue') : t('select_option')}
|
||||
</button>
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 flex-grow">
|
||||
{selectionList?.list?.map((item: any) => {
|
||||
if (selectionList.type === 'employee') {
|
||||
const staff = item.staff;
|
||||
const department = staff?.duties?.departments;
|
||||
const company = department?.companies;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.uu_id}
|
||||
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
|
||||
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
|
||||
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
|
||||
onClick={() => handleSelection(item.uu_id)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
|
||||
{staff?.staff_code?.charAt(0) || 'E'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{t('staff')}: {staff?.staff_code || t('employee')}</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
|
||||
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('department')}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-16">{t('name')}:</span>
|
||||
<span className="text-sm text-gray-600">{department?.department_name || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-16">{t('code')}:</span>
|
||||
<span className="text-sm text-gray-600">{department?.department_code || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('company')}:</span>
|
||||
<span className="ml-2 text-sm text-gray-600">{company?.public_name || company?.formal_name || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOption === item.uu_id && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectionList.type === 'occupant') {
|
||||
const occupantType = item.occupant_types;
|
||||
const buildPart = item.build_parts;
|
||||
const build = buildPart?.build;
|
||||
const enums = buildPart?.api_enum_dropdown_build_parts_part_type_idToapi_enum_dropdown;
|
||||
return (
|
||||
<div
|
||||
key={item.uu_id}
|
||||
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
|
||||
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
|
||||
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
|
||||
onClick={() => handleSelection(item.uu_id)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
|
||||
{occupantType?.occupant_code?.charAt(0) || 'O'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{t('occupant_type')}: {occupantType?.occupant_type}</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-xs text-gray-700">{t('uuid')}:</span>
|
||||
<span className="ml-2 font-mono text-xs text-gray-600">{item?.uu_id}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('occupant_code')}:</span>
|
||||
<span className="ml-2 font-semibold text-indigo-600">{occupantType?.occupant_code}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('building')}:</span>
|
||||
<span className="ml-2 text-gray-600">{build?.build_name || 'Building'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('type')}:</span>
|
||||
<span className="ml-2 text-gray-600">{enums?.value}</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<svg className="w-4 h-4 text-indigo-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
<span className="font-medium text-gray-700">{t('part_details')}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 ml-6 mt-1">
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('code')}:</span>
|
||||
<span className="text-sm text-gray-600">{buildPart?.part_code}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('no')}:</span>
|
||||
<span className="text-sm text-gray-600">{buildPart?.part_no}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('level')}:</span>
|
||||
<span className="text-sm text-gray-600">{buildPart?.part_level}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-xs text-gray-500 w-12">{t('status')}:</span>
|
||||
<span className={`text-sm font-medium ${buildPart?.human_livable ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{buildPart?.human_livable ? t('livable') : t('not_livable')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOption === item.uu_id && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.uu_id}
|
||||
className={`rounded-2xl p-6 cursor-pointer transition-all duration-300 flex flex-col justify-between h-full shadow-lg border-2 ${selectedOption === item.uu_id
|
||||
? 'bg-indigo-100 border-indigo-500 shadow-indigo-200 transform scale-[1.02]'
|
||||
: 'bg-white/90 border-indigo-100 hover:bg-indigo-50 hover:border-indigo-300 hover:shadow-indigo-100'}`}
|
||||
onClick={() => handleSelection(item.uu_id)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-r from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold text-lg mr-4">
|
||||
{item.uu_id?.charAt(0) || 'S'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{selectionList.type || t('selection')}</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{item.uu_id || t('id')}</p>
|
||||
</div>
|
||||
|
||||
{selectedOption === item.uu_id && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mt-10 flex justify-center">
|
||||
<button
|
||||
className={`px-8 py-4 rounded-xl font-bold text-white transition-all duration-300 text-lg ${selectedOption
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transform hover:scale-105'
|
||||
: 'bg-gray-400 cursor-not-allowed'}`}
|
||||
disabled={!selectedOption || isLoading}
|
||||
onClick={handleContinue}
|
||||
>
|
||||
{isLoading ? t('processing') : selectedOption ? t('continue') : t('select_option')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-cyan-50 p-4 sm:p-6 md:p-8">
|
||||
<div className="max-w-6xl mx-auto w-full h-full flex flex-col">
|
||||
<div className="text-center mb-8 sm:mb-10 mt-4 sm:mt-6">
|
||||
<div className="skeleton h-8 w-64 mx-auto mb-2"></div>
|
||||
<div className="skeleton h-5 w-48 mx-auto"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 flex-grow">
|
||||
{/* Skeleton items to match the grid layout */}
|
||||
<div className="rounded-2xl p-6 shadow-lg border-2 bg-white/90 border-indigo-100">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="skeleton w-12 h-12 rounded-xl mr-4"></div>
|
||||
<div className="skeleton h-5 w-32"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-4 w-16 mr-2"></div>
|
||||
<div className="skeleton h-4 w-24"></div>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="skeleton h-4 w-20 mr-2"></div>
|
||||
</div>
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-3 w-12 mr-2"></div>
|
||||
<div className="skeleton h-3 w-20"></div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-3 w-12 mr-2"></div>
|
||||
<div className="skeleton h-3 w-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-4 w-16 mr-2"></div>
|
||||
<div className="skeleton h-4 w-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="skeleton w-6 h-6 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl p-6 shadow-lg border-2 bg-white/90 border-indigo-100">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="skeleton w-12 h-12 rounded-xl mr-4"></div>
|
||||
<div className="skeleton h-5 w-32"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-4 w-16 mr-2"></div>
|
||||
<div className="skeleton h-4 w-24"></div>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="skeleton h-4 w-20 mr-2"></div>
|
||||
</div>
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-3 w-12 mr-2"></div>
|
||||
<div className="skeleton h-3 w-20"></div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-3 w-12 mr-2"></div>
|
||||
<div className="skeleton h-3 w-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-4 w-16 mr-2"></div>
|
||||
<div className="skeleton h-4 w-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="skeleton w-6 h-6 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl p-6 shadow-lg border-2 bg-white/90 border-indigo-100">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="skeleton w-12 h-12 rounded-xl mr-4"></div>
|
||||
<div className="skeleton h-5 w-32"></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-4 w-16 mr-2"></div>
|
||||
<div className="skeleton h-4 w-24"></div>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-gray-100 mt-2">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="skeleton h-4 w-20 mr-2"></div>
|
||||
</div>
|
||||
<div className="ml-6 mt-1 space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-3 w-12 mr-2"></div>
|
||||
<div className="skeleton h-3 w-20"></div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-3 w-12 mr-2"></div>
|
||||
<div className="skeleton h-3 w-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="skeleton h-4 w-16 mr-2"></div>
|
||||
<div className="skeleton h-4 w-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<div className="skeleton w-6 h-6 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mt-10 flex justify-center">
|
||||
<div className="skeleton h-14 w-48 rounded-xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
import { Locale } from 'next-intl';
|
||||
import { checkAccess, checkSelectionOnSelectPage } from '@/app/api/guards';
|
||||
import SelectPageClient from './SelectPage';
|
||||
import LocaleSwitcherServer from '@/components/LocaleSwitcherServer';
|
||||
|
||||
export default async function PageSelect({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
await checkAccess(locale as Locale);
|
||||
await checkSelectionOnSelectPage(locale as Locale);
|
||||
return <SelectPageClient />;
|
||||
// await checkSelectionOnSelectPage(locale as Locale);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='absolute top-2 right-2'>
|
||||
<LocaleSwitcherServer locale={locale} pathname="/login" />
|
||||
</div>
|
||||
<SelectPageClient />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,6 @@ export default async function ProtectedLayout({
|
||||
const headersList = await headers();
|
||||
// const locale = getLocaleFromPath(removeSubStringFromPath(headersList));
|
||||
const removedLocaleRoute = removeSubStringFromPath(headersList);
|
||||
console.log('Removed locale route:', removedLocaleRoute);
|
||||
// console.log('Removed locale route:', removedLocaleRoute);
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'use server';
|
||||
import React from 'react';
|
||||
import { dashboardPages } from '@/pages/office/dashboard/mapper';
|
||||
import { renderPage } from '@/lib/page';
|
||||
import { getSelectToken } from '@/fetchers/token/select';
|
||||
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const pageUrl = "/office/dashboard";
|
||||
const pageToken = "qY56XMEr08wJkNvOR6EYQZKMVdTQEfHdLXGzzxcKU24E"
|
||||
const selectToken = await getSelectToken();
|
||||
try {
|
||||
const RenderPage = renderPage(selectToken, pageToken, dashboardPages);
|
||||
if (RenderPage) {
|
||||
return <>
|
||||
<div>Dashboard Page</div>
|
||||
<div className='flex align-center justify-center h-screen w-screen mt-10 text-2xl'>
|
||||
<RenderPage />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} catch (error) { console.log(error) }
|
||||
return <>
|
||||
<div>Dashboard Page</div>
|
||||
<div>You are not allowed to reach any page under {pageUrl}. Please contact your administrator.</div>
|
||||
</>;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use server';
|
||||
import React from 'react';
|
||||
import { dashboardPages } from '@/pages/venue/dashboard/mapper';
|
||||
import { renderPage } from '@/lib/page';
|
||||
import { getSelectTokenObject } from '@/fetchers/token/select';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const pageUrl = "/venue/dashboard";
|
||||
const pageToken = "IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE"
|
||||
const selectToken = await getSelectTokenObject();
|
||||
if (selectToken) {
|
||||
const RenderPage = renderPage(selectToken, pageToken, dashboardPages);
|
||||
if (RenderPage) {
|
||||
return <>
|
||||
<div className='h-screen w-screen text-2xl'><RenderPage /></div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
return <>
|
||||
<div>Dashboard Page</div>
|
||||
<div>You are not allowed to reach any page under {pageUrl}. Please contact your administrator.</div>
|
||||
</>;
|
||||
};
|
||||
@@ -1,59 +1,43 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { notFound } from "next/navigation";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Locale, locales } from "@/i18n/locales";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import '../globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
params: Promise<{ locale: Locale }>;
|
||||
children: ReactNode;
|
||||
params: Promise<{ locale: Locale }>;
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Omit<Props, 'children'>) {
|
||||
// Properly await params before accessing properties
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'LocaleLayout' });
|
||||
|
||||
return {
|
||||
title: t('title')
|
||||
};
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'LocaleLayout' });
|
||||
return { title: t('title') };
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({ children, params }: Props) {
|
||||
// Properly await params before accessing properties
|
||||
const { locale } = await params;
|
||||
|
||||
// Validate that the incoming locale is valid
|
||||
if (!locales.includes(locale as Locale)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Load messages for all child components
|
||||
const messages = (await import(`@/i18n/${locale}.json`)).default;
|
||||
|
||||
// Enable static rendering
|
||||
// Note: unstable_setRequestLocale is removed as it's causing TypeScript errors
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body className={inter.className}>
|
||||
<NextIntlClientProvider
|
||||
locale={locale}
|
||||
messages={messages}
|
||||
timeZone="Europe/Istanbul" // Configure a default timezone for Turkey
|
||||
>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
const { locale } = await params;
|
||||
if (!locales.includes(locale as Locale)) {
|
||||
redirect('/' + locales[0]);
|
||||
}
|
||||
const messages = (await import(`@/i18n/${locale}.json`)).default;
|
||||
return (
|
||||
<>
|
||||
<html lang={locale}>
|
||||
<body className={inter.className}>
|
||||
<NextIntlClientProvider locale={locale} messages={messages} timeZone="Europe/Istanbul">
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
'use server';
|
||||
import HomePage from '@/app/home-page';
|
||||
import { sendChunksToNest } from '@/lib/init-sync';
|
||||
|
||||
export default async function Home() {
|
||||
sendChunksToNest();
|
||||
return <HomePage />;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export async function POST(req: NextRequest) {
|
||||
console.log("Select attempt for UUID:", uuid);
|
||||
|
||||
const response = await doSelect({ uuid });
|
||||
|
||||
console.log("Select response:", response);
|
||||
if (response.status !== 200) {
|
||||
console.log("Select failed with status:", response.status);
|
||||
return NextResponse.json({ status: 401, message: "Select failed" });
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { type } = await req.json();
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ type: string }> }) {
|
||||
const { type } = await params;
|
||||
}
|
||||
5
ServicesFrontEnd/frontend/src/app/layout.tsx
Normal file
5
ServicesFrontEnd/frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
8
ServicesFrontEnd/frontend/src/app/page.tsx
Normal file
8
ServicesFrontEnd/frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use server'
|
||||
import { redirect } from '@/i18n/navigation';
|
||||
|
||||
const RedirectHome = async () => {
|
||||
return redirect({ locale: "en", href: "/" })
|
||||
}
|
||||
|
||||
export default RedirectHome
|
||||
@@ -1,8 +1,7 @@
|
||||
'use server';
|
||||
import { fetchData } from "@/fetchers/fecther";
|
||||
import { fetchData, fetchDataWithAccessToken } from "@/fetchers/fecther";
|
||||
import { urlSelectEndpoint, urlLoginEndpoint } from "@/fetchers/urls";
|
||||
import { LoginViaAccessKeys } from "@/fetchers/types/login/validations";
|
||||
import { setCookieAccessToken, setCookieSelectToken } from "@/fetchers/token/cookies";
|
||||
|
||||
|
||||
async function doLogin(payload: LoginViaAccessKeys) {
|
||||
@@ -20,7 +19,7 @@ async function doLogin(payload: LoginViaAccessKeys) {
|
||||
}
|
||||
|
||||
async function doSelect(payload: { uuid: string }) {
|
||||
const response = await fetchData(
|
||||
const response = await fetchDataWithAccessToken(
|
||||
urlSelectEndpoint,
|
||||
payload,
|
||||
"POST",
|
||||
|
||||
@@ -111,6 +111,23 @@ async function fetchDataWithToken<T>(
|
||||
return coreFetch<T>(endpoint, { method, cache, timeout }, headers, payload);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch data with authentication token
|
||||
*/
|
||||
async function fetchDataWithAccessToken<T>(
|
||||
endpoint: string,
|
||||
payload?: any,
|
||||
method: HttpMethod = "POST",
|
||||
cache: boolean = false,
|
||||
timeout: number = DEFAULT_TIMEOUT
|
||||
): Promise<ApiResponse<T>> {
|
||||
const accessToken = await getPlainAccessToken();
|
||||
console.log('accessToken', accessToken);
|
||||
const headers = { ...defaultHeaders, acs: accessToken };
|
||||
return coreFetch<T>(endpoint, { method, cache, timeout }, headers, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data with authentication token and UUID
|
||||
*/
|
||||
@@ -133,4 +150,4 @@ async function updateDataWithToken<T>(
|
||||
);
|
||||
}
|
||||
|
||||
export { fetchData, fetchDataWithToken, updateDataWithToken };
|
||||
export { fetchData, fetchDataWithToken, fetchDataWithAccessToken, updateDataWithToken };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
host: process.env.REDIS_HOST || "10.10.2.15",
|
||||
port: parseInt(process.env.REDIS_PORT || "6379", 10),
|
||||
password: process.env.REDIS_PASSWORD || "",
|
||||
password: process.env.REDIS_PASSWORD || "your_strong_password_here",
|
||||
db: parseInt(process.env.REDIS_DB || "0", 10),
|
||||
connectTimeout: 5000,
|
||||
maxRetriesPerRequest: 2,
|
||||
|
||||
@@ -32,6 +32,7 @@ interface UpdateFieldParams<T> {
|
||||
}
|
||||
|
||||
type RScan = Promise<string | null>;
|
||||
type RScanToken = Promise<any | null>;
|
||||
type RExists = Promise<boolean>;
|
||||
type RUpdate = Promise<void>;
|
||||
type RDelete = Promise<void>;
|
||||
@@ -122,3 +123,38 @@ export async function scanByRKeyDouble(params: DScanParams): RScan {
|
||||
} while (cursor !== "0");
|
||||
return keys.length > 0 ? keys[0] : null;
|
||||
}
|
||||
|
||||
export async function scanByRKeySingleToken(params: SScanParams): RScan {
|
||||
const pattern = redisScanAccess(params.rKey);
|
||||
const keys: string[] = [];
|
||||
let cursor = "0";
|
||||
do {
|
||||
const [nextCursor, matchedKeys] = await redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern
|
||||
);
|
||||
cursor = nextCursor;
|
||||
keys.push(...matchedKeys);
|
||||
} while (cursor !== "0");
|
||||
if (keys.length === 0) return null;
|
||||
return await getJSON({ key: keys[0] });
|
||||
}
|
||||
|
||||
export async function scanByRKeyDoubleToken(params: DScanParams): RScan {
|
||||
const pattern = redisScanSelect(params.rKey, params.sKey);
|
||||
console.log("pattern", pattern);
|
||||
const keys: string[] = [];
|
||||
let cursor = "0";
|
||||
do {
|
||||
const [nextCursor, matchedKeys] = await redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern
|
||||
);
|
||||
cursor = nextCursor;
|
||||
keys.push(...matchedKeys);
|
||||
} while (cursor !== "0");
|
||||
if (keys.length === 0) return null;
|
||||
return await getJSON({ key: keys[0] });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
import { AuthError } from "@/fetchers/types/base";
|
||||
import { scanByRKeyDouble } from "@/fetchers/redis/redisService";
|
||||
import { scanByRKeyDouble, scanByRKeyDoubleToken, scanByRKeySingleToken } from "@/fetchers/redis/redisService";
|
||||
import { getPlainAccessToken } from "./access";
|
||||
import { nextCrypto } from "@/fetchers/base";
|
||||
import { getCookieSelectToken, removeCookieTokens, setCookieSelectToken } from "./cookies";
|
||||
@@ -54,6 +54,30 @@ async function getSelectToken() {
|
||||
catch (error) { throw new AuthError("No select token found in headers") }
|
||||
}
|
||||
|
||||
async function getSelectTokenObject() {
|
||||
try {
|
||||
const plainAccessToken = await getPlainAccessToken();
|
||||
const plainSelectToken = await getPlainSelectToken();
|
||||
console.log('plainAccessToken', plainAccessToken);
|
||||
console.log('plainSelectToken', plainSelectToken);
|
||||
const scanToken = await scanByRKeyDoubleToken({ rKey: plainAccessToken, sKey: plainSelectToken });
|
||||
if (!scanToken) throw new AuthError("Select token is invalid");
|
||||
return scanToken;
|
||||
}
|
||||
catch (error) { throw new AuthError("No select token found in headers") }
|
||||
}
|
||||
|
||||
async function getAccessTokenObject() {
|
||||
try {
|
||||
const plainAccessToken = await getPlainAccessToken();
|
||||
console.log('plainAccessToken', plainAccessToken);
|
||||
const scanToken = await scanByRKeySingleToken({ rKey: plainAccessToken });
|
||||
if (!scanToken) throw new AuthError("Access token is invalid");
|
||||
return scanToken;
|
||||
}
|
||||
catch (error) { throw new AuthError("No access token found in headers") }
|
||||
}
|
||||
|
||||
async function setSelectToken(token: string) {
|
||||
console.log('setSelectToken is triggered...');
|
||||
await setCookieSelectToken(token);
|
||||
@@ -75,4 +99,4 @@ async function removeSelectToken() {
|
||||
await removeCookieTokens();
|
||||
}
|
||||
|
||||
export { getSelectToken, setSelectToken, removeSelectToken, getPlainSelectToken, isSelectTokenValid };
|
||||
export { getSelectToken, setSelectToken, removeSelectToken, getPlainSelectToken, isSelectTokenValid, getSelectTokenObject, getAccessTokenObject };
|
||||
|
||||
58
ServicesFrontEnd/frontend/src/lib/init-sync.ts
Normal file
58
ServicesFrontEnd/frontend/src/lib/init-sync.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { employeeMapper } from '@/pages/office/mapper';
|
||||
import { occupantMapper } from '@/pages/venue/mapper';
|
||||
import { Page } from '@/pages/types/page';
|
||||
|
||||
function chunkifyObject<T>(obj: Record<string, T>, chunkSize: number): Record<string, T>[] {
|
||||
const keys = Object.keys(obj);
|
||||
const chunks: Record<string, T>[] = [];
|
||||
for (let i = 0; i < keys.length; i += chunkSize) {
|
||||
const chunk: Record<string, T> = {};
|
||||
keys.slice(i, i + chunkSize).forEach((key) => {
|
||||
chunk[key] = obj[key];
|
||||
});
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export async function sendChunksToNest() {
|
||||
const sendObject: Page = { ...employeeMapper, ...occupantMapper };
|
||||
const lockPath = path.join(__dirname, 'sync.lock');
|
||||
|
||||
if (fs.existsSync(lockPath)) {
|
||||
console.log('🔁 Zaten sync edilmiş, işlem atlandı.');
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = chunkifyObject(sendObject, 50);
|
||||
const totalChunks = chunks.length;
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const chunk = chunks[i];
|
||||
|
||||
// 👇 Bu şekilde index bilgisiyle beraber nested body oluşturuyoruz
|
||||
const payload = {
|
||||
chunkIndex: i + 1,
|
||||
chunkCount: totalChunks,
|
||||
data: chunk,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8001/navigator/page/configure', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log(`✅ [${payload.chunkIndex}/${payload.chunkCount}] Chunk gönderildi. Kayıt sayısı: ${result.count}`);
|
||||
} catch (err) {
|
||||
console.error(`❌ [${i + 1}/${totalChunks}] Chunk gönderimi sırasında hata:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(lockPath, 'done');
|
||||
console.log('🎉 Tüm chunklar gönderildi ve lock dosyası oluşturuldu.');
|
||||
}
|
||||
9
ServicesFrontEnd/frontend/src/lib/page.tsx
Normal file
9
ServicesFrontEnd/frontend/src/lib/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
function renderPage(selectToken: any, pageUrl: string, fromTokenPages: any) {
|
||||
const subPageKey = selectToken.pages[pageUrl];
|
||||
if (Object.keys(fromTokenPages).includes(subPageKey)) {
|
||||
const subPage = fromTokenPages[subPageKey as keyof typeof fromTokenPages];
|
||||
if (subPage) { if (subPage.page) { return subPage.page } }
|
||||
} return null;
|
||||
}
|
||||
|
||||
export { renderPage };
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
const DashboardPtnZblJTri0DnlQaUOikQx: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>DashboardPtnZblJTri0DnlQaUOikQx</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPtnZblJTri0DnlQaUOikQx;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Page } from "@/pages/types/page";
|
||||
import DashboardPtnZblJTri0DnlQaUOikQx from "./PtnZblJTri0DnlQaUOikQx";
|
||||
|
||||
const dashboardPages: Page = {
|
||||
"qY56XMEr08wJkNvOR6EYQZKMVdTQEfHdLXGzzxcKU24E:PtnZblJTri0DnlQaUOikQx": {
|
||||
name: "DashboardPtnZblJTri0DnlQaUOikQx",
|
||||
key: "PtnZblJTri0DnlQaUOikQx",
|
||||
url: "/office/dashboard",
|
||||
page: DashboardPtnZblJTri0DnlQaUOikQx,
|
||||
description: "Dashboard",
|
||||
isDefault: true,
|
||||
typeToken: "L9wBdwV9OlxsLAgh",
|
||||
params: {},
|
||||
events: ["Aevent", "Aevent", "Aevent"],
|
||||
includeTokens: ['*'],
|
||||
excludeTokens: []
|
||||
}
|
||||
};
|
||||
|
||||
export { dashboardPages };
|
||||
4
ServicesFrontEnd/frontend/src/pages/office/mapper.ts
Normal file
4
ServicesFrontEnd/frontend/src/pages/office/mapper.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import {dashboardPages} from "./dashboard/mapper";
|
||||
export const employeeMapper = {
|
||||
...dashboardPages
|
||||
}
|
||||
16
ServicesFrontEnd/frontend/src/pages/types/page.ts
Normal file
16
ServicesFrontEnd/frontend/src/pages/types/page.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface Page {
|
||||
[key: string]: {
|
||||
name: string;
|
||||
key: string;
|
||||
url: string;
|
||||
page: React.FC;
|
||||
description: string;
|
||||
isDefault: boolean;
|
||||
typeToken: string;
|
||||
params: Record<string, boolean>;
|
||||
events?: string[];
|
||||
includeTokens?: string[];
|
||||
excludeTokens?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
const LeftMenu = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<div className="z-50 h-screen">
|
||||
{isOpen && <button className="" onClick={() => setIsOpen(!isOpen)}>Menu</button>}
|
||||
{!isOpen && <h1 className="bg-red-500 w-full h-full">
|
||||
<button className="" onClick={() => setIsOpen(!isOpen)}>LeftMenu</button>
|
||||
</h1>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DashboardhES1KfaPRZeadmmjdryShA: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row h-screen w-screen">
|
||||
<div className="w-full md:w-1/4 lg:w-1/4 xl:w-1/4">
|
||||
<LeftMenu />
|
||||
</div>
|
||||
<div className="w-full md:w-3/4 lg:w-3/4 xl:w-3/4">
|
||||
<h1>DashboardhES1KfaPRZeadmmjdryShA</h1>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardhES1KfaPRZeadmmjdryShA;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Page } from "@/pages/types/page";
|
||||
import DashboardhES1KfaPRZeadmmjdryShA from "./hES1KfaPRZeadmmjdryShA";
|
||||
|
||||
const dashboardPages: Page = {
|
||||
"IbGpchaw3muiY7y9rnV0EJYoPy5XoOOrITT9JlfIbqwE:hES1KfaPRZeadmmjdryShA": {
|
||||
name: "DashboardhES1KfaPRZeadmmjdryShA",
|
||||
key: "hES1KfaPRZeadmmjdryShA",
|
||||
url: "/venue/dashboard",
|
||||
page: DashboardhES1KfaPRZeadmmjdryShA,
|
||||
description: "Dashboard",
|
||||
isDefault: true,
|
||||
typeToken: "j0adQOsJBR0xq24d",
|
||||
params: {},
|
||||
events: [],
|
||||
includeTokens: ["*"],
|
||||
excludeTokens: []
|
||||
}
|
||||
};
|
||||
|
||||
export { dashboardPages };
|
||||
5
ServicesFrontEnd/frontend/src/pages/venue/mapper.ts
Normal file
5
ServicesFrontEnd/frontend/src/pages/venue/mapper.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { dashboardPages } from "./dashboard/mapper";
|
||||
|
||||
export const occupantMapper = {
|
||||
...dashboardPages,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
*.sqlite3
|
||||
*.log
|
||||
*.env
|
||||
venv/
|
||||
.env.*
|
||||
node_modules/
|
||||
.prisma/
|
||||
.prisma-cache/
|
||||
ServicesRunnner/AccountRecordServices/Test/venv/
|
||||
@@ -0,0 +1,22 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PRISMA_SCHEMA_PATH=/app/Depends/schema.prisma
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ServicesRunner/Depends/ /app/Depends/
|
||||
COPY ServicesRunner/AccountRecordServices/Finder/Comment /app/
|
||||
|
||||
COPY ServicesRunner/requirements.txt /app/requirements.txt
|
||||
COPY ServicesRunner/AccountRecordServices/Finder/Comment/entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
194
ServicesRunner/AccountRecordServices/Finder/Accounts/app.py
Normal file
194
ServicesRunner/AccountRecordServices/Finder/Accounts/app.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import time
|
||||
import arrow
|
||||
import pprint
|
||||
|
||||
from json import dumps, loads
|
||||
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from Depends.prisma_client import PrismaService
|
||||
from Depends.service_handler import ProcessCommentFinderService
|
||||
from Depends.config import ConfigServices, MailSendModel, RedisMailSender, Status, RedisTaskObject, FinderComment
|
||||
|
||||
|
||||
class BankReceive(BaseModel):
|
||||
import_file_name: str
|
||||
iban: str
|
||||
bank_date: datetime
|
||||
channel_branch: str
|
||||
currency: Optional[str] = "TL"
|
||||
currency_value: Decimal
|
||||
bank_balance: Decimal
|
||||
additional_balance: Decimal
|
||||
process_name: str
|
||||
process_type: str
|
||||
process_comment: str
|
||||
bank_reference_code: str
|
||||
bank_date_w: int
|
||||
bank_date_m: int
|
||||
bank_date_d: int
|
||||
bank_date_y: int
|
||||
|
||||
|
||||
def check_task_belong_to_this_service(task: RedisTaskObject):
|
||||
"""
|
||||
Check if task belongs to this service
|
||||
"""
|
||||
if not task.service == ConfigServices.SERVICE_PREFIX_FINDER_IBAN:
|
||||
return False
|
||||
if not task.completed:
|
||||
return False
|
||||
if task.is_completed:
|
||||
return False
|
||||
if not task.data:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def write_account_records_row_from_finder_comment(finder_comments: list[FinderComment], prisma_service: PrismaService, saved_list_of_account_records: dict):
|
||||
"""
|
||||
Write account records row from finder comment
|
||||
"""
|
||||
finder_comments = list(finder_comments)
|
||||
for finder_comment in finder_comments:
|
||||
bank_date = arrow.get(finder_comment.bank_date).replace(tzinfo='GMT+3').datetime
|
||||
bank_receive_record = BankReceive(
|
||||
import_file_name=finder_comment.filename, iban=finder_comment.iban, bank_date=bank_date, channel_branch=finder_comment.channel_branch, currency="TL", currency_value=finder_comment.currency_value,
|
||||
bank_balance=finder_comment.balance, additional_balance=finder_comment.additional_balance, process_name=finder_comment.process_name, process_type=finder_comment.process_type,
|
||||
process_comment=finder_comment.process_comment, bank_reference_code=finder_comment.bank_reference_code, build_id=finder_comment.build_id, build_uu_id=finder_comment.build_uu_id,
|
||||
decision_book_id=finder_comment.decision_book_id, decision_book_uu_id=finder_comment.decision_book_uu_id, bank_date_w=bank_date.weekday(), bank_date_m=bank_date.month,
|
||||
bank_date_d=bank_date.day, bank_date_y=bank_date.year
|
||||
)
|
||||
account_record_found = prisma_service.find_first(table="account_records", query={"iban": bank_receive_record.iban, "bank_reference_code": bank_receive_record.bank_reference_code,
|
||||
"bank_date": bank_receive_record.bank_date, "bank_balance": bank_receive_record.bank_balance, "currency_value": bank_receive_record.currency_value},
|
||||
select={"id": True, "iban": True, "bank_reference_code": True, "bank_date": True, "bank_balance": True}
|
||||
)
|
||||
if not account_record_found:
|
||||
created_account_record = prisma_service.create(table="account_records", data=bank_receive_record.dict(), select={"id": True, "iban": True, "bank_reference_code": True, "bank_date": True, "bank_balance": True} )
|
||||
if created_account_record['build_id'] in saved_list_of_account_records.keys():
|
||||
saved_list_of_account_records[created_account_record['build_id']] = [*saved_list_of_account_records[created_account_record['build_id']], created_account_record]
|
||||
else:
|
||||
saved_list_of_account_records[created_account_record['build_id']] = [created_account_record]
|
||||
return saved_list_of_account_records
|
||||
|
||||
|
||||
def enclose_task_and_send_mail_to_build_manager(prisma_service: PrismaService, saved_list_of_account_records: dict, process_comment_finder_service: ProcessCommentFinderService, task: RedisTaskObject):
|
||||
"""
|
||||
Enclose task and send mail to build manager
|
||||
"""
|
||||
if not saved_list_of_account_records:
|
||||
return
|
||||
list_of_new_set, today = [], arrow.now().to('GMT+3').datetime
|
||||
for build_id, saved_list_of_account_record in saved_list_of_account_records.items():
|
||||
build_manager_occupant_type = prisma_service.find_first(table="occupant_types", query={"occupant_code":"BU-MNG", "is_confirmed": True, "active": True})
|
||||
living_space = prisma_service.find_first(
|
||||
table="build_living_space", query={
|
||||
"build_id": build_id, "occupant_type_id": build_manager_occupant_type['id'], "expiry_starts": {"lte": today}, "expiry_ends": {"gte": today}}
|
||||
)
|
||||
build = prisma_service.find_first(table="builds", query={"id": build_id})
|
||||
person = prisma_service.find_first(table="people", query={"id": living_space['person_id']})
|
||||
user = prisma_service.find_first(table="users", query={"person_id": person['id']})
|
||||
send_object = MailSendModel(
|
||||
receivers=[user.email], data=saved_list_of_account_record, template_name=ConfigServices.TEMPLATE_ACCOUNT_RECORDS,
|
||||
subject=f"{build['name']} Cari Durum Bilgilendirme Raporu - {today.strftime('%d/%m/%Y %H:%M')}",
|
||||
)
|
||||
set_mail_object = RedisMailSender(
|
||||
task=task, data=send_object, service=ConfigServices.SERVICE_PREFIX_MAIL_SENDER, status=Status.PENDING, completed=False, created_at=today.strftime('%Y-%m-%d %H:%M:%S')
|
||||
)
|
||||
list_of_new_set.append(set_mail_object)
|
||||
if list_of_new_set:
|
||||
process_comment_finder_service.service_retriever.redis_client.set(ConfigServices.SERVICE_PREFIX_MAIL_SENDER, dumps(
|
||||
{"type": "mail_sender", "data": list_of_new_set, "count": len(list_of_new_set), "created_at": today.strftime('%Y-%m-%d %H:%M:%S')}
|
||||
))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
prisma_service = PrismaService()
|
||||
process_comment_finder_service = ProcessCommentFinderService()
|
||||
|
||||
print("Process Comment service started")
|
||||
try:
|
||||
print("Process Comment service started sleeping for 5 seconds")
|
||||
while True:
|
||||
time.sleep(5)
|
||||
saved_list_of_account_records = dict()
|
||||
tasks = process_comment_finder_service.fetch_all_tasks()
|
||||
for task in tasks:
|
||||
if not check_task_belong_to_this_service(task):
|
||||
continue
|
||||
write_account_records_row_from_finder_comment(
|
||||
finder_comments=task.data.FinderComment, prisma_service=prisma_service, saved_list_of_account_records=saved_list_of_account_records
|
||||
)
|
||||
save_task_object_for_comment_parsing(task=task, process_comment_finder_service=process_comment_finder_service)
|
||||
process_comment_finder_service.update_task_status(task_uuid=task.task, is_completed=True, status=Status.COMPLETED)
|
||||
process_comment_finder_service.delete_task(task_uuid=task.task)
|
||||
enclose_task_and_send_mail_to_build_manager(
|
||||
prisma_service=prisma_service, saved_list_of_account_records=saved_list_of_account_records, process_comment_finder_service=process_comment_finder_service, task=task
|
||||
)
|
||||
except Exception as e:
|
||||
raise
|
||||
finally:
|
||||
prisma_service.disconnect()
|
||||
|
||||
|
||||
def fix_account_records_bank_date(prisma_service: PrismaService, bank_receive_record: BankReceive):
|
||||
account_record_from_other_fields = prisma_service.find_first(
|
||||
table="account_records",
|
||||
query={
|
||||
"iban": bank_receive_record.iban,
|
||||
"bank_reference_code": bank_receive_record.bank_reference_code,
|
||||
"bank_balance": bank_receive_record.bank_balance,
|
||||
"currency_value": bank_receive_record.currency_value,
|
||||
# "process_comment": {"contains": str(bank_receive_record.process_comment), "mode": "insensitive"},
|
||||
},
|
||||
select={
|
||||
"id": True, "iban": True, "bank_reference_code": True, "bank_date": True,
|
||||
"bank_balance": True, "currency_value": True, "process_comment": True
|
||||
}
|
||||
)
|
||||
if account_record_from_other_fields:
|
||||
prisma_service.update(
|
||||
table="account_records", where={"id": account_record_from_other_fields['id']}, data={"bank_date": bank_receive_record.bank_date},
|
||||
)
|
||||
if not account_record_from_other_fields:
|
||||
pprint.pprint({"not_found_bank_receive_record": bank_receive_record})
|
||||
# prisma_service.update(
|
||||
# table="account_records", where={"id": account_record_from_other_fields['id']}, data={"bank_date": bank_receive_record.bank_date},
|
||||
# )
|
||||
# from_database = arrow.get(account_record_from_other_fields['bank_date']).to('GMT+3').datetime
|
||||
# print('old date', from_database, " - new date ", bank_receive_record.bank_date)
|
||||
|
||||
|
||||
def commented_out_code():
|
||||
account_record_found = None
|
||||
|
||||
old_bank_date=arrow.get(finder_comment.bank_date).datetime
|
||||
|
||||
if not account_record_found:
|
||||
account_record_found_with_old_date = prisma_service.find_first(
|
||||
table="account_records",
|
||||
query={
|
||||
"iban": bank_receive_record.iban, "bank_reference_code": bank_receive_record.bank_reference_code,
|
||||
"bank_date": old_bank_date, "bank_balance": bank_receive_record.bank_balance,
|
||||
},
|
||||
)
|
||||
if account_record_found_with_old_date:
|
||||
prisma_service.update(
|
||||
table="account_records", where={"id": account_record_found_with_old_date.id}, data={"bank_date": bank_receive_record.bank_date},
|
||||
)
|
||||
if account_record_found:
|
||||
print('-' * 150)
|
||||
pprint.pprint(
|
||||
{
|
||||
"account_record_found": dict(account_record_found),
|
||||
"bank_receive_record": bank_receive_record.dict(),
|
||||
"bank_receive_record.bank_date": bank_receive_record.bank_date,
|
||||
"account_record_found.bank_date": account_record_found["bank_date"],
|
||||
}
|
||||
)
|
||||
print('-' * 150)
|
||||
return
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
VENV_PATH="/opt/venv"
|
||||
REQUIREMENTS_PATH="/app/requirements.txt"
|
||||
SCHEMA_PATH="/app/Depends/schema.prisma"
|
||||
PRISMA_BINARY_PATH="/root/.cache/prisma-python/binaries"
|
||||
|
||||
if [ ! -x "$VENV_PATH/bin/python" ]; then
|
||||
python -m venv "$VENV_PATH"
|
||||
"$VENV_PATH/bin/pip" install pip --upgrade
|
||||
"$VENV_PATH/bin/pip" install --no-cache-dir -r "$REQUIREMENTS_PATH"
|
||||
"$VENV_PATH/bin/prisma" generate --schema "$SCHEMA_PATH"
|
||||
fi
|
||||
|
||||
if ! find "$PRISMA_BINARY_PATH" -type f -name "prisma-query-engine-debian-openssl-3.0.x" | grep -q .; then
|
||||
"$VENV_PATH/bin/prisma" py fetch
|
||||
fi
|
||||
|
||||
exec "$VENV_PATH/bin/python" -u app.py
|
||||
@@ -0,0 +1,14 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
*.sqlite3
|
||||
*.log
|
||||
*.env
|
||||
venv/
|
||||
.env.*
|
||||
node_modules/
|
||||
.prisma/
|
||||
.prisma-cache/
|
||||
ServicesRunnner/AccountRecordServices/Test/venv/
|
||||
23
ServicesRunner/AccountRecordServices/Finder/Iban/Dockerfile
Normal file
23
ServicesRunner/AccountRecordServices/Finder/Iban/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PRISMA_SCHEMA_PATH=/app/Depends/schema.prisma
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ServicesRunner/Depends/ /app/Depends
|
||||
|
||||
COPY ServicesRunner/requirements.txt /app/requirements.txt
|
||||
|
||||
COPY ServicesRunner/AccountRecordServices/Finder/Iban/entrypoint.sh /entrypoint.sh
|
||||
COPY ServicesRunner/AccountRecordServices/Finder/Iban /app/
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
108
ServicesRunner/AccountRecordServices/Finder/Iban/app.py
Normal file
108
ServicesRunner/AccountRecordServices/Finder/Iban/app.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import time
|
||||
import arrow
|
||||
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from Depends.prisma_client import PrismaService
|
||||
from Depends.service_handler import IbanFinderService
|
||||
from Depends.config import ConfigServices, Status, FinderIban, RedisTaskObject
|
||||
|
||||
|
||||
class IbanRecord(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
iban: str
|
||||
build_id: int
|
||||
build_uu_id: str
|
||||
expiry_starts: datetime
|
||||
expiry_ends: datetime
|
||||
|
||||
|
||||
class DecisionBookRecord(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
build_id: int
|
||||
build_uu_id: str
|
||||
expiry_starts: datetime
|
||||
expiry_ends: datetime
|
||||
|
||||
|
||||
def check_task_belong_to_this_service(task: RedisTaskObject):
|
||||
if not task.service == ConfigServices.SERVICE_PREFIX_MAIL_PARSER:
|
||||
return False
|
||||
if not task.completed:
|
||||
return False
|
||||
if not task.data:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def extract_build_iban_from_task(task: RedisTaskObject, finder_iban: FinderIban, write_object: dict) -> tuple[bool, dict]:
|
||||
bank_date = arrow.get(finder_iban.bank_date).datetime
|
||||
iban_record_db = prisma_service.find_first(
|
||||
table="build_ibans",
|
||||
query={
|
||||
"active": True, "deleted": False, "is_confirmed": True, "iban": finder_iban.iban,
|
||||
"expiry_starts": {"lte": bank_date}, "expiry_ends": {"gte": bank_date},
|
||||
},
|
||||
select={"id": None, "uu_id": None, "iban": None, "build_id": None, "build_uu_id": None, "expiry_starts": None, "expiry_ends": None}
|
||||
)
|
||||
if iban_record_db:
|
||||
iban_record = IbanRecord(**iban_record_db)
|
||||
write_object["build_id"] = iban_record.build_id
|
||||
write_object["build_uu_id"] = iban_record.build_uu_id
|
||||
return True, write_object
|
||||
return False, write_object
|
||||
|
||||
|
||||
def extract_decision_book_from_task(write_object: dict) -> tuple[bool, dict]:
|
||||
bank_date = arrow.get(write_object["bank_date"]).datetime
|
||||
decision_book_record_db = prisma_service.find_first(
|
||||
table="build_decision_book",
|
||||
query={
|
||||
"active": True, "deleted": False, "is_confirmed": True, "build_id": write_object["build_id"],
|
||||
"expiry_starts": {"lte": bank_date}, "expiry_ends": {"gte": bank_date},
|
||||
},
|
||||
select={"id": None, "uu_id": None, "build_id": None, "build_uu_id": None, "expiry_starts": None, "expiry_ends": None}
|
||||
)
|
||||
if decision_book_record_db:
|
||||
decision_book_record = DecisionBookRecord(**decision_book_record_db)
|
||||
write_object["build_decision_book_id"] = decision_book_record.id
|
||||
write_object["build_decision_book_uu_id"] = decision_book_record.uu_id
|
||||
return True, write_object
|
||||
return False, write_object
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
prisma_service = PrismaService()
|
||||
iban_finder_service = IbanFinderService()
|
||||
print("Find Build Iban service started")
|
||||
try:
|
||||
print("Find Build Iban service started sleeping for 5 seconds")
|
||||
while True:
|
||||
time.sleep(5)
|
||||
tasks = iban_finder_service.fetch_all_tasks()
|
||||
for task in tasks:
|
||||
if not check_task_belong_to_this_service(task):
|
||||
continue
|
||||
if list(task.data.FinderIban):
|
||||
finder_iban_list = []
|
||||
for finder_iban in list(task.data.FinderIban):
|
||||
write_object = finder_iban.dict()
|
||||
is_build_found, is_decision_book_found = False, False
|
||||
is_build_found, write_object = extract_build_iban_from_task(task, finder_iban, write_object)
|
||||
if is_build_found:
|
||||
is_decision_book_found, write_object = extract_decision_book_from_task(write_object)
|
||||
if is_build_found or is_decision_book_found:
|
||||
finder_iban_list.append(write_object)
|
||||
if finder_iban_list:
|
||||
iban_finder_service.update_service_data(task.task, ConfigServices.SERVICE_PREFIX_FINDER_COMMENT, finder_iban_list)
|
||||
iban_finder_service.change_service(task.task, ConfigServices.SERVICE_PREFIX_FINDER_IBAN, Status.COMPLETED, True)
|
||||
continue
|
||||
iban_finder_service.change_service(task.task, ConfigServices.SERVICE_PREFIX_FINDER_IBAN, Status.FAILED, True)
|
||||
except Exception as e:
|
||||
raise
|
||||
finally:
|
||||
prisma_service.disconnect()
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
VENV_PATH="/opt/venv"
|
||||
REQUIREMENTS_PATH="/app/requirements.txt"
|
||||
SCHEMA_PATH="/app/Depends/schema.prisma"
|
||||
PRISMA_BINARY_PATH="/root/.cache/prisma-python/binaries"
|
||||
|
||||
if [ ! -x "$VENV_PATH/bin/python" ]; then
|
||||
python -m venv "$VENV_PATH"
|
||||
"$VENV_PATH/bin/pip" install pip --upgrade
|
||||
"$VENV_PATH/bin/pip" install --no-cache-dir -r "$REQUIREMENTS_PATH"
|
||||
"$VENV_PATH/bin/prisma" generate --schema "$SCHEMA_PATH"
|
||||
fi
|
||||
|
||||
if ! find "$PRISMA_BINARY_PATH" -type f -name "prisma-query-engine-debian-openssl-3.0.x" | grep -q .; then
|
||||
"$VENV_PATH/bin/prisma" py fetch
|
||||
fi
|
||||
|
||||
exec "$VENV_PATH/bin/python" -u app.py
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -0,0 +1,14 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
*.sqlite3
|
||||
*.log
|
||||
*.env
|
||||
venv/
|
||||
.env.*
|
||||
node_modules/
|
||||
.prisma/
|
||||
.prisma-cache/
|
||||
ServicesRunnner/AccountRecordServices/Test/venv/
|
||||
@@ -0,0 +1,22 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PRISMA_SCHEMA_PATH=/app/Depends/schema.prisma
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ServicesRunner/Depends/ /app/Depends/
|
||||
COPY ServicesRunner/AccountRecordServices/Finder/Parser/Comment /app/
|
||||
|
||||
COPY ServicesRunner/requirements.txt /app/requirements.txt
|
||||
COPY ServicesRunner/AccountRecordServices/Finder/Parser/Comment/entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,243 @@
|
||||
import time
|
||||
import arrow
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from matchers import ParsedComment, Parser
|
||||
from models import BuildingCluster, BuildPart, BuildLivingSpace, Person, User, OccupantType
|
||||
|
||||
from Depends.prisma_client import PrismaService
|
||||
from Depends.config import ConfigServices, RedisTaskObject
|
||||
from Depends.service_handler import ProcessCommentParserService
|
||||
|
||||
|
||||
def check_task_belong_to_this_service(task: RedisTaskObject):
|
||||
"""
|
||||
Check if task belongs to this service
|
||||
"""
|
||||
if not task.service == ConfigServices.TASK_COMMENT_PARSER:
|
||||
return False
|
||||
if not task.completed:
|
||||
return False
|
||||
if task.is_completed:
|
||||
return False
|
||||
if not task.data:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_all_person_data_due_to_build(prisma_service: PrismaService):
|
||||
"""
|
||||
Get all person data due to build with comprehensive inner joins
|
||||
Returns a dictionary of buildings clustered with their build parts, people, and living spaces
|
||||
"""
|
||||
buildings_dict, today = {}, arrow.now().to('GMT+3').datetime
|
||||
occupant_flat_owner = prisma_service.find_first(table="occupant_types", query={"occupant_code": "FL-OWN", "active": True, "is_confirmed": True}, include={"user_types": True})
|
||||
occupant_tenant = prisma_service.find_first(table="occupant_types", query={"occupant_code": "FL-TEN", "active": True, "is_confirmed": True}, include={"user_types": True})
|
||||
possible_money_sender_occupants = [occupant_flat_owner.id, occupant_tenant.id]
|
||||
buildings = prisma_service.find_many(table="build", query={"active": True, "is_confirmed": True,"expiry_starts": {"lte": today}, "expiry_ends": {"gte": today}})
|
||||
for build in buildings:
|
||||
buildings_dict[str(build.id)] = BuildingCluster(
|
||||
id=build.id,
|
||||
uu_id=build.uu_id,
|
||||
build_name=build.build_name,
|
||||
build_no=build.build_no,
|
||||
build_date=str(build.build_date),
|
||||
decision_period_date=str(build.decision_period_date),
|
||||
expiry_starts=str(build.expiry_starts),
|
||||
expiry_ends=str(build.expiry_ends),
|
||||
is_confirmed=build.is_confirmed,
|
||||
active=build.active,
|
||||
build_parts=[]
|
||||
)
|
||||
build_parts = prisma_service.find_many(table="build_parts", query={"build_id": build.id, "active": True, "is_confirmed": True, "human_livable": True, "expiry_starts": {"lte": today}, "expiry_ends": {"gte": today}})
|
||||
for build_part in build_parts:
|
||||
part_obj = BuildPart(
|
||||
id=build_part.id,
|
||||
uu_id=build_part.uu_id,
|
||||
part_no=build_part.part_no,
|
||||
part_level=build_part.part_level,
|
||||
part_code=build_part.part_code,
|
||||
part_gross_size=build_part.part_gross_size,
|
||||
part_net_size=build_part.part_net_size,
|
||||
human_livable=build_part.human_livable,
|
||||
build_id=build_part.build_id,
|
||||
build_uu_id=build_part.build_uu_id,
|
||||
is_confirmed=build_part.is_confirmed,
|
||||
active=build_part.active,
|
||||
living_spaces=[],
|
||||
build=None
|
||||
)
|
||||
living_spaces = prisma_service.find_many(
|
||||
table="build_living_space", include={"occupant_types": True, "people": {"include": {"users": True}}},
|
||||
query={"build_parts_id": build_part.id, "active": True, "is_confirmed": True, "expiry_starts": {"lte": today}, "expiry_ends": {"gte": today}, "occupant_type_id": {"in": possible_money_sender_occupants}},
|
||||
)
|
||||
for living_space in living_spaces:
|
||||
person = living_space.people
|
||||
user = prisma_service.find_first(table="users", query={"person_id": person.id, "active": True, "is_confirmed": True})
|
||||
user_of_person = None
|
||||
if user:
|
||||
user_of_person = User(
|
||||
id=user.id,
|
||||
uu_id=user.uu_id,
|
||||
user_tag=user.user_tag,
|
||||
user_type=user.user_type,
|
||||
email=user.email,
|
||||
phone_number=user.phone_number,
|
||||
related_company=user.related_company,
|
||||
is_confirmed=user.is_confirmed,
|
||||
active=user.active
|
||||
)
|
||||
person_obj = Person(
|
||||
id=person.id,
|
||||
uu_id=person.uu_id,
|
||||
firstname=person.firstname,
|
||||
surname=person.surname,
|
||||
middle_name=person.middle_name,
|
||||
birthname=person.birthname,
|
||||
is_confirmed=person.is_confirmed,
|
||||
active=person.active,
|
||||
user=user_of_person
|
||||
)
|
||||
occupant_type = living_space.occupant_types
|
||||
occupant_type_obj = OccupantType(
|
||||
id=occupant_type.id,
|
||||
uu_id=occupant_type.uu_id,
|
||||
occupant_code=occupant_type.occupant_code,
|
||||
occupant_type=occupant_type.occupant_type,
|
||||
is_confirmed=occupant_type.is_confirmed,
|
||||
active=occupant_type.active,
|
||||
user_type_uu_id=occupant_type.user_type_uu_id
|
||||
)
|
||||
living_space_obj = BuildLivingSpace(
|
||||
id=living_space.id,
|
||||
uu_id=living_space.uu_id,
|
||||
expiry_starts=str(living_space.expiry_starts),
|
||||
expiry_ends=str(living_space.expiry_ends),
|
||||
fix_value=float(living_space.fix_value),
|
||||
fix_percent=float(living_space.fix_percent),
|
||||
agreement_no=living_space.agreement_no,
|
||||
marketing_process=living_space.marketing_process,
|
||||
build_parts_id=living_space.build_parts_id,
|
||||
build_parts_uu_id=living_space.build_parts_uu_id,
|
||||
person_id=living_space.person_id,
|
||||
person_uu_id=living_space.person_uu_id,
|
||||
occupant_type_id=living_space.occupant_type_id,
|
||||
occupant_type_uu_id=living_space.occupant_type_uu_id,
|
||||
is_confirmed=living_space.is_confirmed,
|
||||
active=living_space.active,
|
||||
person=person_obj,
|
||||
occupant_types=occupant_type_obj
|
||||
)
|
||||
part_obj.living_spaces.append(living_space_obj)
|
||||
buildings_dict[str(build.id)].build_parts.append(part_obj)
|
||||
return {i: v.dict(exclude_none=True) for i, v in buildings_dict.items()}
|
||||
|
||||
|
||||
def get_all_companies_data(prisma_service: PrismaService):
|
||||
return prisma_service.find_many(table="companies", query={"active": True, "is_confirmed": True})
|
||||
|
||||
|
||||
def get_all_person_data_due_to_account_record(prisma_service: PrismaService):
|
||||
arriving_account_records = prisma_service.find_many(table="account_records", query={"is_predicted": False, "active": True, "is_confirmed": True, "approved_record": False, "currency_value": {"gt": 0}})
|
||||
debt_account_records = prisma_service.find_many(table="account_records", query={"is_predicted": False, "active": True, "is_confirmed": True, "approved_record": False, "currency_value": {"lt": 0}})
|
||||
return arriving_account_records, debt_account_records
|
||||
|
||||
|
||||
def check_if_any_account_record_added(prisma_service: PrismaService):
|
||||
any_record = prisma_service.find_first(table="account_records", query={"is_predicted": False, "active": True, "is_confirmed": True, "approved_record": False})
|
||||
return any_record is not None
|
||||
|
||||
|
||||
def check_if_any_building_added(prisma_service: PrismaService, build_id_list: list[str | int]):
|
||||
already_build_ids_list = [int(i) for i in build_id_list]
|
||||
any_building = prisma_service.find_first(table="build", query={"active": True, "is_confirmed": True, "id": {"not": {"in": already_build_ids_list} }})
|
||||
return any_building is not None
|
||||
|
||||
|
||||
def update_account_record_set_is_predict_true(prisma_service: PrismaService, account_record_id: int):
|
||||
return prisma_service.update(table="account_records", query={"id": account_record_id}, data={"is_predicted": True})
|
||||
|
||||
|
||||
def update_account_records(prisma_service: PrismaService, parsed_record: ParsedComment, collect_possible_parts_dict: dict[str, list[dict]]):
|
||||
payment_type_result = None
|
||||
if not parsed_record.people:
|
||||
return
|
||||
person = parsed_record.people[0]
|
||||
if parsed_record.payment_types:
|
||||
if parsed_record.payment_types[0] == "aidat":
|
||||
payment_type_result = prisma_service.find_first(table="api_enum_dropdown", query={"key":"BDT-D"})
|
||||
elif parsed_record.payment_types[0] == "tadilat":
|
||||
payment_type_result = prisma_service.find_first(table="api_enum_dropdown", query={"key":"BDT-R"})
|
||||
build_parts_id = collect_possible_parts_dict[str(person.id)][0]["id"]
|
||||
build_parts_uu_id = collect_possible_parts_dict[str(person.id)][0]["uu_id"]
|
||||
add_dict = {
|
||||
"build_parts": {"connect": {"id": int(build_parts_id)}}, "build_parts_uu_id": str(build_parts_uu_id),
|
||||
"people_account_records_send_person_idTopeople": {"connect": {"id": int(person.id)}}, "send_person_uu_id": str(person.uu_id), "is_predicted": True
|
||||
}
|
||||
if payment_type_result:
|
||||
add_dict["api_enum_dropdown_account_records_payment_result_typeToapi_enum_dropdown"] = {"connect": {"id": int(payment_type_result.id)}}
|
||||
add_dict["payment_result_type_uu_id"] = str(payment_type_result.uu_id)
|
||||
return prisma_service.update(table="account_records", where={"id": int(parsed_record.account_record_id)}, data=add_dict)
|
||||
|
||||
|
||||
def set_prediction_to_redis(process_comment_parser_service: ProcessCommentParserService, parsed_record: ParsedComment, possible: list[dict]):
|
||||
predict_account_records = process_comment_parser_service.get_predict_account_record()
|
||||
predict_account_records[str(parsed_record.account_record_id)] = {
|
||||
"account_record_id": parsed_record.account_record_id, "build_id": parsed_record.build_id, "payment_type": parsed_record.payment_types,
|
||||
"months": parsed_record.months, "years": parsed_record.years, "parts": parsed_record.parts, "predictions": possible,
|
||||
}
|
||||
process_comment_parser_service.set_predict_account_record(predict_account_record=predict_account_records)
|
||||
update_account_record_set_is_predict_true(prisma_service=prisma_service, account_record_id=parsed_record.account_record_id)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Process Comment Parser service started")
|
||||
renew = False
|
||||
prisma_service = PrismaService()
|
||||
process_comment_parser_service = ProcessCommentParserService()
|
||||
search_people = get_all_person_data_due_to_build(prisma_service)
|
||||
process_comment_parser_service.set_task_requirements(search_people)
|
||||
arriving_account_records, debt_account_records = get_all_person_data_due_to_account_record(prisma_service)
|
||||
try:
|
||||
while True:
|
||||
if not check_if_any_account_record_added(prisma_service) or renew:
|
||||
arriving_account_records, debt_account_records = get_all_person_data_due_to_account_record(prisma_service)
|
||||
renew = False
|
||||
print("Process Comment Parser service started sleeping for 5 seconds")
|
||||
tasks_dict: dict[str, BuildingCluster] = process_comment_parser_service.get_task_requirements()
|
||||
task_requirements: dict[str, BuildingCluster] = {idx: BuildingCluster(**value) for idx, value in tasks_dict.items()}
|
||||
if not check_if_any_building_added(prisma_service, list(task_requirements.keys())):
|
||||
search_people = get_all_person_data_due_to_build(prisma_service)
|
||||
process_comment_parser_service.set_task_requirements(search_people)
|
||||
parser = Parser(account_records=arriving_account_records, task_requirements=task_requirements)
|
||||
parsed_records = parser.parse()
|
||||
for parsed_record in parsed_records:
|
||||
collect_possible_parts_dict = {}
|
||||
if not parsed_record.people:
|
||||
continue
|
||||
for person in parsed_record.people:
|
||||
build_id = parsed_record.build_id
|
||||
person_id = person.id
|
||||
building = task_requirements[str(build_id)]
|
||||
for build_part in building.build_parts:
|
||||
for living_space in build_part.living_spaces:
|
||||
if str(living_space.person_id) == str(person_id):
|
||||
if str(person_id) in collect_possible_parts_dict:
|
||||
collect_possible_parts_dict[str(person_id)] = [*collect_possible_parts_dict[str(person_id)], build_part.dict()]
|
||||
else:
|
||||
collect_possible_parts_dict[str(person_id)] = [build_part.dict()]
|
||||
if len(collect_possible_parts_dict.keys()) == 1:
|
||||
for key, possible in collect_possible_parts_dict.items():
|
||||
if len(possible) == 1:
|
||||
update_account_records(prisma_service=prisma_service, parsed_record=parsed_record, collect_possible_parts_dict=collect_possible_parts_dict)
|
||||
else:
|
||||
set_prediction_to_redis(process_comment_parser_service=process_comment_parser_service, parsed_record=parsed_record, possible=possible)
|
||||
renew = True
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
print(f"Process Comment Parser service error: {str(e)}")
|
||||
raise e
|
||||
finally:
|
||||
prisma_service.disconnect()
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
VENV_PATH="/opt/venv"
|
||||
REQUIREMENTS_PATH="/app/requirements.txt"
|
||||
SCHEMA_PATH="/app/Depends/schema.prisma"
|
||||
PRISMA_BINARY_PATH="/root/.cache/prisma-python/binaries"
|
||||
|
||||
if [ ! -x "$VENV_PATH/bin/python" ]; then
|
||||
python -m venv "$VENV_PATH"
|
||||
"$VENV_PATH/bin/pip" install pip --upgrade
|
||||
"$VENV_PATH/bin/pip" install --no-cache-dir -r "$REQUIREMENTS_PATH"
|
||||
"$VENV_PATH/bin/prisma" generate --schema "$SCHEMA_PATH"
|
||||
fi
|
||||
|
||||
if ! find "$PRISMA_BINARY_PATH" -type f -name "prisma-query-engine-debian-openssl-3.0.x" | grep -q .; then
|
||||
"$VENV_PATH/bin/prisma" py fetch
|
||||
fi
|
||||
|
||||
exec "$VENV_PATH/bin/python" -u app.py
|
||||
@@ -0,0 +1,604 @@
|
||||
import pprint
|
||||
import re
|
||||
import arrow
|
||||
|
||||
from json import loads, dumps
|
||||
from unidecode import unidecode
|
||||
from models import BuildingCluster, Person
|
||||
|
||||
|
||||
turkish_months = ["OCAK", "ŞUBAT", "MART", "NİSAN", "MAYIS", "HAZİRAN", "TEMMUZ", "AĞUSTOS", "EYLÜL", "EKİM", "KASIM", "ARALIK"]
|
||||
turkish_months_abbr = {
|
||||
"OCA": "OCAK", "SUB": "ŞUBAT", "ŞUB": "ŞUBAT", "MAR": "MART", "NIS": "NİSAN", "MAY": "MAYIS", "HAZ": "HAZİRAN", "HZR": "HAZİRAN",
|
||||
"TEM": "TEMMUZ", "AGU": "AĞUSTOS", "AGT": "AĞUSTOS", "EYL": "EYLÜL", "EKI": "EKİM", "KAS": "KASIM", "ARA": "ARALIK", "AGUSTOS": "AĞUSTOS"
|
||||
}
|
||||
month_to_number_dict = {
|
||||
"ocak": 1, "şubat": 2, "mart": 3, "nisan": 4, "mayıs": 5, "haziran": 6, "temmuz": 7, "ağustos": 8, "eylül": 9, "ekim": 10, "kasım": 11, "aralık": 12,
|
||||
"ocak": 1, "subat": 2, "mart": 3, "nisan": 4, "mayis": 5, "haziran": 6, "temmuz": 7, "agustos": 8, "eylul": 9, "ekim": 10, "kasim": 11, "aralik": 12
|
||||
}
|
||||
start_year = 1950
|
||||
current_year = arrow.now().year
|
||||
|
||||
|
||||
class ParsedComment:
|
||||
|
||||
def __init__(self, account_record_id: int, org_comment: str, build_id: int) -> None:
|
||||
self.account_record_id: int = account_record_id
|
||||
self.org_comment: str = org_comment
|
||||
self.build_id: int = build_id
|
||||
self.comment: str = None
|
||||
self.people: list[dict] = []
|
||||
self.parts: list[dict] = []
|
||||
self.months: list[str] = []
|
||||
self.years: list[str] = []
|
||||
self.payment_types: list[str] = []
|
||||
|
||||
def set_people(self, people: list[dict]) -> None:
|
||||
self.people = people
|
||||
|
||||
def set_parts(self, parts: list[dict]) -> None:
|
||||
self.parts = parts
|
||||
|
||||
def set_months(self, months: list[str]) -> None:
|
||||
self.months = months
|
||||
|
||||
def set_years(self, years: list[str]) -> None:
|
||||
self.years = years
|
||||
|
||||
def set_payment_types(self, payment_types: list[str]) -> None:
|
||||
self.payment_types = payment_types
|
||||
|
||||
|
||||
class ParserHelpers:
|
||||
|
||||
@staticmethod
|
||||
def normalize_text(text: str) -> str:
|
||||
text = text.replace('İ', 'i')
|
||||
text = text.replace('I', 'ı')
|
||||
text = text.replace('Ş', 'ş')
|
||||
text = text.replace('Ğ', 'ğ')
|
||||
text = text.replace('Ü', 'ü')
|
||||
text = text.replace('Ö', 'ö')
|
||||
text = text.replace('Ç', 'ç')
|
||||
return unidecode(text).lower()
|
||||
|
||||
|
||||
class ParserRequirements(ParserHelpers):
|
||||
|
||||
def create_pattern(parts, formats, separators=None):
|
||||
"""
|
||||
parts: dict
|
||||
formats: list[list[tuple[str, str]]]
|
||||
separators: list[str]
|
||||
"""
|
||||
if separators is None:
|
||||
separators = [""]
|
||||
patterns = []
|
||||
for fmt in formats:
|
||||
for sep in separators:
|
||||
pattern_parts = []
|
||||
for part_type, part_name in fmt:
|
||||
if part_name in parts and part_type in parts[part_name]:
|
||||
pattern_parts.append(re.escape(parts[part_name][part_type]))
|
||||
if pattern_parts:
|
||||
patterns.append(r"\b" + sep.join(pattern_parts) + r"\b")
|
||||
return patterns
|
||||
|
||||
@classmethod
|
||||
def generate_dictonary_of_patterns(cls, person: Person):
|
||||
"""Completly remove middle_name instead do regex firstName + SomeWord + surname"""
|
||||
patterns_dict = {}
|
||||
person_patterns, firstname, birthname = set(), person.firstname.strip() if person.firstname else "", person.birthname.strip() if person.birthname else ""
|
||||
middle_name, surname = person.middle_name.strip() if person.middle_name else "", person.surname.strip() if person.surname else ""
|
||||
if not firstname or not surname:
|
||||
return patterns_dict
|
||||
name_parts = {
|
||||
'firstname': {'orig': firstname, 'norm': cls.normalize_text(firstname) if firstname else "", 'init': cls.normalize_text(firstname)[0] if firstname else ""},
|
||||
'surname': {'orig': surname, 'norm': cls.normalize_text(surname) if surname else "", 'init': cls.normalize_text(surname)[0] if surname else ""}
|
||||
}
|
||||
if middle_name:
|
||||
name_parts['middle_name'] = {'orig': middle_name, 'norm': cls.normalize_text(middle_name) if middle_name else "", 'init': cls.normalize_text(middle_name)[0] if middle_name else ""}
|
||||
if birthname and cls.normalize_text(birthname) != cls.normalize_text(surname):
|
||||
name_parts['birthname'] = {'orig': birthname, 'norm': cls.normalize_text(birthname), 'init': cls.normalize_text(birthname)[0] if birthname else ""}
|
||||
name_formats = [[('orig', 'firstname'), ('orig', 'surname')], [('norm', 'firstname'), ('norm', 'surname')], [('orig', 'surname'), ('orig', 'firstname')], [('norm', 'surname'), ('norm', 'firstname')]]
|
||||
if 'middle_name' in name_parts:
|
||||
name_formats = [[('orig', 'firstname'), ('orig', 'middle_name'), ('orig', 'surname')], [('norm', 'firstname'), ('norm', 'middle_name'), ('norm', 'surname')]]
|
||||
person_patterns.update(cls.create_pattern(name_parts, name_formats, [" ", ""]))
|
||||
if 'middle_name' in name_parts:
|
||||
middle_name_formats = [[('orig', 'firstname'), ('orig', 'middle_name')], [('norm', 'firstname'), ('norm', 'middle_name')], [('orig', 'middle_name'), ('orig', 'surname')], [('norm', 'middle_name'), ('norm', 'surname')],]
|
||||
person_patterns.update(cls.create_pattern(name_parts, middle_name_formats, [" ", ""]))
|
||||
if 'birthname' in name_parts and name_parts['surname']['orig'] != name_parts['birthname']['orig']:
|
||||
birthname_formats = [
|
||||
[('orig', 'firstname'), ('orig', 'birthname')], [('norm', 'firstname'), ('norm', 'birthname')],
|
||||
[('orig', 'birthname'), ('orig', 'firstname')], [('norm', 'birthname'), ('norm', 'firstname')]
|
||||
]
|
||||
person_patterns.update(cls.create_pattern(name_parts, birthname_formats, [" ", ""]))
|
||||
initial_formats = [[('init', 'firstname'), ('init', 'middle_name'), ('init', 'surname')], [('init', 'firstname'), ('init', 'surname')]]
|
||||
person_patterns.update(cls.create_pattern(name_parts, initial_formats, ["", ".", " ", ". "]))
|
||||
if 'middle_name' in name_parts:
|
||||
triple_initial_formats = [[('init', 'firstname'), ('init', 'middle_name'), ('init', 'surname')]]
|
||||
person_patterns.update(cls.create_pattern(name_parts, triple_initial_formats, ["", ".", " ", ". "]))
|
||||
compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in person_patterns]
|
||||
patterns_dict[str(person.id)] = compiled_patterns
|
||||
return patterns_dict
|
||||
|
||||
|
||||
class CommentParser(ParserHelpers):
|
||||
|
||||
def __init__(self, account_record, people_regex_dict: dict, people_dict: dict) -> None:
|
||||
self.original_comment: str = account_record.process_comment
|
||||
self.comment: str = self.clean_text(account_record.process_comment)
|
||||
self.people_regex_dict: dict = people_regex_dict
|
||||
self.people: dict = people_dict
|
||||
self.account_record_id: str = str(account_record.id)
|
||||
self.build_id: str = str(account_record.build_id)
|
||||
self.parsed_comment: ParsedComment = ParsedComment(account_record_id=self.account_record_id, org_comment=self.original_comment, build_id=self.build_id)
|
||||
|
||||
@staticmethod
|
||||
def clean_text_apartment_number(text: str, match):
|
||||
clean_text = text.replace(match.group(0), '').strip()
|
||||
clean_text = re.sub(r'\s+', ' ', clean_text).strip()
|
||||
return clean_text
|
||||
|
||||
@staticmethod
|
||||
def clean_text(text: str) -> str:
|
||||
text = str(text)
|
||||
text = re.sub(r'\d{8,}', ' ', text)
|
||||
# text = re.sub(r'\b[A-Za-z0-9]*?[0-9]+[A-Za-z0-9]*?[A-Za-z]+[A-Za-z0-9]*\b|\b[A-Za-z0-9]*?[A-Za-z]+[A-Za-z0-9]*?[0-9]+[A-Za-z0-9]*\b', ' ', text)
|
||||
text = text.replace("/", " ")
|
||||
text = text.replace("_", " ")
|
||||
text_remove_underscore = text.replace("-", " ").replace("+", " ")
|
||||
text_remove_asterisk = text_remove_underscore.replace("*", " ")
|
||||
text_remove_comma = text_remove_asterisk.replace(",", " ")
|
||||
text_remove_dots = text_remove_comma.replace(".", " ")
|
||||
text_remove_dots = re.sub(r'\s+', ' ', text_remove_dots)
|
||||
text_remove_dots = text_remove_dots.strip()
|
||||
return text_remove_dots
|
||||
|
||||
def get_people_regex_by_build_id(self) -> dict:
|
||||
"""
|
||||
Get people regex by build id
|
||||
"""
|
||||
return self.people_regex_dict.get(self.build_id, {})
|
||||
|
||||
def get_person(self, person_id: str) -> Person | None:
|
||||
return self.people[str(self.build_id)].get(person_id, None)
|
||||
|
||||
def parse_comment(self) -> ParsedComment:
|
||||
"""
|
||||
Parse comment and extract information
|
||||
"""
|
||||
self.extract_person_name_with_regex()
|
||||
self.extract_build_parts_info()
|
||||
self.extract_months()
|
||||
self.extract_years()
|
||||
self.extract_payment_type()
|
||||
self.comment = self.comment.strip()
|
||||
self.parsed_comment.comment = self.comment
|
||||
return self.parsed_comment
|
||||
|
||||
def get_text_initials(matched_text: str):
|
||||
return [unidecode(word.strip())[0].upper() for word in matched_text.split() if word.strip()]
|
||||
|
||||
def extract_person_name_with_regex(self):
|
||||
all_matches, found_dict = [], {}
|
||||
build_regex = self.get_people_regex_by_build_id()
|
||||
for person_id, patterns in build_regex.items():
|
||||
person_matches = []
|
||||
person = self.get_person(str(person_id))
|
||||
if not person:
|
||||
continue
|
||||
firstname_norm = str(self.normalize_text(person.firstname)).strip() if person.firstname else ""
|
||||
# middle_name_norm = str(self.normalize_text(person.middle_name)).strip() if person.middle_name else ""
|
||||
surname_norm = str(self.normalize_text(person.surname)).strip() if person.surname else ""
|
||||
birthname_norm = str(self.normalize_text(person.birthname)).strip() if person.birthname else ""
|
||||
text_norm = str(self.normalize_text(self.comment))
|
||||
for pattern in patterns[str(person_id)]:
|
||||
for match in pattern.finditer(text_norm):
|
||||
start, end = match.span()
|
||||
matched_text: str = self.comment[start:end]
|
||||
matched_text_norm = self.normalize_text(matched_text)
|
||||
is_valid_match = False
|
||||
if len(matched_text_norm.split()) <= 1:
|
||||
is_valid_match = False
|
||||
else:
|
||||
has_firstname = firstname_norm and firstname_norm in matched_text_norm
|
||||
has_surname = surname_norm and surname_norm in matched_text_norm
|
||||
has_birthname = birthname_norm and birthname_norm in matched_text_norm
|
||||
if (has_firstname and has_surname) or (has_firstname and has_birthname):
|
||||
is_valid_match = True
|
||||
if is_valid_match:
|
||||
person_matches.append({'matched_text': matched_text, 'start': start, 'end': end})
|
||||
if person_matches:
|
||||
person_matches.sort(key=lambda x: len(x['matched_text']), reverse=True)
|
||||
non_overlapping_matches = []
|
||||
for match in person_matches:
|
||||
overlaps = False
|
||||
for existing_match in non_overlapping_matches:
|
||||
if (match['start'] < existing_match['end'] and match['end'] > existing_match['start']):
|
||||
overlaps = True
|
||||
break
|
||||
if not overlaps:
|
||||
non_overlapping_matches.append(match)
|
||||
if non_overlapping_matches:
|
||||
found_dict["name_match"] = person
|
||||
all_matches.extend([(match, person) for match in non_overlapping_matches])
|
||||
if all_matches:
|
||||
all_matches.sort(key=lambda x: x[0]['start'], reverse=True)
|
||||
for match, person in all_matches:
|
||||
matched_text: str = match['matched_text']
|
||||
matched_words = matched_text.split()
|
||||
for word in matched_words:
|
||||
word_norm = str(self.normalize_text(word)).strip()
|
||||
if not word_norm:
|
||||
continue
|
||||
text_norm = self.normalize_text(self.comment)
|
||||
if not any([person_com for person_com in self.parsed_comment.people if str(person_com.id) == str(person.id)]):
|
||||
self.parsed_comment.people.append(person)
|
||||
for word_match in re.finditer(rf'\b{re.escape(word_norm)}\b', text_norm, re.IGNORECASE):
|
||||
start, end = word_match.span()
|
||||
self.comment = self.comment[:start] + ' ' * (end - start) + self.comment[end:]
|
||||
self.comment = re.sub(r'\s+', ' ', self.comment).strip()
|
||||
|
||||
def extract_build_parts_info(self):
|
||||
"""
|
||||
Daire numarasını çeşitli Türkçe yazım biçimlerinden tek regex ile ayıklar.
|
||||
Eşleşme bulunursa:
|
||||
- numarayı self.parsed_comment.parts'a ekler
|
||||
- metni temizler (senin clean_text_apartment_number metodunla)
|
||||
"""
|
||||
COMBINED_APT_PATTERN = re.compile(
|
||||
r"""
|
||||
\b(?:
|
||||
(?P<n1>\d+)\s*nolu\s*dair\w* # 2 nolu daire / 3 nolu dairenin
|
||||
| (?P<n2>\d+)\s*no\s*lu\s*dair\w* # 12 No lu daire
|
||||
| (?P<n3>\d+)nolu\s*dair\w* # 11nolu daire / 2NOLU DAIRE
|
||||
| (?P<n4>\d+)\s*numaral[ıi]\s*dai\w* # 9 numaralı dai/daire
|
||||
| dair[eé]?\s*no\.?\s*(?P<n5>\d+) # Daire No 12 / Daire No. 12
|
||||
| \bd\s*[:\-]?\s*(?P<n6>\d+) # D:10 / D-10
|
||||
| \bno\b(?!\s*lu)\s*[:\-]?\s*(?P<n7>\d+) # NO:11 / NO :3 (nolu hariç)
|
||||
| dair[eé]?\s*(?P<n8>\d+) # daire 3
|
||||
| (?P<n9>\d+)\s*numara # 9 NUMARA
|
||||
| \bno\s*/\s*(?P<n10>\d+) # NO/11
|
||||
| /(?P<n11>\d+) # /11
|
||||
)\b
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE
|
||||
)
|
||||
m = COMBINED_APT_PATTERN.search(self.comment)
|
||||
if not m:
|
||||
return
|
||||
for g in m.groups():
|
||||
if g:
|
||||
apartment_number = g
|
||||
break
|
||||
self.parsed_comment.parts.append(apartment_number)
|
||||
self.comment = self.clean_text_apartment_number(self.comment, m)
|
||||
return
|
||||
|
||||
def extract_months(self):
|
||||
"""
|
||||
Extract Turkish month names and abbreviations from the process comment
|
||||
"""
|
||||
original_text = self.comment
|
||||
working_text = original_text
|
||||
for month in turkish_months:
|
||||
pattern = re.compile(r'\b' + re.escape(month) + r'\b', re.IGNORECASE)
|
||||
for match in pattern.finditer(original_text):
|
||||
matched_text = match.group(0)
|
||||
normalized_month = self.normalize_text(month)
|
||||
month_number = None
|
||||
if month.lower() in month_to_number_dict:
|
||||
month_number = month_to_number_dict[month.lower()]
|
||||
elif normalized_month in month_to_number_dict:
|
||||
month_number = month_to_number_dict[normalized_month]
|
||||
month_info = {'name': month, 'number': month_number}
|
||||
self.parsed_comment.months.append(month_info)
|
||||
working_text = working_text.replace(matched_text, '', 1)
|
||||
|
||||
for abbr, full_month in turkish_months_abbr.items():
|
||||
pattern = re.compile(r'\b' + re.escape(abbr) + r'\b', re.IGNORECASE)
|
||||
for match in pattern.finditer(working_text):
|
||||
matched_text = match.group(0)
|
||||
normalized_month = self.normalize_text(full_month)
|
||||
month_number = None
|
||||
if full_month.lower() in month_to_number_dict:
|
||||
month_number = month_to_number_dict[full_month.lower()]
|
||||
elif normalized_month in month_to_number_dict:
|
||||
month_number = month_to_number_dict[normalized_month]
|
||||
month_info = {'name': full_month, 'number': month_number}
|
||||
self.parsed_comment.months.append(month_info)
|
||||
working_text = working_text.replace(matched_text, '', 1)
|
||||
self.comment = working_text
|
||||
|
||||
def extract_years(self):
|
||||
"""
|
||||
Extract years from the process comment
|
||||
"""
|
||||
original_text = self.comment
|
||||
working_text = original_text
|
||||
for year in range(start_year, current_year + 1):
|
||||
pattern = re.compile(r'\b' + str(year) + r'\b', re.IGNORECASE)
|
||||
for match in pattern.finditer(original_text):
|
||||
matched_text = match.group(0)
|
||||
if str(matched_text).isdigit():
|
||||
self.parsed_comment.years.append(int(matched_text))
|
||||
working_text = working_text.replace(matched_text, '', 1)
|
||||
self.comment = working_text
|
||||
|
||||
def extract_payment_type(self):
|
||||
"""
|
||||
Extract payment type from the process comment : aidat, AİD, aidatı, TADİLAT, YAKIT, yakıt, yakit
|
||||
"""
|
||||
original_text = self.comment
|
||||
working_text = original_text
|
||||
payment_keywords = {
|
||||
'aidat': ['aidat', 'aİd', 'aid', 'aidatı', 'aidati'],
|
||||
'tadilat': ['tadilat', 'tadİlat', 'tadilatı'],
|
||||
'yakit': ['yakit', 'yakıt', 'yakıtı', 'yakiti']
|
||||
}
|
||||
for payment_type, keywords in payment_keywords.items():
|
||||
for keyword in keywords:
|
||||
pattern = re.compile(r'\b' + keyword + r'\b', re.IGNORECASE)
|
||||
for match in pattern.finditer(original_text):
|
||||
matched_text = match.group(0)
|
||||
if payment_type not in self.parsed_comment.payment_types:
|
||||
self.parsed_comment.payment_types.append(payment_type)
|
||||
working_text = working_text.replace(matched_text, '', 1)
|
||||
self.comment = working_text
|
||||
|
||||
|
||||
class Parser:
|
||||
|
||||
def __init__(self, account_records: list, task_requirements: dict[str, BuildingCluster]) -> None:
|
||||
"""
|
||||
Initialize parser with account records and task requirements
|
||||
"""
|
||||
self.account_records: list = account_records
|
||||
self.task_requirements: dict[str, BuildingCluster] = task_requirements
|
||||
self.people_dict: dict[str, Person] = {}
|
||||
self.people_regex_dict: dict = self.prepare_people_regex_dict()
|
||||
self.parsed_records: list[ParsedComment] = []
|
||||
|
||||
def prepare_people_regex_dict(self):
|
||||
"""Prepare regex dictionary for people"""
|
||||
regex_pattern_dict = {}
|
||||
for build_id, build_cluster in self.task_requirements.items():
|
||||
for build_part in build_cluster.build_parts:
|
||||
for living_space in build_part.living_spaces:
|
||||
person: Person = living_space.person
|
||||
if str(build_id) in self.people_dict:
|
||||
if not str(person.id) in self.people_dict[str(build_id)]:
|
||||
self.people_dict[str(build_id)][str(person.id)] = person
|
||||
else:
|
||||
self.people_dict[str(build_id)] = {str(person.id): person}
|
||||
for build_id, people in self.people_dict.items():
|
||||
people: dict[str, Person] = people
|
||||
for person_id, person in people.items():
|
||||
if str(build_id) not in regex_pattern_dict:
|
||||
regex_pattern_dict[str(build_id)] = {}
|
||||
regex_pattern_dict[str(build_id)][str(person_id)] = ParserRequirements.generate_dictonary_of_patterns(person)
|
||||
return regex_pattern_dict
|
||||
|
||||
def parse(self):
|
||||
"""Parse account records based on task requirements"""
|
||||
for account_record in self.account_records:
|
||||
if not account_record.build_id:
|
||||
continue
|
||||
comment_parser = CommentParser(account_record=account_record, people_regex_dict=self.people_regex_dict, people_dict=self.people_dict)
|
||||
parsed_comment = comment_parser.parse_comment()
|
||||
self.parsed_records.append(parsed_comment)
|
||||
return self.parsed_records
|
||||
|
||||
|
||||
def commented_code():
|
||||
def main(account_records, people):
|
||||
|
||||
list_of_regex_patterns = generate_dictonary_of_patterns(people=people)
|
||||
dicts_found, dicts_not_found, count_extracted = dict(), dict(), 0
|
||||
for account_record in account_records:
|
||||
account_record_id = str(account_record["id"])
|
||||
found_dict = {}
|
||||
process_comment_iteration = clean_text(text=account_record["process_comment"])
|
||||
found_dict, cleaned_process_comment = extract_person_name_with_regex(found_dict=found_dict, process_comment=process_comment_iteration, patterns_dict=list_of_regex_patterns, people=people)
|
||||
found_dict, cleaned_process_comment = extract_build_parts_info(found_dict=found_dict, process_comment=cleaned_process_comment)
|
||||
found_dict, cleaned_process_comment = extract_months(found_dict=found_dict, process_comment=cleaned_process_comment)
|
||||
found_dict, cleaned_process_comment = extract_year(found_dict=found_dict, process_comment=cleaned_process_comment)
|
||||
found_dict, cleaned_process_comment = extract_payment_type(found_dict=found_dict, process_comment=cleaned_process_comment)
|
||||
if found_dict:
|
||||
dicts_found[str(account_record_id)] = found_dict
|
||||
else:
|
||||
dicts_not_found[str(account_record_id)] = account_record_id
|
||||
|
||||
for id_, item in dicts_found.items():
|
||||
months_are_valid = bool(item.get("months", []))
|
||||
years_are_valid = bool(item.get("years", []))
|
||||
payment_types_are_valid = bool(item.get("payment_types", []))
|
||||
apartment_number_are_valid = bool(item.get("apartment_number", []))
|
||||
person_name_are_valid = bool(item.get("name_match", []))
|
||||
account_record_to_save = AccountRecords.query.filter_by(id=int(id_)).first()
|
||||
save_dict = dict(account_records_id=account_record_to_save.id, account_records_uu_id=str(account_record_to_save.uu_id), prediction_model="regex", treshold=1, is_first_prediction=False)
|
||||
update_dict = dict(prediction_model="regex", treshold=1, is_first_prediction=False)
|
||||
if any([months_are_valid, years_are_valid, payment_types_are_valid, apartment_number_are_valid, person_name_are_valid]):
|
||||
count_extracted += 1
|
||||
if months_are_valid:
|
||||
print(f"months: {item['months']}")
|
||||
data_to_save = dumps({"data": item['months']})
|
||||
prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="months", prediction_model="regex").first()
|
||||
if not prediction_result:
|
||||
created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="months", prediction_result=data_to_save)
|
||||
created_account_prediction.save()
|
||||
else:
|
||||
prediction_result.update(**update_dict, prediction_result=data_to_save)
|
||||
prediction_result.save()
|
||||
if years_are_valid:
|
||||
print(f"years: {item['years']}")
|
||||
data_to_save = dumps({"data": item['years']})
|
||||
prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="years", prediction_model="regex").first()
|
||||
if not prediction_result:
|
||||
created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="years", prediction_result=data_to_save)
|
||||
created_account_prediction.save()
|
||||
else:
|
||||
prediction_result.update(**update_dict, prediction_result=data_to_save)
|
||||
prediction_result.save()
|
||||
if payment_types_are_valid:
|
||||
print(f"payment_types: {item['payment_types']}")
|
||||
data_to_save = dumps({"data": item['payment_types']})
|
||||
prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="payment_types", prediction_model="regex").first()
|
||||
if not prediction_result:
|
||||
created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="payment_types", prediction_result=data_to_save)
|
||||
created_account_prediction.save()
|
||||
else:
|
||||
prediction_result.update(**update_dict, prediction_result=data_to_save)
|
||||
prediction_result.save()
|
||||
if apartment_number_are_valid:
|
||||
print(f"apartment_number: {item['apartment_number']}")
|
||||
prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="apartment_number", prediction_model="regex").first()
|
||||
if not prediction_result:
|
||||
created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="apartment_number", prediction_result=item['apartment_number'])
|
||||
created_account_prediction.save()
|
||||
else:
|
||||
prediction_result.update(**update_dict, prediction_result=item['apartment_number'])
|
||||
prediction_result.save()
|
||||
if person_name_are_valid:
|
||||
print(f"person_name: {item['name_match']}")
|
||||
data_to_save = dumps({"data": item['name_match']})
|
||||
prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="person_name", prediction_model="regex").first()
|
||||
if not prediction_result:
|
||||
created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="person_name", prediction_result=data_to_save)
|
||||
created_account_prediction.save()
|
||||
else:
|
||||
prediction_result.update(**update_dict, prediction_result=data_to_save)
|
||||
prediction_result.save()
|
||||
|
||||
print("\n===== SUMMARY =====")
|
||||
print(f"extracted data total : {count_extracted}")
|
||||
print(f"not extracted data total : {len(account_records) - count_extracted}")
|
||||
print(f"Total account records processed : {len(account_records)}")
|
||||
|
||||
|
||||
|
||||
# def extract_build_parts_info(self):
|
||||
# """
|
||||
# Regex of parts such as :
|
||||
# 2 nolu daire
|
||||
# 9 NUMARALI DAI
|
||||
# daire 3
|
||||
# 3 nolu dairenin
|
||||
# 11nolu daire
|
||||
# Daire No 12
|
||||
# 2NOLU DAIRE
|
||||
# 12 No lu daire
|
||||
# D:10
|
||||
# NO:11
|
||||
# NO :3
|
||||
# """
|
||||
# apartment_number = None
|
||||
# pattern1 = re.compile(r'(\d+)\s*nolu\s*daire', re.IGNORECASE)
|
||||
# match = pattern1.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern4 = re.compile(r'(\d+)\s*nolu\s*daire\w*', re.IGNORECASE)
|
||||
# match = pattern4.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern5 = re.compile(r'(\d+)nolu\s*daire', re.IGNORECASE)
|
||||
# match = pattern5.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern7 = re.compile(r'(\d+)nolu\s*daire', re.IGNORECASE)
|
||||
# match = pattern7.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern8 = re.compile(r'(\d+)\s*no\s*lu\s*daire', re.IGNORECASE)
|
||||
# match = pattern8.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern6 = re.compile(r'daire\s*no\s*(\d+)', re.IGNORECASE)
|
||||
# match = pattern6.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern2 = re.compile(r'(\d+)\s*numarali\s*dai', re.IGNORECASE)
|
||||
# match = pattern2.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern3 = re.compile(r'daire\s*(\d+)', re.IGNORECASE)
|
||||
# match = pattern3.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern9 = re.compile(r'd\s*:\s*(\d+)', re.IGNORECASE)
|
||||
# match = pattern9.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# pattern10 = re.compile(r'no\s*:\s*(\d+)', re.IGNORECASE)
|
||||
# match = pattern10.search(self.comment)
|
||||
# if match:
|
||||
# apartment_number = match.group(1)
|
||||
# self.parsed_comment.parts.append(apartment_number)
|
||||
# self.comment = self.clean_text_apartment_number(self.comment, match)
|
||||
# return
|
||||
# # return found_dict, self.comment
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
|
||||
# people_query = sqlalchemy_text("""
|
||||
# SELECT DISTINCT ON (p.id) p.firstname, p.middle_name, p.surname, p.birthname, bl.id
|
||||
# FROM public.people as p
|
||||
# INNER JOIN public.build_living_space as bl ON bl.person_id = p.id
|
||||
# INNER JOIN public.build_parts as bp ON bp.id = bl.build_parts_id
|
||||
# INNER JOIN public.build as b ON b.id = bp.build_id
|
||||
# WHERE b.id = 1
|
||||
# ORDER BY p.id
|
||||
# """)
|
||||
|
||||
# people_raw = session.execute(people_query).all()
|
||||
# remove_duplicate = list()
|
||||
# clean_people_list = list()
|
||||
# for person in people_raw:
|
||||
# merged_name = f"{person[0]} {person[1]} {person[2]} {person[3]}"
|
||||
# if merged_name not in remove_duplicate:
|
||||
# clean_people_list.append(person)
|
||||
# remove_duplicate.append(merged_name)
|
||||
|
||||
# people = [{"firstname": p[0], "middle_name": p[1], "surname": p[2], "birthname": p[3], 'id': p[4]} for p in clean_people_list]
|
||||
# query_account_records = sqlalchemy_text("""
|
||||
# SELECT a.id, a.iban, a.bank_date, a.process_comment FROM public.account_records as a where currency_value > 0
|
||||
# """) # and bank_date::date >= '2020-01-01'
|
||||
# account_records = session.execute(query_account_records).all()
|
||||
# account_records = [{"id": ar[0], "iban": ar[1], "bank_date": ar[2], "process_comment": ar[3]} for ar in account_records]
|
||||
|
||||
# try:
|
||||
# main(session=session, account_records=account_records, people=people)
|
||||
# except Exception as e:
|
||||
# print(f"{e}")
|
||||
|
||||
# session.close()
|
||||
# session_factory.remove()
|
||||
@@ -0,0 +1,93 @@
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
user_tag: str
|
||||
user_type: str
|
||||
email: str
|
||||
phone_number: str
|
||||
related_company: str
|
||||
is_confirmed: bool
|
||||
active: bool
|
||||
|
||||
|
||||
class Person(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
firstname: str
|
||||
surname: str
|
||||
middle_name: Optional[str] = ""
|
||||
birthname: Optional[str] = ""
|
||||
# national_identity_id: str
|
||||
is_confirmed: bool
|
||||
active: bool
|
||||
user: Optional[User] = None
|
||||
|
||||
|
||||
class OccupantType(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
occupant_code: str
|
||||
occupant_type: str
|
||||
is_confirmed: bool
|
||||
active: bool
|
||||
user_type_uu_id: Optional[str] = None
|
||||
|
||||
|
||||
class BuildPart(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
part_no: str
|
||||
part_level: str
|
||||
part_code: str
|
||||
part_gross_size: float
|
||||
part_net_size: float
|
||||
human_livable: bool
|
||||
build_id: int
|
||||
build_uu_id: str
|
||||
is_confirmed: bool
|
||||
active: bool
|
||||
living_spaces: Optional[List['BuildLivingSpace']] = None
|
||||
|
||||
|
||||
class BuildLivingSpace(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
expiry_starts: str
|
||||
expiry_ends: str
|
||||
fix_value: float
|
||||
fix_percent: float
|
||||
agreement_no: str
|
||||
marketing_process: bool
|
||||
build_parts_id: int
|
||||
build_parts_uu_id: str
|
||||
person_id: int
|
||||
person_uu_id: str
|
||||
occupant_type_id: int
|
||||
occupant_type_uu_id: str
|
||||
is_confirmed: bool
|
||||
active: bool
|
||||
person: Optional[Person] = None
|
||||
occupant_type: Optional[OccupantType] = None
|
||||
|
||||
|
||||
class BuildingCluster(BaseModel):
|
||||
id: int
|
||||
uu_id: str
|
||||
build_name: str
|
||||
build_no: str
|
||||
build_date: str
|
||||
decision_period_date: str
|
||||
expiry_starts: str
|
||||
expiry_ends: str
|
||||
is_confirmed: bool
|
||||
active: bool
|
||||
build_parts: List['BuildPart'] = []
|
||||
|
||||
|
||||
# Update forward references for models with circular dependencies
|
||||
BuildPart.update_forward_refs()
|
||||
BuildingCluster.update_forward_refs()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user