Compare commits

...

25 Commits

Author SHA1 Message Date
9dd8740171 redis handler implemented Servies Task has redis object 2025-08-20 22:25:24 +03:00
4e6774a15b updated Services Task database 2025-08-19 20:17:14 +03:00
e4f6afbc93 parser excel publish chain task build 2025-08-18 18:31:29 +03:00
61529f7d94 rabbitmq implemented and tested 2025-08-17 21:14:46 +03:00
9543d136aa task services added 2025-08-15 22:30:21 +03:00
456203f5cf Comment Parser Regex service completed 2025-08-14 21:20:40 +03:00
82b1d4825b updated Parser comment 2025-08-14 00:11:38 +03:00
4ec9031005 updated Parser comment 2025-08-14 00:10:57 +03:00
7a5521648c Comment service added mail sender service 2025-08-11 20:22:26 +03:00
ca98adc338 Service Runner Finder and complete task chain completed 2025-08-11 19:26:49 +03:00
405ba2e95d ignore added 2025-08-10 11:36:07 +03:00
7452e05a92 venv and prisma cahce deleted 2025-08-10 11:30:56 +03:00
bd12fe02ae updated async prisma runner 2025-08-10 11:29:09 +03:00
a00c2942f5 async prisma runner completed 2025-08-10 11:23:18 +03:00
768f0a5daf updated prisma service async runner 2025-08-10 11:18:00 +03:00
c2fd263f27 Revert "updated Service Runner email Reader"
This reverts commit 81184a8acc.
2025-08-10 11:14:54 +03:00
ac1980566a Revert "updated prisma service async runner"
This reverts commit db0ae34948.
2025-08-10 11:05:45 +03:00
db0ae34948 updated prisma service async runner 2025-08-10 11:01:26 +03:00
81184a8acc updated Service Runner email Reader 2025-08-08 17:15:19 +03:00
a830cc079d page retriever and menu @redis added 2025-08-07 13:04:02 +03:00
a986ddbb95 added pages tested via backend 2025-08-06 16:33:03 +03:00
9232da69d3 updated event controllers and service event mtach tested 2025-08-05 14:42:24 +03:00
aa8f0b8f31 updated accounts service navigator 2025-08-03 18:23:26 +03:00
1b87dee60d updated events 2025-08-02 20:08:43 +03:00
b54bbe2db2 updated select response 2025-07-31 22:37:40 +03:00
200 changed files with 19808 additions and 1082 deletions

8
.gitignore vendored
View File

@@ -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
View File

@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}

0
=9.4.1 Normal file
View File

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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")

View File

@@ -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) }
}

View File

@@ -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() { }
}

View File

@@ -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 },
});
// }
// })
// }
// )
}
}

View File

@@ -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();
});
});

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

View File

@@ -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,

View File

@@ -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 { }

View File

@@ -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,

View File

@@ -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(),
}),
},
});
}

View File

@@ -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) {

View File

@@ -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: {

View File

@@ -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') }
}
}

View 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 {}

View 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');
},
};

View 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();
});
});

View 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();
}
}

View File

@@ -0,0 +1,2 @@
export const REDIS_CLIENT = 'REDIS_CLIENT';
export const REDIS_OPTIONS = 'REDIS_OPTIONS';

View 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[];
}

View 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() }
}
}

View 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();
});
});

View File

@@ -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;
}
/**

View File

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

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

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

View 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();
});
});

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

View 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));

View 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))
};

View 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();
});
});

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

View 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: [],
}
],
}
];

View 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: [],
},
]

View 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));

View 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();
});
});

View 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)
}
}

View 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() { }
}

View 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();
});
});

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

View File

@@ -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>;

View File

@@ -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 { }

View File

@@ -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({

View 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.`) }
}
}

View 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class MongoHandler {
constructor() { }
}

View File

@@ -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> {

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

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

View File

@@ -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 { }

View File

@@ -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": {
"@/*": ["*"]
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
</>
)
}
}

View File

@@ -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>
);
}

View File

@@ -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}</>;
}

View File

@@ -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>
</>;
};

View File

@@ -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>
</>;
};

View File

@@ -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>
</>
);
}

View File

@@ -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 />;
}

View File

@@ -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" });

View File

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

View File

@@ -0,0 +1,5 @@
import { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return children;
}

View File

@@ -0,0 +1,8 @@
'use server'
import { redirect } from '@/i18n/navigation';
const RedirectHome = async () => {
return redirect({ locale: "en", href: "/" })
}
export default RedirectHome

View File

@@ -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",

View File

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

View File

@@ -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,

View File

@@ -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] });
}

View File

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

View 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.');
}

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

View File

@@ -0,0 +1,11 @@
'use client';
const DashboardPtnZblJTri0DnlQaUOikQx: React.FC = () => {
return (
<div>
<h1>DashboardPtnZblJTri0DnlQaUOikQx</h1>
</div>
);
}
export default DashboardPtnZblJTri0DnlQaUOikQx;

View File

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

View File

@@ -0,0 +1,4 @@
import {dashboardPages} from "./dashboard/mapper";
export const employeeMapper = {
...dashboardPages
}

View 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[];
}
}

View File

@@ -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;

View File

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

View File

@@ -0,0 +1,5 @@
import { dashboardPages } from "./dashboard/mapper";
export const occupantMapper = {
...dashboardPages,
}

View File

@@ -0,0 +1,14 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.db
*.sqlite3
*.log
*.env
venv/
.env.*
node_modules/
.prisma/
.prisma-cache/
ServicesRunnner/AccountRecordServices/Test/venv/

View File

@@ -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"]

View 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

View File

@@ -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

View File

@@ -0,0 +1,14 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.db
*.sqlite3
*.log
*.env
venv/
.env.*
node_modules/
.prisma/
.prisma-cache/
ServicesRunnner/AccountRecordServices/Test/venv/

View 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"]

View 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()

View File

@@ -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

View File

@@ -0,0 +1 @@
3.12

View File

@@ -0,0 +1,14 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.db
*.sqlite3
*.log
*.env
venv/
.env.*
node_modules/
.prisma/
.prisma-cache/
ServicesRunnner/AccountRecordServices/Test/venv/

View File

@@ -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"]

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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