Merge branch 'develop' into dev
* develop: fix log (#22) update "place" insert (#21) add budgetSourceOther Merge pull request #20 from Frappet/task/#1839 fix update sort sort พัฒนาบุคลากร
This commit is contained in:
commit
80f929dbbd
6 changed files with 195 additions and 67 deletions
22
.github/workflows/discord-notify.yml
vendored
Normal file
22
.github/workflows/discord-notify.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: Discord PR Notify
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
discord:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Send Discord
|
||||||
|
run: |
|
||||||
|
curl -X POST "${{ secrets.DISCORD_WEBHOOK_PULLREQUEST }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"embeds": [{
|
||||||
|
"title": "🔔 **Service:** ${{ github.repository }}",
|
||||||
|
"description": "👤 **Author:** ${{ github.event.pull_request.user.login }}\n🌿 **Branch:** ${{ github.event.pull_request.head.ref }} → ${{ github.event.pull_request.base.ref }}\n📦 **Pull Request:** [#${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }})",
|
||||||
|
"color": 5814783,
|
||||||
|
"timestamp": "${{ github.event.pull_request.created_at }}"
|
||||||
|
}]
|
||||||
|
}'
|
||||||
|
|
@ -2022,6 +2022,8 @@ export class DevelopmentController extends Controller {
|
||||||
@Query("nodeId") nodeId?: string | null,
|
@Query("nodeId") nodeId?: string | null,
|
||||||
@Query("node") node?: number | null,
|
@Query("node") node?: number | null,
|
||||||
@Query("keyword") keyword?: string,
|
@Query("keyword") keyword?: string,
|
||||||
|
@Query("sortBy") sortBy?: string,
|
||||||
|
@Query("descending") descending?: boolean,
|
||||||
) {
|
) {
|
||||||
let _data = await new permission().PermissionOrgList(request, "SYS_DEV_PROJECT");
|
let _data = await new permission().PermissionOrgList(request, "SYS_DEV_PROJECT");
|
||||||
await new CallAPI()
|
await new CallAPI()
|
||||||
|
|
@ -2030,7 +2032,7 @@ export class DevelopmentController extends Controller {
|
||||||
_data = x;
|
_data = x;
|
||||||
})
|
})
|
||||||
.catch((x) => {});
|
.catch((x) => {});
|
||||||
const [development, total] = await AppDataSource.getRepository(Development)
|
let query = await AppDataSource.getRepository(Development)
|
||||||
.createQueryBuilder("development")
|
.createQueryBuilder("development")
|
||||||
.andWhere(year > 0 ? "development.year LIKE :year" : "1=1", {
|
.andWhere(year > 0 ? "development.year LIKE :year" : "1=1", {
|
||||||
year: `${year.toString()}`,
|
year: `${year.toString()}`,
|
||||||
|
|
@ -2129,8 +2131,18 @@ export class DevelopmentController extends Controller {
|
||||||
"development.child3",
|
"development.child3",
|
||||||
"development.child4",
|
"development.child4",
|
||||||
])
|
])
|
||||||
.orderBy("development.year", "DESC")
|
|
||||||
.addOrderBy("development.createdAt", "DESC")
|
if (sortBy) {
|
||||||
|
query = query.orderBy(
|
||||||
|
`development.${sortBy}`,
|
||||||
|
descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
query = query.orderBy("development.year", "DESC")
|
||||||
|
.addOrderBy("development.createdAt", "DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [development, total] = await query
|
||||||
.skip((page - 1) * pageSize)
|
.skip((page - 1) * pageSize)
|
||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
@ -2617,6 +2629,18 @@ export class DevelopmentController extends Controller {
|
||||||
where: { developmentId: id, isDone: false },
|
where: { developmentId: id, isDone: false },
|
||||||
relations: ["development"],
|
relations: ["development"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const developmentAddresss = await this.developmentAddresssRepository.find({
|
||||||
|
where: { developmentId: id}
|
||||||
|
})
|
||||||
|
|
||||||
|
const places = developmentAddresss
|
||||||
|
.map(addr =>
|
||||||
|
`- ${addr.address}, ${addr.addressType === "IN_COUNTRY" ? addr.provinceName : addr.country} (${addr.addressType === "IN_COUNTRY" ? "ภายในประเทศ" : "ภายนอกประเทศ"})`
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
getDevelopment.map(async (x) => {
|
getDevelopment.map(async (x) => {
|
||||||
const _data = Object.assign(new DevelopmentHistory(), x);
|
const _data = Object.assign(new DevelopmentHistory(), x);
|
||||||
|
|
@ -2627,7 +2651,7 @@ export class DevelopmentController extends Controller {
|
||||||
name: x.development == null ? null : x.development.projectName,
|
name: x.development == null ? null : x.development.projectName,
|
||||||
topic: x.development == null ? null : x.development.topicAcademic,
|
topic: x.development == null ? null : x.development.topicAcademic,
|
||||||
yearly: x.development == null ? null : x.development.year,
|
yearly: x.development == null ? null : x.development.year,
|
||||||
place: x.development == null ? null : x.development.addressAcademic,
|
place: places == null ? null : places,
|
||||||
duration: x.trainingDays,
|
duration: x.trainingDays,
|
||||||
department: x.development == null ? null : x.development.root,
|
department: x.development == null ? null : x.development.root,
|
||||||
numberOrder: x.order,
|
numberOrder: x.order,
|
||||||
|
|
@ -2650,7 +2674,7 @@ export class DevelopmentController extends Controller {
|
||||||
name: x.development == null ? null : x.development.projectName,
|
name: x.development == null ? null : x.development.projectName,
|
||||||
topic: x.development == null ? null : x.development.topicAcademic,
|
topic: x.development == null ? null : x.development.topicAcademic,
|
||||||
yearly: x.development == null ? null : x.development.year,
|
yearly: x.development == null ? null : x.development.year,
|
||||||
place: x.development == null ? null : x.development.addressAcademic,
|
place: places == null ? null : places,
|
||||||
duration: x.trainingDays,
|
duration: x.trainingDays,
|
||||||
department: x.development == null ? null : x.development.root,
|
department: x.development == null ? null : x.development.root,
|
||||||
numberOrder: x.order,
|
numberOrder: x.order,
|
||||||
|
|
|
||||||
|
|
@ -249,11 +249,13 @@ export class DevelopmentEmployeeHistoryController extends Controller {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
root: string | null;
|
root: string | null;
|
||||||
|
sortBy?: string;
|
||||||
|
descending?: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
await new permission().PermissionList(request, "SYS_DEV_HISTORY_EMP");
|
await new permission().PermissionList(request, "SYS_DEV_HISTORY_EMP");
|
||||||
const type = "EMPLOYEE";
|
const type = "EMPLOYEE";
|
||||||
const [development, total] = await AppDataSource.getRepository(DevelopmentHistory)
|
let query = await AppDataSource.getRepository(DevelopmentHistory)
|
||||||
.createQueryBuilder("developmentHistory")
|
.createQueryBuilder("developmentHistory")
|
||||||
.leftJoinAndSelect("developmentHistory.development", "development")
|
.leftJoinAndSelect("developmentHistory.development", "development")
|
||||||
.leftJoinAndSelect("developmentHistory.employeePosLevel", "employeePosLevel")
|
.leftJoinAndSelect("developmentHistory.employeePosLevel", "employeePosLevel")
|
||||||
|
|
@ -326,8 +328,39 @@ export class DevelopmentEmployeeHistoryController extends Controller {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.orderBy("development.year", "DESC")
|
|
||||||
.addOrderBy("developmentHistory.createdAt", "DESC")
|
if (body.sortBy) {
|
||||||
|
if(body.sortBy === "year" || body.sortBy === "projectName"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`development.${body.sortBy}`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(body.sortBy === "posType"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`employeePosType.posTypeName`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(body.sortBy === "posLevel"){
|
||||||
|
query = query
|
||||||
|
.orderBy(`employeePosType.posTypeShortName`,body.descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`employeePosLevel.posLevelName`,body.descending ? "DESC" : "ASC")
|
||||||
|
}else if(body.sortBy === "fullName"){
|
||||||
|
query = query
|
||||||
|
.orderBy(`developmentHistory.prefix`,body.descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`developmentHistory.firstName`,body.descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`developmentHistory.lastName`,body.descending ? "DESC" : "ASC")
|
||||||
|
}else{
|
||||||
|
query = query.orderBy(
|
||||||
|
`developmentHistory.${body.sortBy}`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
query = query.orderBy("development.year", "DESC")
|
||||||
|
.addOrderBy("developmentHistory.createdAt", "DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [development, total] = await query
|
||||||
.skip((body.page - 1) * body.pageSize)
|
.skip((body.page - 1) * body.pageSize)
|
||||||
.take(body.pageSize)
|
.take(body.pageSize)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
|
||||||
|
|
@ -241,11 +241,13 @@ export class DevelopmentOfficerHistoryController extends Controller {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
root: string | null;
|
root: string | null;
|
||||||
|
sortBy?: string;
|
||||||
|
descending?: boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
await new permission().PermissionList(request, "SYS_DEV_HISTORY_OFFICER");
|
await new permission().PermissionList(request, "SYS_DEV_HISTORY_OFFICER");
|
||||||
const type = "OFFICER";
|
const type = "OFFICER";
|
||||||
const [development, total] = await AppDataSource.getRepository(DevelopmentHistory)
|
let query = await AppDataSource.getRepository(DevelopmentHistory)
|
||||||
.createQueryBuilder("developmentHistory")
|
.createQueryBuilder("developmentHistory")
|
||||||
.leftJoinAndSelect("developmentHistory.development", "development")
|
.leftJoinAndSelect("developmentHistory.development", "development")
|
||||||
.leftJoinAndSelect("developmentHistory.posLevel", "posLevel")
|
.leftJoinAndSelect("developmentHistory.posLevel", "posLevel")
|
||||||
|
|
@ -314,11 +316,44 @@ export class DevelopmentOfficerHistoryController extends Controller {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.orderBy("development.year", "DESC")
|
|
||||||
.addOrderBy("developmentHistory.createdAt", "DESC")
|
if (body.sortBy) {
|
||||||
|
if(body.sortBy === "year" || body.sortBy === "root" || body.sortBy === "projectName"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`development.${body.sortBy}`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(body.sortBy === "posType"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`posType.posTypeName`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(body.sortBy === "posLevel"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`posLevel.posLevelName`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(body.sortBy === "fullName"){
|
||||||
|
query = query
|
||||||
|
.orderBy(`developmentHistory.prefix`,body.descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`developmentHistory.firstName`,body.descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`developmentHistory.lastName`,body.descending ? "DESC" : "ASC")
|
||||||
|
}else{
|
||||||
|
query = query.orderBy(
|
||||||
|
`developmentHistory.${body.sortBy}`,
|
||||||
|
body.descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
query = query.orderBy("development.year", "DESC")
|
||||||
|
.addOrderBy("developmentHistory.createdAt", "DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [development, total] = await query
|
||||||
.skip((body.page - 1) * body.pageSize)
|
.skip((body.page - 1) * body.pageSize)
|
||||||
.take(body.pageSize)
|
.take(body.pageSize)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
const formattedData = development.map((item) => ({
|
const formattedData = development.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
citizenId: item.citizenId,
|
citizenId: item.citizenId,
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,8 @@ export class DevelopmentScholarshipController extends Controller {
|
||||||
@Query("keyword") keyword?: string,
|
@Query("keyword") keyword?: string,
|
||||||
@Query("year") year?: number,
|
@Query("year") year?: number,
|
||||||
@Query("scholarshipType") scholarshipType?: string,
|
@Query("scholarshipType") scholarshipType?: string,
|
||||||
|
@Query("sortBy") sortBy?: string,
|
||||||
|
@Query("descending") descending?: boolean,
|
||||||
) {
|
) {
|
||||||
let _data = await new permission().PermissionOrgList(request, "SYS_DEV_SCHOLARSHIP");
|
let _data = await new permission().PermissionOrgList(request, "SYS_DEV_SCHOLARSHIP");
|
||||||
await new CallAPI()
|
await new CallAPI()
|
||||||
|
|
@ -168,7 +170,7 @@ export class DevelopmentScholarshipController extends Controller {
|
||||||
_data = x;
|
_data = x;
|
||||||
})
|
})
|
||||||
.catch((x) => {});
|
.catch((x) => {});
|
||||||
const [development, total] = await AppDataSource.getRepository(DevelopmentScholarship)
|
let query = await AppDataSource.getRepository(DevelopmentScholarship)
|
||||||
.createQueryBuilder("developmentScholarship")
|
.createQueryBuilder("developmentScholarship")
|
||||||
.leftJoinAndSelect("developmentScholarship.posLevel", "posLevel")
|
.leftJoinAndSelect("developmentScholarship.posLevel", "posLevel")
|
||||||
.leftJoinAndSelect("developmentScholarship.posType", "posType")
|
.leftJoinAndSelect("developmentScholarship.posType", "posType")
|
||||||
|
|
@ -242,11 +244,44 @@ export class DevelopmentScholarshipController extends Controller {
|
||||||
root: _data.root,
|
root: _data.root,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.orderBy("developmentScholarship.scholarshipYear", "DESC")
|
|
||||||
.addOrderBy("developmentScholarship.createdAt", "DESC")
|
if (sortBy) {
|
||||||
|
if(sortBy === "posType"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`posType.posTypeName`,
|
||||||
|
descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(sortBy === "posLevel"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`posLevel.posLevelName`,
|
||||||
|
descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(sortBy === "year"){
|
||||||
|
query = query.orderBy(
|
||||||
|
`developmentScholarship.scholarshipYear`,
|
||||||
|
descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}else if(sortBy === "fullName"){
|
||||||
|
query = query
|
||||||
|
.orderBy(`developmentScholarship.prefix`,descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`developmentScholarship.firstName`,descending ? "DESC" : "ASC")
|
||||||
|
.addOrderBy(`developmentScholarship.lastName`,descending ? "DESC" : "ASC")
|
||||||
|
}else{
|
||||||
|
query = query.orderBy(
|
||||||
|
`developmentScholarship.${sortBy}`,
|
||||||
|
descending ? "DESC" : "ASC"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
query = query.orderBy("developmentScholarship.scholarshipYear", "DESC")
|
||||||
|
.addOrderBy("developmentScholarship.createdAt", "DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [development, total] = await query
|
||||||
.skip((page - 1) * pageSize)
|
.skip((page - 1) * pageSize)
|
||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
.getManyAndCount();
|
.getManyAndCount();
|
||||||
|
|
||||||
const formattedData = development.map((item) => ({
|
const formattedData = development.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
year: item.scholarshipYear,
|
year: item.scholarshipYear,
|
||||||
|
|
@ -338,6 +373,7 @@ export class DevelopmentScholarshipController extends Controller {
|
||||||
: null,
|
: null,
|
||||||
scholarshipYear: getDevelopment.scholarshipYear ? getDevelopment.scholarshipYear : null,
|
scholarshipYear: getDevelopment.scholarshipYear ? getDevelopment.scholarshipYear : null,
|
||||||
budgetSource: getDevelopment.budgetSource ? getDevelopment.budgetSource : null,
|
budgetSource: getDevelopment.budgetSource ? getDevelopment.budgetSource : null,
|
||||||
|
budgetSourceOther: getDevelopment.budgetSourceOther ? getDevelopment.budgetSourceOther : null,
|
||||||
budgetApprove: getDevelopment.budgetApprove ? getDevelopment.budgetApprove : null,
|
budgetApprove: getDevelopment.budgetApprove ? getDevelopment.budgetApprove : null,
|
||||||
bookNo: getDevelopment.bookNo ? getDevelopment.bookNo : null,
|
bookNo: getDevelopment.bookNo ? getDevelopment.bookNo : null,
|
||||||
bookNoDate: getDevelopment.bookNoDate ? getDevelopment.bookNoDate : null,
|
bookNoDate: getDevelopment.bookNoDate ? getDevelopment.bookNoDate : null,
|
||||||
|
|
|
||||||
|
|
@ -184,61 +184,39 @@ class CheckAuth {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public async checkOrg(token: any, keycloakId: string) {
|
public async checkOrg(token: any, keycloakId: string) {
|
||||||
try {
|
const redisClient = await this.redis.createClient({
|
||||||
// Validate required environment variables
|
host: process.env.REDIS_HOST,
|
||||||
const REDIS_HOST = process.env.REDIS_HOST;
|
port: process.env.REDIS_PORT,
|
||||||
const REDIS_PORT = process.env.REDIS_PORT ? Number(process.env.REDIS_PORT) : 6379;
|
})
|
||||||
|
const getAsync = promisify(redisClient.get).bind(redisClient)
|
||||||
|
try {
|
||||||
|
let reply = await getAsync("org_" + keycloakId)
|
||||||
|
if (reply != null) {
|
||||||
|
reply = JSON.parse(reply)
|
||||||
|
} else {
|
||||||
|
if (!keycloakId) throw new Error("No KeycloakId provided")
|
||||||
|
const x = await new CallAPI().GetData(
|
||||||
|
{
|
||||||
|
headers: { authorization: token },
|
||||||
|
},
|
||||||
|
`/org/permission/checkOrg/${keycloakId}`,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
if (!REDIS_HOST) {
|
const data = {
|
||||||
throw new Error("REDIS_HOST is not set in environment variables");
|
orgRootId: x.orgRootId,
|
||||||
}
|
orgChild1Id: x.orgChild1Id,
|
||||||
|
orgChild2Id: x.orgChild2Id,
|
||||||
|
orgChild3Id: x.orgChild3Id,
|
||||||
|
orgChild4Id: x.orgChild4Id,
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[REDIS] Connecting to Redis at ${REDIS_HOST}:${REDIS_PORT}`);
|
return data
|
||||||
|
}
|
||||||
// Create Redis client
|
} catch (error) {
|
||||||
const redisClient = this.redis.createClient({
|
console.error("Error calling API:", error)
|
||||||
socket: {
|
throw error
|
||||||
host: REDIS_HOST,
|
}
|
||||||
port: REDIS_PORT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
redisClient.on("error", (err: any) => {
|
|
||||||
console.error("[REDIS] Connection error:", err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
await redisClient.connect();
|
|
||||||
console.log("[REDIS] Connected successfully!");
|
|
||||||
|
|
||||||
const getAsync = promisify(redisClient.get).bind(redisClient);
|
|
||||||
|
|
||||||
let reply = await getAsync("org_" + keycloakId);
|
|
||||||
if (reply != null) {
|
|
||||||
reply = JSON.parse(reply);
|
|
||||||
} else {
|
|
||||||
if (!keycloakId) throw new Error("No KeycloakId provided");
|
|
||||||
const x = await new CallAPI().GetData(
|
|
||||||
{
|
|
||||||
headers: { authorization: token },
|
|
||||||
},
|
|
||||||
`/org/permission/checkOrg/${keycloakId}`,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
orgRootId: x.orgRootId,
|
|
||||||
orgChild1Id: x.orgChild1Id,
|
|
||||||
orgChild2Id: x.orgChild2Id,
|
|
||||||
orgChild3Id: x.orgChild3Id,
|
|
||||||
orgChild4Id: x.orgChild4Id,
|
|
||||||
};
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error calling API:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
public async PermissionCreate(req: RequestWithUser, system: string) {
|
public async PermissionCreate(req: RequestWithUser, system: string) {
|
||||||
return await this.Permission(req, system, "CREATE");
|
return await this.Permission(req, system, "CREATE");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue