Compare commits

..

2 Commits

28 changed files with 2975 additions and 173 deletions

968
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -102,6 +102,7 @@ export interface DailyReportRequest {
designation?: string | undefined;
employeeName?: string | undefined;
employeeCode?: string | undefined;
fileType: string;
}
export interface DailySummary {
@ -144,23 +145,15 @@ export interface RangeReportRequest {
fromDate: string;
toDate: string;
branchId?: number | undefined;
departmentId?:
| number
| undefined;
/** NEW */
companyId?:
| number
| undefined;
/** NEW */
designation?:
departmentId?: number | undefined;
companyId?: number | undefined;
designation?: string | undefined;
employeeName?: string | undefined;
employeeCode?:
| string
| undefined;
/** NEW */
employeeName?:
| string
| undefined;
/** NEW */
employeeCode?: string | undefined;
/** <--- ADD THIS LINE */
fileType: string;
}
export interface RangeMetrics {
@ -277,6 +270,7 @@ export interface PendingRegData {
firstName: string;
lastName: string;
branchName: string;
reviewerName: string;
}
export interface PendingRegResponse {
@ -292,6 +286,17 @@ export interface ReviewRegRequest {
managedBranchIds: number[];
}
export interface AllRegularizationsResponse {
success: boolean;
data: PendingRegData[];
}
/** Add to common messages */
export interface ExportReportResponse {
fileContent: Buffer;
fileName: string;
}
function createBaseEmpty(): Empty {
return {};
}
@ -1316,6 +1321,7 @@ function createBaseDailyReportRequest(): DailyReportRequest {
designation: undefined,
employeeName: undefined,
employeeCode: undefined,
fileType: "",
};
}
@ -1342,6 +1348,9 @@ export const DailyReportRequest: MessageFns<DailyReportRequest> = {
if (message.employeeCode !== undefined) {
writer.uint32(58).string(message.employeeCode);
}
if (message.fileType !== "") {
writer.uint32(66).string(message.fileType);
}
return writer;
},
@ -1408,6 +1417,14 @@ export const DailyReportRequest: MessageFns<DailyReportRequest> = {
message.employeeCode = reader.string();
continue;
}
case 8: {
if (tag !== 66) {
break;
}
message.fileType = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -1446,6 +1463,11 @@ export const DailyReportRequest: MessageFns<DailyReportRequest> = {
: isSet(object.employee_code)
? globalThis.String(object.employee_code)
: undefined,
fileType: isSet(object.fileType)
? globalThis.String(object.fileType)
: isSet(object.file_type)
? globalThis.String(object.file_type)
: "",
};
},
@ -1472,6 +1494,9 @@ export const DailyReportRequest: MessageFns<DailyReportRequest> = {
if (message.employeeCode !== undefined) {
obj.employeeCode = message.employeeCode;
}
if (message.fileType !== "") {
obj.fileType = message.fileType;
}
return obj;
},
@ -1487,6 +1512,7 @@ export const DailyReportRequest: MessageFns<DailyReportRequest> = {
message.designation = object.designation ?? undefined;
message.employeeName = object.employeeName ?? undefined;
message.employeeCode = object.employeeCode ?? undefined;
message.fileType = object.fileType ?? "";
return message;
},
};
@ -2047,6 +2073,7 @@ function createBaseRangeReportRequest(): RangeReportRequest {
designation: undefined,
employeeName: undefined,
employeeCode: undefined,
fileType: "",
};
}
@ -2076,6 +2103,9 @@ export const RangeReportRequest: MessageFns<RangeReportRequest> = {
if (message.employeeCode !== undefined) {
writer.uint32(66).string(message.employeeCode);
}
if (message.fileType !== "") {
writer.uint32(74).string(message.fileType);
}
return writer;
},
@ -2150,6 +2180,14 @@ export const RangeReportRequest: MessageFns<RangeReportRequest> = {
message.employeeCode = reader.string();
continue;
}
case 9: {
if (tag !== 74) {
break;
}
message.fileType = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -2197,6 +2235,11 @@ export const RangeReportRequest: MessageFns<RangeReportRequest> = {
: isSet(object.employee_code)
? globalThis.String(object.employee_code)
: undefined,
fileType: isSet(object.fileType)
? globalThis.String(object.fileType)
: isSet(object.file_type)
? globalThis.String(object.file_type)
: "",
};
},
@ -2226,6 +2269,9 @@ export const RangeReportRequest: MessageFns<RangeReportRequest> = {
if (message.employeeCode !== undefined) {
obj.employeeCode = message.employeeCode;
}
if (message.fileType !== "") {
obj.fileType = message.fileType;
}
return obj;
},
@ -2242,6 +2288,7 @@ export const RangeReportRequest: MessageFns<RangeReportRequest> = {
message.designation = object.designation ?? undefined;
message.employeeName = object.employeeName ?? undefined;
message.employeeCode = object.employeeCode ?? undefined;
message.fileType = object.fileType ?? "";
return message;
},
};
@ -3822,6 +3869,7 @@ function createBasePendingRegData(): PendingRegData {
firstName: "",
lastName: "",
branchName: "",
reviewerName: "",
};
}
@ -3866,6 +3914,9 @@ export const PendingRegData: MessageFns<PendingRegData> = {
if (message.branchName !== "") {
writer.uint32(106).string(message.branchName);
}
if (message.reviewerName !== "") {
writer.uint32(114).string(message.reviewerName);
}
return writer;
},
@ -3980,6 +4031,14 @@ export const PendingRegData: MessageFns<PendingRegData> = {
message.branchName = reader.string();
continue;
}
case 14: {
if (tag !== 114) {
break;
}
message.reviewerName = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -4048,6 +4107,11 @@ export const PendingRegData: MessageFns<PendingRegData> = {
: isSet(object.branch_name)
? globalThis.String(object.branch_name)
: "",
reviewerName: isSet(object.reviewerName)
? globalThis.String(object.reviewerName)
: isSet(object.reviewer_name)
? globalThis.String(object.reviewer_name)
: "",
};
},
@ -4092,6 +4156,9 @@ export const PendingRegData: MessageFns<PendingRegData> = {
if (message.branchName !== "") {
obj.branchName = message.branchName;
}
if (message.reviewerName !== "") {
obj.reviewerName = message.reviewerName;
}
return obj;
},
@ -4113,6 +4180,7 @@ export const PendingRegData: MessageFns<PendingRegData> = {
message.firstName = object.firstName ?? "";
message.lastName = object.lastName ?? "";
message.branchName = object.branchName ?? "";
message.reviewerName = object.reviewerName ?? "";
return message;
},
};
@ -4345,6 +4413,166 @@ export const ReviewRegRequest: MessageFns<ReviewRegRequest> = {
},
};
function createBaseAllRegularizationsResponse(): AllRegularizationsResponse {
return { success: false, data: [] };
}
export const AllRegularizationsResponse: MessageFns<AllRegularizationsResponse> = {
encode(message: AllRegularizationsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.success !== false) {
writer.uint32(8).bool(message.success);
}
for (const v of message.data) {
PendingRegData.encode(v!, writer.uint32(18).fork()).join();
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): AllRegularizationsResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAllRegularizationsResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.success = reader.bool();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.data.push(PendingRegData.decode(reader, reader.uint32()));
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): AllRegularizationsResponse {
return {
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
data: globalThis.Array.isArray(object?.data) ? object.data.map((e: any) => PendingRegData.fromJSON(e)) : [],
};
},
toJSON(message: AllRegularizationsResponse): unknown {
const obj: any = {};
if (message.success !== false) {
obj.success = message.success;
}
if (message.data?.length) {
obj.data = message.data.map((e) => PendingRegData.toJSON(e));
}
return obj;
},
create<I extends Exact<DeepPartial<AllRegularizationsResponse>, I>>(base?: I): AllRegularizationsResponse {
return AllRegularizationsResponse.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<AllRegularizationsResponse>, I>>(object: I): AllRegularizationsResponse {
const message = createBaseAllRegularizationsResponse();
message.success = object.success ?? false;
message.data = object.data?.map((e) => PendingRegData.fromPartial(e)) || [];
return message;
},
};
function createBaseExportReportResponse(): ExportReportResponse {
return { fileContent: Buffer.alloc(0), fileName: "" };
}
export const ExportReportResponse: MessageFns<ExportReportResponse> = {
encode(message: ExportReportResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.fileContent.length !== 0) {
writer.uint32(10).bytes(message.fileContent);
}
if (message.fileName !== "") {
writer.uint32(18).string(message.fileName);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): ExportReportResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseExportReportResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.fileContent = Buffer.from(reader.bytes());
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.fileName = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): ExportReportResponse {
return {
fileContent: isSet(object.fileContent)
? Buffer.from(bytesFromBase64(object.fileContent))
: isSet(object.file_content)
? Buffer.from(bytesFromBase64(object.file_content))
: Buffer.alloc(0),
fileName: isSet(object.fileName)
? globalThis.String(object.fileName)
: isSet(object.file_name)
? globalThis.String(object.file_name)
: "",
};
},
toJSON(message: ExportReportResponse): unknown {
const obj: any = {};
if (message.fileContent.length !== 0) {
obj.fileContent = base64FromBytes(message.fileContent);
}
if (message.fileName !== "") {
obj.fileName = message.fileName;
}
return obj;
},
create<I extends Exact<DeepPartial<ExportReportResponse>, I>>(base?: I): ExportReportResponse {
return ExportReportResponse.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<ExportReportResponse>, I>>(object: I): ExportReportResponse {
const message = createBaseExportReportResponse();
message.fileContent = object.fileContent ?? Buffer.alloc(0);
message.fileName = object.fileName ?? "";
return message;
},
};
/**
* ==========================================
* SERVICES
@ -4410,6 +4638,26 @@ export const AttendanceServiceService = {
Buffer.from(SingleRangeReportResponse.encode(value).finish()),
responseDeserialize: (value: Buffer): SingleRangeReportResponse => SingleRangeReportResponse.decode(value),
},
exportDailyReport: {
path: "/ams.AttendanceService/ExportDailyReport" as const,
requestStream: false as const,
responseStream: false as const,
requestSerialize: (value: DailyReportRequest): Buffer => Buffer.from(DailyReportRequest.encode(value).finish()),
requestDeserialize: (value: Buffer): DailyReportRequest => DailyReportRequest.decode(value),
responseSerialize: (value: ExportReportResponse): Buffer =>
Buffer.from(ExportReportResponse.encode(value).finish()),
responseDeserialize: (value: Buffer): ExportReportResponse => ExportReportResponse.decode(value),
},
exportAdminRangeReport: {
path: "/ams.AttendanceService/ExportAdminRangeReport" as const,
requestStream: false as const,
responseStream: false as const,
requestSerialize: (value: RangeReportRequest): Buffer => Buffer.from(RangeReportRequest.encode(value).finish()),
requestDeserialize: (value: Buffer): RangeReportRequest => RangeReportRequest.decode(value),
responseSerialize: (value: ExportReportResponse): Buffer =>
Buffer.from(ExportReportResponse.encode(value).finish()),
responseDeserialize: (value: Buffer): ExportReportResponse => ExportReportResponse.decode(value),
},
} as const;
export interface AttendanceServiceServer extends UntypedServiceImplementation {
@ -4419,6 +4667,8 @@ export interface AttendanceServiceServer extends UntypedServiceImplementation {
getDailyReport: handleUnaryCall<DailyReportRequest, DailyReportResponse>;
getAdminRangeReport: handleUnaryCall<RangeReportRequest, AdminRangeReportResponse>;
getSingleEmployeeRangeReport: handleUnaryCall<SingleRangeReportRequest, SingleRangeReportResponse>;
exportDailyReport: handleUnaryCall<DailyReportRequest, ExportReportResponse>;
exportAdminRangeReport: handleUnaryCall<RangeReportRequest, ExportReportResponse>;
}
export interface AttendanceServiceClient extends Client {
@ -4512,6 +4762,36 @@ export interface AttendanceServiceClient extends Client {
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: SingleRangeReportResponse) => void,
): ClientUnaryCall;
exportDailyReport(
request: DailyReportRequest,
callback: (error: ServiceError | null, response: ExportReportResponse) => void,
): ClientUnaryCall;
exportDailyReport(
request: DailyReportRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: ExportReportResponse) => void,
): ClientUnaryCall;
exportDailyReport(
request: DailyReportRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: ExportReportResponse) => void,
): ClientUnaryCall;
exportAdminRangeReport(
request: RangeReportRequest,
callback: (error: ServiceError | null, response: ExportReportResponse) => void,
): ClientUnaryCall;
exportAdminRangeReport(
request: RangeReportRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: ExportReportResponse) => void,
): ClientUnaryCall;
exportAdminRangeReport(
request: RangeReportRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: ExportReportResponse) => void,
): ClientUnaryCall;
}
export const AttendanceServiceClient = makeGenericClientConstructor(
@ -4552,12 +4832,25 @@ export const RegularizationServiceService = {
responseSerialize: (value: SuccessResponse): Buffer => Buffer.from(SuccessResponse.encode(value).finish()),
responseDeserialize: (value: Buffer): SuccessResponse => SuccessResponse.decode(value),
},
/** NEW */
getAllRegularizations: {
path: "/ams.RegularizationService/GetAllRegularizations" as const,
requestStream: false as const,
responseStream: false as const,
requestSerialize: (value: Empty): Buffer => Buffer.from(Empty.encode(value).finish()),
requestDeserialize: (value: Buffer): Empty => Empty.decode(value),
responseSerialize: (value: AllRegularizationsResponse): Buffer =>
Buffer.from(AllRegularizationsResponse.encode(value).finish()),
responseDeserialize: (value: Buffer): AllRegularizationsResponse => AllRegularizationsResponse.decode(value),
},
} as const;
export interface RegularizationServiceServer extends UntypedServiceImplementation {
createRegularizationRequest: handleUnaryCall<CreateRegRequest, SuccessResponse>;
getPendingRegularizations: handleUnaryCall<PendingRegRequest, PendingRegResponse>;
reviewRegularizationRequest: handleUnaryCall<ReviewRegRequest, SuccessResponse>;
/** NEW */
getAllRegularizations: handleUnaryCall<Empty, AllRegularizationsResponse>;
}
export interface RegularizationServiceClient extends Client {
@ -4606,6 +4899,22 @@ export interface RegularizationServiceClient extends Client {
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: SuccessResponse) => void,
): ClientUnaryCall;
/** NEW */
getAllRegularizations(
request: Empty,
callback: (error: ServiceError | null, response: AllRegularizationsResponse) => void,
): ClientUnaryCall;
getAllRegularizations(
request: Empty,
metadata: Metadata,
callback: (error: ServiceError | null, response: AllRegularizationsResponse) => void,
): ClientUnaryCall;
getAllRegularizations(
request: Empty,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: AllRegularizationsResponse) => void,
): ClientUnaryCall;
}
export const RegularizationServiceClient = makeGenericClientConstructor(
@ -4662,6 +4971,14 @@ export const DashboardServiceClient = makeGenericClientConstructor(
serviceName: string;
};
function bytesFromBase64(b64: string): Uint8Array {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
}
function base64FromBytes(arr: Uint8Array): string {
return globalThis.Buffer.from(arr).toString("base64");
}
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T

View File

@ -307,6 +307,8 @@ export interface RosterEmployee {
departmentName: string;
departmentId: number;
jobTitle: string;
managerName: string;
managerId: string;
}
export interface RosterResponse {
@ -4772,6 +4774,8 @@ function createBaseRosterEmployee(): RosterEmployee {
departmentName: "",
departmentId: 0,
jobTitle: "",
managerName: "",
managerId: "",
};
}
@ -4810,6 +4814,12 @@ export const RosterEmployee: MessageFns<RosterEmployee> = {
if (message.jobTitle !== "") {
writer.uint32(90).string(message.jobTitle);
}
if (message.managerName !== "") {
writer.uint32(98).string(message.managerName);
}
if (message.managerId !== "") {
writer.uint32(106).string(message.managerId);
}
return writer;
},
@ -4908,6 +4918,22 @@ export const RosterEmployee: MessageFns<RosterEmployee> = {
message.jobTitle = reader.string();
continue;
}
case 12: {
if (tag !== 98) {
break;
}
message.managerName = reader.string();
continue;
}
case 13: {
if (tag !== 106) {
break;
}
message.managerId = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -4974,6 +5000,16 @@ export const RosterEmployee: MessageFns<RosterEmployee> = {
: isSet(object.job_title)
? globalThis.String(object.job_title)
: "",
managerName: isSet(object.managerName)
? globalThis.String(object.managerName)
: isSet(object.manager_name)
? globalThis.String(object.manager_name)
: "",
managerId: isSet(object.managerId)
? globalThis.String(object.managerId)
: isSet(object.manager_id)
? globalThis.String(object.manager_id)
: "",
};
},
@ -5012,6 +5048,12 @@ export const RosterEmployee: MessageFns<RosterEmployee> = {
if (message.jobTitle !== "") {
obj.jobTitle = message.jobTitle;
}
if (message.managerName !== "") {
obj.managerName = message.managerName;
}
if (message.managerId !== "") {
obj.managerId = message.managerId;
}
return obj;
},
@ -5031,6 +5073,8 @@ export const RosterEmployee: MessageFns<RosterEmployee> = {
message.departmentName = object.departmentName ?? "";
message.departmentId = object.departmentId ?? 0;
message.jobTitle = object.jobTitle ?? "";
message.managerName = object.managerName ?? "";
message.managerId = object.managerId ?? "";
return message;
},
};

View File

@ -174,6 +174,8 @@ export interface LeaveApplicationPayload {
dateTo: string;
isHalfDay: boolean;
reason: string;
/** NEW */
applicantRole: string;
}
export interface ApplyLeaveResponse {
@ -193,8 +195,11 @@ export interface ApplyLeaveResponse {
export interface ManagerActionRequest {
managerId: number;
applicationId: number;
/** For rejection */
reason?: string | undefined;
reason?:
| string
| undefined;
/** NEW */
userRole: string;
}
export interface PendingLeaveData {
@ -246,6 +251,31 @@ export interface LedgerReportResponse {
data: LedgerData[];
}
export interface EmptyRequest {
}
export interface AllApplicationData {
applicationId: number;
employeeId: number;
leaveType: string;
dateFrom: string;
dateTo: string;
numberOfDays: number;
status: string;
reason: string;
employeeCode: string;
firstName: string;
lastName: string;
departmentName: string;
jobTitle: string;
managerName: string;
}
export interface AllApplicationsResponse {
success: boolean;
data: AllApplicationData[];
}
function createBaseSuccessResponse(): SuccessResponse {
return { success: false, message: "" };
}
@ -2279,6 +2309,7 @@ function createBaseLeaveApplicationPayload(): LeaveApplicationPayload {
dateTo: "",
isHalfDay: false,
reason: "",
applicantRole: "",
};
}
@ -2308,6 +2339,9 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
if (message.reason !== "") {
writer.uint32(66).string(message.reason);
}
if (message.applicantRole !== "") {
writer.uint32(74).string(message.applicantRole);
}
return writer;
},
@ -2382,6 +2416,14 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
message.reason = reader.string();
continue;
}
case 9: {
if (tag !== 74) {
break;
}
message.applicantRole = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -2429,6 +2471,11 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
? globalThis.Boolean(object.is_half_day)
: false,
reason: isSet(object.reason) ? globalThis.String(object.reason) : "",
applicantRole: isSet(object.applicantRole)
? globalThis.String(object.applicantRole)
: isSet(object.applicant_role)
? globalThis.String(object.applicant_role)
: "",
};
},
@ -2458,6 +2505,9 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
if (message.reason !== "") {
obj.reason = message.reason;
}
if (message.applicantRole !== "") {
obj.applicantRole = message.applicantRole;
}
return obj;
},
@ -2474,6 +2524,7 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
message.dateTo = object.dateTo ?? "";
message.isHalfDay = object.isHalfDay ?? false;
message.reason = object.reason ?? "";
message.applicantRole = object.applicantRole ?? "";
return message;
},
};
@ -2647,7 +2698,7 @@ export const ApplyLeaveResponse: MessageFns<ApplyLeaveResponse> = {
};
function createBaseManagerActionRequest(): ManagerActionRequest {
return { managerId: 0, applicationId: 0, reason: undefined };
return { managerId: 0, applicationId: 0, reason: undefined, userRole: "" };
}
export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
@ -2661,6 +2712,9 @@ export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
if (message.reason !== undefined) {
writer.uint32(26).string(message.reason);
}
if (message.userRole !== "") {
writer.uint32(34).string(message.userRole);
}
return writer;
},
@ -2695,6 +2749,14 @@ export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
message.reason = reader.string();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.userRole = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -2717,6 +2779,11 @@ export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
? globalThis.Number(object.application_id)
: 0,
reason: isSet(object.reason) ? globalThis.String(object.reason) : undefined,
userRole: isSet(object.userRole)
? globalThis.String(object.userRole)
: isSet(object.user_role)
? globalThis.String(object.user_role)
: "",
};
},
@ -2731,6 +2798,9 @@ export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
if (message.reason !== undefined) {
obj.reason = message.reason;
}
if (message.userRole !== "") {
obj.userRole = message.userRole;
}
return obj;
},
@ -2742,6 +2812,7 @@ export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
message.managerId = object.managerId ?? 0;
message.applicationId = object.applicationId ?? 0;
message.reason = object.reason ?? undefined;
message.userRole = object.userRole ?? "";
return message;
},
};
@ -3536,6 +3607,456 @@ export const LedgerReportResponse: MessageFns<LedgerReportResponse> = {
},
};
function createBaseEmptyRequest(): EmptyRequest {
return {};
}
export const EmptyRequest: MessageFns<EmptyRequest> = {
encode(_: EmptyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): EmptyRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseEmptyRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(_: any): EmptyRequest {
return {};
},
toJSON(_: EmptyRequest): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<EmptyRequest>, I>>(base?: I): EmptyRequest {
return EmptyRequest.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<EmptyRequest>, I>>(_: I): EmptyRequest {
const message = createBaseEmptyRequest();
return message;
},
};
function createBaseAllApplicationData(): AllApplicationData {
return {
applicationId: 0,
employeeId: 0,
leaveType: "",
dateFrom: "",
dateTo: "",
numberOfDays: 0,
status: "",
reason: "",
employeeCode: "",
firstName: "",
lastName: "",
departmentName: "",
jobTitle: "",
managerName: "",
};
}
export const AllApplicationData: MessageFns<AllApplicationData> = {
encode(message: AllApplicationData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.applicationId !== 0) {
writer.uint32(8).int32(message.applicationId);
}
if (message.employeeId !== 0) {
writer.uint32(16).int32(message.employeeId);
}
if (message.leaveType !== "") {
writer.uint32(26).string(message.leaveType);
}
if (message.dateFrom !== "") {
writer.uint32(34).string(message.dateFrom);
}
if (message.dateTo !== "") {
writer.uint32(42).string(message.dateTo);
}
if (message.numberOfDays !== 0) {
writer.uint32(49).double(message.numberOfDays);
}
if (message.status !== "") {
writer.uint32(58).string(message.status);
}
if (message.reason !== "") {
writer.uint32(66).string(message.reason);
}
if (message.employeeCode !== "") {
writer.uint32(74).string(message.employeeCode);
}
if (message.firstName !== "") {
writer.uint32(82).string(message.firstName);
}
if (message.lastName !== "") {
writer.uint32(90).string(message.lastName);
}
if (message.departmentName !== "") {
writer.uint32(98).string(message.departmentName);
}
if (message.jobTitle !== "") {
writer.uint32(106).string(message.jobTitle);
}
if (message.managerName !== "") {
writer.uint32(114).string(message.managerName);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): AllApplicationData {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAllApplicationData();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.applicationId = reader.int32();
continue;
}
case 2: {
if (tag !== 16) {
break;
}
message.employeeId = reader.int32();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.leaveType = reader.string();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.dateFrom = reader.string();
continue;
}
case 5: {
if (tag !== 42) {
break;
}
message.dateTo = reader.string();
continue;
}
case 6: {
if (tag !== 49) {
break;
}
message.numberOfDays = reader.double();
continue;
}
case 7: {
if (tag !== 58) {
break;
}
message.status = reader.string();
continue;
}
case 8: {
if (tag !== 66) {
break;
}
message.reason = reader.string();
continue;
}
case 9: {
if (tag !== 74) {
break;
}
message.employeeCode = reader.string();
continue;
}
case 10: {
if (tag !== 82) {
break;
}
message.firstName = reader.string();
continue;
}
case 11: {
if (tag !== 90) {
break;
}
message.lastName = reader.string();
continue;
}
case 12: {
if (tag !== 98) {
break;
}
message.departmentName = reader.string();
continue;
}
case 13: {
if (tag !== 106) {
break;
}
message.jobTitle = reader.string();
continue;
}
case 14: {
if (tag !== 114) {
break;
}
message.managerName = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): AllApplicationData {
return {
applicationId: isSet(object.applicationId)
? globalThis.Number(object.applicationId)
: isSet(object.application_id)
? globalThis.Number(object.application_id)
: 0,
employeeId: isSet(object.employeeId)
? globalThis.Number(object.employeeId)
: isSet(object.employee_id)
? globalThis.Number(object.employee_id)
: 0,
leaveType: isSet(object.leaveType)
? globalThis.String(object.leaveType)
: isSet(object.leave_type)
? globalThis.String(object.leave_type)
: "",
dateFrom: isSet(object.dateFrom)
? globalThis.String(object.dateFrom)
: isSet(object.date_from)
? globalThis.String(object.date_from)
: "",
dateTo: isSet(object.dateTo)
? globalThis.String(object.dateTo)
: isSet(object.date_to)
? globalThis.String(object.date_to)
: "",
numberOfDays: isSet(object.numberOfDays)
? globalThis.Number(object.numberOfDays)
: isSet(object.number_of_days)
? globalThis.Number(object.number_of_days)
: 0,
status: isSet(object.status) ? globalThis.String(object.status) : "",
reason: isSet(object.reason) ? globalThis.String(object.reason) : "",
employeeCode: isSet(object.employeeCode)
? globalThis.String(object.employeeCode)
: isSet(object.employee_code)
? globalThis.String(object.employee_code)
: "",
firstName: isSet(object.firstName)
? globalThis.String(object.firstName)
: isSet(object.first_name)
? globalThis.String(object.first_name)
: "",
lastName: isSet(object.lastName)
? globalThis.String(object.lastName)
: isSet(object.last_name)
? globalThis.String(object.last_name)
: "",
departmentName: isSet(object.departmentName)
? globalThis.String(object.departmentName)
: isSet(object.department_name)
? globalThis.String(object.department_name)
: "",
jobTitle: isSet(object.jobTitle)
? globalThis.String(object.jobTitle)
: isSet(object.job_title)
? globalThis.String(object.job_title)
: "",
managerName: isSet(object.managerName)
? globalThis.String(object.managerName)
: isSet(object.manager_name)
? globalThis.String(object.manager_name)
: "",
};
},
toJSON(message: AllApplicationData): unknown {
const obj: any = {};
if (message.applicationId !== 0) {
obj.applicationId = Math.round(message.applicationId);
}
if (message.employeeId !== 0) {
obj.employeeId = Math.round(message.employeeId);
}
if (message.leaveType !== "") {
obj.leaveType = message.leaveType;
}
if (message.dateFrom !== "") {
obj.dateFrom = message.dateFrom;
}
if (message.dateTo !== "") {
obj.dateTo = message.dateTo;
}
if (message.numberOfDays !== 0) {
obj.numberOfDays = message.numberOfDays;
}
if (message.status !== "") {
obj.status = message.status;
}
if (message.reason !== "") {
obj.reason = message.reason;
}
if (message.employeeCode !== "") {
obj.employeeCode = message.employeeCode;
}
if (message.firstName !== "") {
obj.firstName = message.firstName;
}
if (message.lastName !== "") {
obj.lastName = message.lastName;
}
if (message.departmentName !== "") {
obj.departmentName = message.departmentName;
}
if (message.jobTitle !== "") {
obj.jobTitle = message.jobTitle;
}
if (message.managerName !== "") {
obj.managerName = message.managerName;
}
return obj;
},
create<I extends Exact<DeepPartial<AllApplicationData>, I>>(base?: I): AllApplicationData {
return AllApplicationData.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<AllApplicationData>, I>>(object: I): AllApplicationData {
const message = createBaseAllApplicationData();
message.applicationId = object.applicationId ?? 0;
message.employeeId = object.employeeId ?? 0;
message.leaveType = object.leaveType ?? "";
message.dateFrom = object.dateFrom ?? "";
message.dateTo = object.dateTo ?? "";
message.numberOfDays = object.numberOfDays ?? 0;
message.status = object.status ?? "";
message.reason = object.reason ?? "";
message.employeeCode = object.employeeCode ?? "";
message.firstName = object.firstName ?? "";
message.lastName = object.lastName ?? "";
message.departmentName = object.departmentName ?? "";
message.jobTitle = object.jobTitle ?? "";
message.managerName = object.managerName ?? "";
return message;
},
};
function createBaseAllApplicationsResponse(): AllApplicationsResponse {
return { success: false, data: [] };
}
export const AllApplicationsResponse: MessageFns<AllApplicationsResponse> = {
encode(message: AllApplicationsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.success !== false) {
writer.uint32(8).bool(message.success);
}
for (const v of message.data) {
AllApplicationData.encode(v!, writer.uint32(18).fork()).join();
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): AllApplicationsResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAllApplicationsResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break;
}
message.success = reader.bool();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.data.push(AllApplicationData.decode(reader, reader.uint32()));
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): AllApplicationsResponse {
return {
success: isSet(object.success) ? globalThis.Boolean(object.success) : false,
data: globalThis.Array.isArray(object?.data) ? object.data.map((e: any) => AllApplicationData.fromJSON(e)) : [],
};
},
toJSON(message: AllApplicationsResponse): unknown {
const obj: any = {};
if (message.success !== false) {
obj.success = message.success;
}
if (message.data?.length) {
obj.data = message.data.map((e) => AllApplicationData.toJSON(e));
}
return obj;
},
create<I extends Exact<DeepPartial<AllApplicationsResponse>, I>>(base?: I): AllApplicationsResponse {
return AllApplicationsResponse.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<AllApplicationsResponse>, I>>(object: I): AllApplicationsResponse {
const message = createBaseAllApplicationsResponse();
message.success = object.success ?? false;
message.data = object.data?.map((e) => AllApplicationData.fromPartial(e)) || [];
return message;
},
};
/**
* ==========================================
* SERVICES
@ -3960,6 +4481,56 @@ export const ReportsServiceClient = makeGenericClientConstructor(
serviceName: string;
};
/**
* ==========================================
* HR MANAGER SERVICE (NEW)
* ==========================================
*/
export type HRManagerServiceService = typeof HRManagerServiceService;
export const HRManagerServiceService = {
getAllLeaveApplications: {
path: "/lms.HRManagerService/GetAllLeaveApplications" as const,
requestStream: false as const,
responseStream: false as const,
requestSerialize: (value: EmptyRequest): Buffer => Buffer.from(EmptyRequest.encode(value).finish()),
requestDeserialize: (value: Buffer): EmptyRequest => EmptyRequest.decode(value),
responseSerialize: (value: AllApplicationsResponse): Buffer =>
Buffer.from(AllApplicationsResponse.encode(value).finish()),
responseDeserialize: (value: Buffer): AllApplicationsResponse => AllApplicationsResponse.decode(value),
},
} as const;
export interface HRManagerServiceServer extends UntypedServiceImplementation {
getAllLeaveApplications: handleUnaryCall<EmptyRequest, AllApplicationsResponse>;
}
export interface HRManagerServiceClient extends Client {
getAllLeaveApplications(
request: EmptyRequest,
callback: (error: ServiceError | null, response: AllApplicationsResponse) => void,
): ClientUnaryCall;
getAllLeaveApplications(
request: EmptyRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: AllApplicationsResponse) => void,
): ClientUnaryCall;
getAllLeaveApplications(
request: EmptyRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: AllApplicationsResponse) => void,
): ClientUnaryCall;
}
export const HRManagerServiceClient = makeGenericClientConstructor(
HRManagerServiceService,
"lms.HRManagerService",
) as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HRManagerServiceClient;
service: typeof HRManagerServiceService;
serviceName: string;
};
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T

View File

@ -53,7 +53,7 @@ CREATE TABLE attendance_regularizations (
employee_id INT NOT NULL, -- Logical Reference (The Applicant)
branch_id INT NOT NULL,
attendance_id INT NULL, -- Internal FK to processed ledger row
regularization_type ENUM('MISPUNCH', 'WFH_REQUEST', 'ON_DUTY') NOT NULL,
regularization_type ENUM('MISPUNCH', 'HALF_DAY', 'WFH_REQUEST', 'ON_DUTY') NOT NULL;
target_date DATE NOT NULL,
requested_check_in TIMESTAMP NULL,
requested_check_out TIMESTAMP NULL,

View File

@ -58,6 +58,8 @@ CREATE TABLE leave_applications (
status ENUM('DRAFT', 'PENDING', 'PENDING_LOP', 'APPROVED', 'APPROVED_LOP', 'REJECTED', 'CANCELLED') DEFAULT 'PENDING',
manager_approved_by INT NULL, -- Logical Reference to EMS employees table (Approver)
hr_approved_by INT NULL,
approver_role VARCHAR(20) NOT NULL DEFAULT 'MANAGER',
applicant_role VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE',
FOREIGN KEY (leave_type_id) REFERENCES leave_types(leave_type_id) ON DELETE RESTRICT,
INDEX idx_lms_status_lookup (employee_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -12,12 +12,15 @@ service AttendanceService {
rpc GetDailyReport(DailyReportRequest) returns (DailyReportResponse);
rpc GetAdminRangeReport(RangeReportRequest) returns (AdminRangeReportResponse);
rpc GetSingleEmployeeRangeReport(SingleRangeReportRequest) returns (SingleRangeReportResponse);
rpc ExportDailyReport(DailyReportRequest) returns (ExportReportResponse);
rpc ExportAdminRangeReport(RangeReportRequest) returns (ExportReportResponse);
}
service RegularizationService {
rpc CreateRegularizationRequest(CreateRegRequest) returns (SuccessResponse);
rpc GetPendingRegularizations(PendingRegRequest) returns (PendingRegResponse);
rpc ReviewRegularizationRequest(ReviewRegRequest) returns (SuccessResponse);
rpc GetAllRegularizations(Empty) returns (AllRegularizationsResponse); // NEW
}
service DashboardService {
@ -97,6 +100,7 @@ message DailyReportRequest {
optional string designation = 5; // Job Title
optional string employee_name = 6;
optional string employee_code = 7;
string file_type = 8;
}
message DailySummary {
@ -136,10 +140,11 @@ message RangeReportRequest {
string to_date = 2;
optional int32 branch_id = 3;
optional int32 department_id = 4;
optional int32 company_id = 5; // NEW
optional string designation = 6; // NEW
optional string employee_name = 7; // NEW
optional string employee_code = 8; // NEW
optional int32 company_id = 5;
optional string designation = 6;
optional string employee_name = 7;
optional string employee_code = 8;
string file_type = 9; // <--- ADD THIS LINE
}
message RangeMetrics {
@ -250,6 +255,7 @@ message PendingRegData {
string first_name = 11;
string last_name = 12;
string branch_name = 13;
string reviewer_name = 14;
}
message PendingRegResponse {
@ -264,3 +270,18 @@ message ReviewRegRequest {
string user_role = 4;
repeated int32 managed_branch_ids = 5;
}
message AllRegularizationsResponse {
bool success = 1;
repeated PendingRegData data = 2;
}
// Add to common messages
message ExportReportResponse {
bytes file_content = 1;
string file_name = 2;
}

View File

@ -320,6 +320,8 @@ message RosterEmployee {
string department_name = 9;
int32 department_id = 10;
string job_title = 11;
string manager_name = 12;
string manager_id = 13;
}
message RosterResponse {

View File

@ -173,6 +173,7 @@ message LeaveApplicationPayload {
string date_to = 6;
bool is_half_day = 7;
string reason = 8;
string applicant_role = 9; // NEW
}
message ApplyLeaveResponse {
@ -190,7 +191,8 @@ message ApplyLeaveResponse {
message ManagerActionRequest {
int32 manager_id = 1;
int32 application_id = 2;
optional string reason = 3; // For rejection
optional string reason = 3;
string user_role = 4; // NEW
}
message PendingLeaveData {
@ -239,3 +241,36 @@ message LedgerReportResponse {
int32 report_year = 3;
repeated LedgerData data = 4;
}
// ==========================================
// HR MANAGER SERVICE (NEW)
// ==========================================
service HRManagerService {
rpc GetAllLeaveApplications(EmptyRequest) returns (AllApplicationsResponse);
}
message EmptyRequest {}
message AllApplicationData {
int32 application_id = 1;
int32 employee_id = 2;
string leave_type = 3;
string date_from = 4;
string date_to = 5;
double number_of_days = 6;
string status = 7;
string reason = 8;
string employee_code = 9;
string first_name = 10;
string last_name = 11;
string department_name = 12;
string job_title = 13;
string manager_name = 14;
}
message AllApplicationsResponse {
bool success = 1;
repeated AllApplicationData data = 2;
}

View File

@ -178,5 +178,25 @@ export const AttendanceServiceImplementation = {
console.error("gRPC getSingleEmployeeRangeReport Error:", error);
return { success: false, employee: undefined, fromDate: "", toDate: "", totalDays: 0, history: [] };
}
},
// Add these to AttendanceServiceImplementation
async exportDailyReport(req: any) {
try {
const result = await AttendanceService.exportDailyReport(req);
return { success: true, fileContent: result.file_content, fileName: result.file_name };
} catch (error: any) {
console.error("gRPC exportDailyReport Error:", error);
return { success: false, fileName: "" };
}
},
async exportAdminRangeReport(req: any) {
try {
const result = await AttendanceService.exportAdminRangeReport(req);
return { success: true, fileContent: result.file_content, fileName: result.file_name };
} catch (error: any) {
console.error("gRPC exportAdminRangeReport Error:", error);
return { success: false, fileName: "" };
}
}
};

View File

@ -69,5 +69,30 @@ export const RegularizationServiceImplementation = {
console.error("gRPC reviewRegularizationRequest Error:", error);
return { success: false, message: error.message };
}
},
async getAllRegularizations(req: any) {
try {
const data = await RegService.getAllRegularizations();
const mappedData = data.map((reg: any) => ({
regularizationId: Number(reg.regularization_id) || 0,
employeeId: Number(reg.employee_id) || 0,
targetDate: reg.target_date ? new Date(reg.target_date).toISOString() : "",
regularizationType: reg.regularization_type || "",
requestedCheckIn: reg.requested_check_in ? new Date(reg.requested_check_in).toISOString() : "",
requestedCheckOut: reg.requested_check_out ? new Date(reg.requested_check_out).toISOString() : "",
reason: reg.reason || "",
status: reg.status || "",
branchId: Number(reg.branch_id) || 0,
employeeCode: reg.employee_code || "",
firstName: reg.first_name || "",
lastName: reg.last_name || "",
branchName: reg.branch_name || "N/A",
reviewerName: reg.reviewer_name || "N/A"
}));
return { success: true, data: mappedData };
} catch (e: any) {
console.error("gRPC getAllRegularizations Error:", e);
return { success: false, data: [] };
}
}
};

View File

@ -79,7 +79,7 @@ export const findAttendanceByEmployeeIds = async (employeeIds: number[], fromDat
if (employeeIds.length === 0) return [];
const placeholders = employeeIds.map(() => "?").join(",");
console.log("DB Query Params:", { fromDate, toDate, employeeIds });
// console.log("DB Query Params:", { fromDate, toDate, employeeIds });
const [rows]: any = await pool.execute(
`SELECT

View File

@ -40,3 +40,13 @@ export const updateRegularizationStatus = async (id: number, status: string, rev
[status, reviewerId, id]
);
};
export const findAllRegularizations = async () => {
const [rows] = await pool.execute(
`SELECT regularization_id, employee_id, target_date, regularization_type,
requested_check_in, requested_check_out, reason, status, branch_id, reviewed_by_id
FROM attendance_regularizations
ORDER BY target_date DESC`
);
return rows;
};

View File

@ -2,15 +2,337 @@
import pool from "../db.ts";
import { getEmployeeRoster } from "core/internal-client.ts";
import * as AttendanceRepo from "../repositories/attendance.repository.ts";
import ExcelJS from "npm:exceljs@4.4.0";
import PDFDocument from "npm:pdfkit@0.13.0";
// Helper function to draw tables in PDF
function drawPdfTable(doc: any, headers: string[], rows: any[], columnWidths: number[]) {
const startX = doc.page.margins.left;
let y = doc.y;
const cellPadding = 5;
// 1. Draw Headers
doc.font('Helvetica-Bold').fontSize(9);
let headerHeight = 20;
headers.forEach((header, i) => {
const textHeight = doc.heightOfString(header, { width: columnWidths[i] - cellPadding * 2 });
headerHeight = Math.max(headerHeight, textHeight + cellPadding * 2);
});
let x = startX;
headers.forEach((header, i) => {
// Dark Green background, White text, Black border (Matching Excel)
doc.fill('#063B00').rect(x, y, columnWidths[i], headerHeight).fill();
doc.fillColor('#FFFFFF').text(header, x + cellPadding, y + cellPadding, { width: columnWidths[i] - cellPadding * 2, align: 'center' });
doc.lineWidth(0.5).strokeColor('#000000').rect(x, y, columnWidths[i], headerHeight).stroke();
x += columnWidths[i];
});
y += headerHeight;
// 2. Draw Rows
doc.font('Helvetica').fontSize(8);
rows.forEach((row: any) => {
let rowHeight = 15;
row.forEach((cell: any, i: number) => {
const textHeight = doc.heightOfString(String(cell ?? '-'), { width: columnWidths[i] - cellPadding * 2 });
rowHeight = Math.max(rowHeight, textHeight + cellPadding * 2);
});
if (y + rowHeight > doc.page.height - doc.page.margins.bottom) {
doc.addPage();
y = doc.page.margins.top;
}
x = startX;
row.forEach((cell: any, i: number) => {
let textColor = '#000000'; // Default black
let bgColor = null;
// Apply background and text colors for the Status column (always the last column)
if (i === headers.length - 1) {
const statusStr = String(cell ?? '').toUpperCase();
if (statusStr === 'FULL_DAY') {
bgColor = '#C6EFCE'; textColor = '#006100';
} else if (statusStr === 'ABSENT') {
bgColor = '#FFC7CE'; textColor = '#9C0006';
} else if (statusStr === 'HALF_DAY') {
bgColor = '#FFEB9C'; textColor = '#9C6500';
} else if (statusStr === 'MISPUNCH') {
bgColor = '#B1A0C7'; textColor = '#4B2E83';
} else if (statusStr === 'LATE') {
bgColor = '#FFC000'; textColor = '#000000';
}
}
// Draw background color if applicable
if (bgColor) {
doc.fillColor(bgColor).rect(x, y, columnWidths[i], rowHeight).fill();
}
// Draw text
doc.fillColor(textColor).text(String(cell ?? '-'), x + cellPadding, y + cellPadding, { width: columnWidths[i] - cellPadding * 2 });
// Draw black border for rows
doc.lineWidth(0.5).strokeColor('#000000').rect(x, y, columnWidths[i], rowHeight).stroke();
x += columnWidths[i];
});
y += rowHeight;
});
doc.y = y + 10;
}
// Helper function specifically for Excel Daily Report generation
async function generateExcelDailyReport(roster: any[], formattedDate: string, dayName: string, generatedDate: string, summary: any): Promise<Uint8Array> {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Daily Report');
// Merge cells across 12 columns (A to L)
worksheet.mergeCells('A1:L1');
const titleCell = worksheet.getCell('A1');
titleCell.value = 'Report Name : Daily Attendance Report';
titleCell.font = { size: 11, bold: true };
titleCell.alignment = { vertical: 'middle', horizontal: 'left' };
worksheet.mergeCells('A2:L2');
const dateCell = worksheet.getCell('A2');
dateCell.value = `Date : ${formattedDate} (${dayName})`;
dateCell.font = { size: 11, bold: true };
dateCell.alignment = { vertical: 'middle', horizontal: 'left' };
worksheet.mergeCells('A3:L3');
const genDateCell = worksheet.getCell('A3');
genDateCell.value = `Generated Date : ${generatedDate}`;
genDateCell.font = { size: 11, bold: true };
genDateCell.alignment = { vertical: 'middle', horizontal: 'left' };
// NEW: Summary Metrics Row
worksheet.mergeCells('A4:L4');
const summaryCell = worksheet.getCell('A4');
summaryCell.value = `Total employees: ${summary.total_active_workforce || 0} | Present: ${summary.present || 0} | Late: ${summary.late || 0} | Mispunches: ${summary.mispunches || 0} | Absent: ${summary.absent || 0}`;
summaryCell.font = { size: 11, bold: true, color: { argb: 'FF000000' } }; // Black text
summaryCell.alignment = { vertical: 'middle', horizontal: 'left' };
worksheet.getRow(5).height = 10; // Spacer row
// Row 6: Headers (12 columns)
const headerRow = worksheet.getRow(6);
headerRow.values = ['Date', 'Day', 'Emp Code', 'Name', 'Company', 'Branch', 'Department', 'Job Role', 'Check-In', 'Check-Out', 'Worked Hours', 'Status'];
// Style Header Row (Green Background, White Text)
headerRow.eachCell((cell) => {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: '063B00' } // Dark Green
};
cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }; // White text
cell.alignment = { vertical: 'middle', horizontal: 'center' };
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
});
headerRow.height = 20;
// Add Data Rows starting from Row 7
roster.forEach((r: any, index: number) => {
const rowIndex = 7 + index;
const row = worksheet.getRow(rowIndex);
row.values = [
formattedDate,
dayName,
r.employee_code,
r.full_name,
r.company_name || '-',
r.branch_name || '-',
r.department_name || '-',
r.designation || '-',
r.check_in ? r.check_in.substring(0, 5) : '-',
r.check_out ? r.check_out.substring(0, 5) : '-',
r.worked_hours,
r.status
];
// Add borders to data cells
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
cell.alignment = { vertical: 'middle', horizontal: 'left' };
// Apply colors to the Status column (Column 12 / 'L')
if (colNumber === 12) {
const statusStr = (r.status || '').toUpperCase();
if (statusStr === 'FULL_DAY') {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC6EFCE' } }; // Light Green background
cell.font = { color: { argb: 'FF006100' }, bold: true }; // Dark Green text
} else if (statusStr === 'ABSENT') {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC7CE' } }; // Light Red background
cell.font = { color: { argb: 'FF9C0006' }, bold: true }; // Dark Red text
} else if (statusStr === 'HALF_DAY') {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFEB9C' } }; // Light Orange/Yellow background
cell.font = { color: { argb: 'FF9C6500' }, bold: true }; // Dark Orange text
} else if (statusStr === 'MISPUNCH') {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFB1A0C7' } }; // Light Purple background
cell.font = { color: { argb: 'FF4B2E83' }, bold: true }; // Dark Purple text
} else if (statusStr === 'LATE') {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC000' } }; // Gold background
cell.font = { color: { argb: 'FF000000' }, bold: true }; // Black text
}
}
});
row.commit();
});
// Set 12 Column Widths (Increased Day width to 70 to prevent "Wednesday" text wrapping)
worksheet.columns = [
{ width: 12 }, // Date
{ width: 65 }, // Day
{ width: 12 }, // Emp Code
{ width: 25 }, // Name
{ width: 22 }, // Company
{ width: 18 }, // Branch
{ width: 22 }, // Department
{ width: 25 }, // Job Role
{ width: 12 }, // Check-In
{ width: 12 }, // Check-Out
{ width: 12 }, // Worked Hours
{ width: 12 }, // Status
];
const buffer = await workbook.xlsx.writeBuffer();
return new Uint8Array(buffer);
}
// Helper function specifically for Excel Monthly Report generation (Matrix Layout)
async function generateExcelMonthlyReport(report: any[], dates: string[], formattedFromDate: string, formattedToDate: string, generatedDate: string): Promise<Uint8Array> {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Monthly Attendance');
const numDates = dates.length;
const totalCols = 1 + numDates;
// Top Level Headings
worksheet.mergeCells(1, 1, 1, totalCols);
const titleCell = worksheet.getCell(1, 1);
titleCell.value = 'Report Name : Monthly Attendance Report';
titleCell.font = { size: 11, bold: true };
titleCell.alignment = { vertical: 'middle', horizontal: 'left' };
worksheet.mergeCells(2, 1, 2, totalCols);
const periodCell = worksheet.getCell(2, 1);
periodCell.value = `Period : ${formattedFromDate} To ${formattedToDate}`;
periodCell.font = { size: 11, bold: true };
periodCell.alignment = { vertical: 'middle', horizontal: 'left' };
worksheet.mergeCells(3, 1, 3, totalCols);
const genDateCell = worksheet.getCell(3, 1);
genDateCell.value = `Generated Date : ${generatedDate}`;
genDateCell.font = { size: 11, bold: true };
genDateCell.alignment = { vertical: 'middle', horizontal: 'left' };
// Column Widths
worksheet.getColumn(1).width = 12;
for (let i = 0; i < numDates; i++) {
worksheet.getColumn(i + 2).width = 10;
}
let currentRow = 5;
report.forEach((emp: any) => {
const history = emp.attendance_history || [];
// --- Employee Summary Header ---
worksheet.mergeCells(currentRow, 1, currentRow, totalCols);
const empCell = worksheet.getCell(currentRow, 1);
empCell.value = `Employee Code: ${emp.employee_code} | Name: ${emp.first_name} ${emp.last_name} | Company: ${emp.company_name || '-'} | Branch: ${emp.branch_name || '-'} | Department: ${emp.department_name || '-'} | Job Role: ${emp.designation || '-'} | Full Days: ${emp.range_metrics?.full_days || 0} | Half Days: ${emp.range_metrics?.half_days || 0} | Mispunches: ${emp.range_metrics?.mispunches || 0} | Late Arrivals: ${emp.range_metrics?.late_arrivals || 0}`;
empCell.font = { bold: true, size: 10 };
empCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF2F2F2' } };
empCell.alignment = { vertical: 'middle', horizontal: 'left' };
currentRow++;
// --- Table Headers (Field | Date 1 | Date 2 | ...) ---
const headerRow = worksheet.getRow(currentRow);
headerRow.getCell(1).value = 'Field';
for (let i = 0; i < numDates; i++) {
const dateObj = new Date(dates[i] + 'T00:00:00');
const dayStr = dateObj.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'short' });
headerRow.getCell(i + 2).value = `${dayStr}\n${dayName}`;
}
headerRow.eachCell((cell) => {
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '063B00' } }; // Dark Green
cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }; // White text
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
cell.border = { top: {style:'thin', color:{argb:'FF000000'}}, left: {style:'thin', color:{argb:'FF000000'}}, bottom: {style:'thin', color:{argb:'FF000000'}}, right: {style:'thin', color:{argb:'FF000000'}} };
});
headerRow.height = 25;
currentRow++;
// --- Data Rows (Check-In, Check-Out, Hours, Status) ---
const fields = ['Check-In', 'Check-Out', 'Hours', 'Status'];
fields.forEach((field) => {
const row = worksheet.getRow(currentRow);
row.getCell(1).value = field;
row.getCell(1).font = { bold: true };
row.getCell(1).alignment = { horizontal: 'left' };
row.getCell(1).border = { top: {style:'thin', color:{argb:'FF000000'}}, left: {style:'thin', color:{argb:'FF000000'}}, bottom: {style:'thin', color:{argb:'FF000000'}}, right: {style:'thin', color:{argb:'FF000000'}} };
for (let i = 0; i < numDates; i++) {
const log = history.find((h: any) => h.work_date === dates[i]);
const cell = row.getCell(i + 2);
if (log) {
if (field === 'Check-In') cell.value = log.check_in ? log.check_in.substring(0, 5) : '-';
else if (field === 'Check-Out') cell.value = log.check_out ? log.check_out.substring(0, 5) : '-';
else if (field === 'Hours') cell.value = log.worked_hours;
else if (field === 'Status') {
cell.value = log.final_status;
const statusStr = (log.final_status || '').toUpperCase();
if (statusStr === 'FULL_DAY') { cell.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FFC6EFCE'} }; cell.font = { color:{argb:'FF006100'}, bold:true }; }
else if (statusStr === 'ABSENT') { cell.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FFFFC7CE'} }; cell.font = { color:{argb:'FF9C0006'}, bold:true }; }
else if (statusStr === 'HALF_DAY') { cell.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FFFFEB9C'} }; cell.font = { color:{argb:'FF9C6500'}, bold:true }; }
else if (statusStr === 'MISPUNCH') { cell.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FFB1A0C7'} }; cell.font = { color:{argb:'FF4B2E83'}, bold:true }; }
else if (statusStr === 'LATE') { cell.fill = { type:'pattern', pattern:'solid', fgColor:{argb:'FFFFC000'} }; cell.font = { color:{argb:'FF000000'}, bold:true }; }
}
} else {
cell.value = '-';
}
cell.border = { top: {style:'thin', color:{argb:'FF000000'}}, left: {style:'thin', color:{argb:'FF000000'}}, bottom: {style:'thin', color:{argb:'FF000000'}}, right: {style:'thin', color:{argb:'FF000000'}} };
cell.alignment = { horizontal: 'center' };
}
currentRow++;
});
currentRow++; // empty row between employees
});
const buffer = await workbook.xlsx.writeBuffer();
return new Uint8Array(buffer);
}
// Helper function to apply filters to the roster
const applyRosterFilters = (roster: any[], filters: any) => {
let filtered = [...roster];
if (filters.companyId) filtered = filtered.filter(e => e.companyId === Number(filters.companyId));
if (filters.branchId) filtered = filtered.filter(e => e.branchId === Number(filters.branchId));
if (filters.departmentId) filtered = filtered.filter(e => e.departmentId === Number(filters.departmentId));
if (filters.designation) filtered = filtered.filter(e => e.jobTitle?.toLowerCase().includes(filters.designation.toLowerCase()) ?? false);
if (filters.designation) {
const desigStr = String(filters.designation).toLowerCase();
const desigNum = Number(filters.designation);
filtered = filtered.filter(e =>
e.jobTitle?.toLowerCase().includes(desigStr) ||
e.jobTitleId === desigNum ||
e.jobRoleId === desigNum
);
}
if (filters.employeeName) filtered = filtered.filter(e => e.fullName?.toLowerCase().includes(filters.employeeName.toLowerCase()) ?? false);
if (filters.employeeCode) filtered = filtered.filter(e => e.employeeCode?.toLowerCase().includes(filters.employeeCode.toLowerCase()) ?? false);
return filtered;
@ -36,10 +358,6 @@ export const processDailyAttendance = async (body: any) => {
console.log("AMS Worker Received Body:", JSON.stringify(body));
let datesToProcess: string[] = [];
// FIX: Check for camelCase first
// FIX: Protobuf converts undefined to empty string "".
// We must check .length > 0 instead of just truthiness.
// FIX: The generated Protobuf object uses snake_case for this message
if (body.work_date && body.work_date.length > 0) {
datesToProcess.push(body.work_date);
} else if (body.start_date?.length > 0 && body.end_date?.length > 0) {
@ -57,7 +375,6 @@ export const processDailyAttendance = async (body: any) => {
await connection.beginTransaction();
const employeeRoster = await getEmployeeRoster();
// DEBUG LOG: See if the roster is actually being fetched
console.log(`[AMS Worker] Fetched ${employeeRoster.length} employees from EMS.`);
if (employeeRoster.length > 0) {
console.log("[AMS Worker] First employee in roster:", employeeRoster[0]);
@ -69,7 +386,6 @@ export const processDailyAttendance = async (body: any) => {
const rawLogs = await AttendanceRepo.findRawLogsByDate(targetDate);
if (rawLogs.length === 0) continue;
// DEBUG LOG: See how many logs were found for this date
console.log(`[AMS Worker] Date ${targetDate}: Found ${rawLogs.length} raw logs.`);
const groupedLogs = rawLogs.reduce((acc: any, log: any) => {
@ -81,58 +397,17 @@ export const processDailyAttendance = async (body: any) => {
for (const empCode in groupedLogs) {
const employee = employeeMap.get(empCode);
// DEBUG LOG: See why employees are being skipped
if (!employee) {
// console.log(`[AMS Worker] Skipping code ${empCode} - Not found in EMS roster.`);
continue;
}
if (!employee.isActive) {
// console.log(`[AMS Worker] Skipping code ${empCode} - Employee is inactive.`);
continue;
}
const punches = groupedLogs[empCode];
const shift = await AttendanceRepo.findShiftDetails(employee.branchId, employee.companyId, connection);
// let check_in = null, check_out = null, worked_hours = 0.0;
// let check_in_status = "ON_TIME", final_status = "ABSENT";
// if (punches.length >= 2) {
// check_in = punches[0];
// check_out = punches[punches.length - 1];
// const firstPunchMs = new Date(check_in.replace(' ', 'T')).getTime();
// const lastPunchMs = new Date(check_out.replace(' ', 'T')).getTime();
// worked_hours = Math.round(((lastPunchMs - firstPunchMs) / (1000 * 60 * 60)) * 100) / 100;
// const rawCheckInTimeStr = check_in.split(' ')[1];
// const [punchH, punchM, punchS] = rawCheckInTimeStr.split(':').map(Number);
// const [shiftH, shiftM, shiftS] = shift.start_time.split(':').map(Number);
// if ((punchH * 3600 + punchM * 60 + punchS) > (shiftH * 3600 + shiftM * 60 + shift.grace_period_minutes * 60)) {
// check_in_status = "LATE";
// }
// if (worked_hours >= 7.0) final_status = "FULL_DAY";
// else if (worked_hours >= 4.0) final_status = "HALF_DAY";
// else final_status = "ABSENT";
// } else if (punches.length === 1) {
// check_in = punches[0];
// check_in_status = "MISPUNCH";
// final_status = "MISPUNCH";
// }
// await AttendanceRepo.upsertProcessedAttendance({
// employeeId: employee.employeeId,
// company_id: employee.companyId,
// branch_id: employee.branchId,
// targetDate, check_in, check_out, worked_hours, check_in_status, final_status
// }, connection);
// 1. Fetch the dynamic policy for this employee's company/branch
const policy = await AttendanceRepo.findAttendancePolicy(employee.companyId, employee.branchId);
// Use policy grace period, fallback to shift grace period if policy doesn't exist
const effectiveGracePeriod = policy.late_grace_period_minutes ?? shift.grace_period_minutes;
let check_in = null, check_out = null, worked_hours = 0.0;
@ -144,19 +419,16 @@ export const processDailyAttendance = async (body: any) => {
const firstPunchMs = new Date(check_in.replace(' ', 'T')).getTime();
let lastPunchMs = new Date(check_out.replace(' ', 'T')).getTime();
// Handle Night Shift edge case
if (shift.is_night_shift && lastPunchMs < firstPunchMs) {
lastPunchMs += 24 * 60 * 60 * 1000;
}
worked_hours = Math.round(((lastPunchMs - firstPunchMs) / (1000 * 60 * 60)) * 100) / 100;
// 2. Calculate Base Status using DYNAMIC thresholds
if (worked_hours >= Number(policy.min_hours_full_day)) final_status = "FULL_DAY";
else if (worked_hours >= Number(policy.min_hours_half_day)) final_status = "HALF_DAY";
else final_status = "ABSENT";
// 3. Calculate Punctuality
const rawCheckInTimeStr = check_in.split(' ')[1];
const [punchH, punchM, punchS] = rawCheckInTimeStr.split(':').map(Number);
const [shiftH, shiftM, shiftS] = shift.start_time.split(':').map(Number);
@ -168,12 +440,8 @@ export const processDailyAttendance = async (body: any) => {
check_in_status = "LATE";
}
// 4. Apply Late Penalty (The 3rd Late Rule)
// Rule: Can ONLY downgrade FULL_DAY to HALF_DAY. Cannot affect ABSENT or HALF_DAY.
if (check_in_status === "LATE" && final_status === "FULL_DAY") {
const latesThisMonth = await AttendanceRepo.countLatesThisMonth(employee.employeeId, targetDate, connection);
// If they already had 2 lates, this is their 3rd. Downgrade to HALF_DAY.
if (latesThisMonth >= policy.max_lates_allowed_per_month) {
final_status = "HALF_DAY";
}
@ -181,7 +449,7 @@ export const processDailyAttendance = async (body: any) => {
} else if (punches.length === 1) {
check_in = punches[0];
check_in_status = "MISPUNCH"; // Excluded from late penalty logic
check_in_status = "MISPUNCH";
final_status = "MISPUNCH";
}
@ -229,7 +497,6 @@ export const getDailyReport = async (req: any) => {
const attendanceRows = await AttendanceRepo.findDailyAttendance(req.date);
let employeeRoster = await getEmployeeRoster();
// Apply filters
employeeRoster = applyRosterFilters(employeeRoster, req);
const summary = { total_active_workforce: employeeRoster.length, present: 0, late: 0, mispunches: 0, absent: 0 };
@ -270,7 +537,6 @@ export const getDailyReport = async (req: any) => {
export const getAdminRangeReport = async (req: any) => {
let employeeRoster = await getEmployeeRoster();
// Apply filters
employeeRoster = applyRosterFilters(employeeRoster, req);
if (employeeRoster.length === 0) return { date_range: { from: req.fromDate, to: req.toDate }, global_summary: { total_records_evaluated: 0, full_days: 0, half_days: 0, late_instances: 0, mispunches: 0 }, report: [] };
@ -336,7 +602,6 @@ export const getSingleEmployeeRangeReport = async (employeeId: number, fromDate:
export const getDashboardMetrics = async (req: any) => {
let employeeRoster = await getEmployeeRoster();
// Apply filters
employeeRoster = applyRosterFilters(employeeRoster, req);
const attendanceRows = await AttendanceRepo.findDailyAttendance(req.date);
@ -353,3 +618,203 @@ export const getDashboardMetrics = async (req: any) => {
total_not_checked_in: totalNotCheckedIn
};
};
export const exportDailyReport = async (req: any) => {
const data = await getDailyReport(req);
const roster = data.roster || [];
const summary = data.summary || {};
const fileType = req.fileType || 'excel';
let file_content: Uint8Array;
let file_name: string;
const reportDate = new Date(req.date + 'T00:00:00');
const formattedDate = reportDate.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
const dayName = reportDate.toLocaleDateString('en-US', { weekday: 'long' });
const generatedDate = new Date().toLocaleString('en-GB', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
if (fileType === 'pdf') {
const doc = new PDFDocument({ margin: 30, size: 'A4', layout: 'landscape' });
const chunks: Uint8Array[] = [];
doc.on('data', (chunk: Uint8Array) => chunks.push(chunk));
doc.fontSize(10).fillColor('#000000').text(`Report Name : Daily Attendance Report`, { align: 'left' });
doc.fontSize(10).fillColor('#000000').text(`Date : ${formattedDate} (${dayName})`, { align: 'left' });
doc.fontSize(10).fillColor('#000000').text(`Generated Date : ${generatedDate}`, { align: 'left' });
// Add Summary Metrics Line (Black color)
doc.fontSize(10).fillColor('#000000').text(`Total employees: ${summary.total_active_workforce || 0} | Present: ${summary.present || 0} | Late: ${summary.late || 0} | Mispunches: ${summary.mispunches || 0} | Absent: ${summary.absent || 0}`, { align: 'left' });
doc.moveDown();
const headers = ['Date', 'Day', 'Emp Code', 'Name', 'Company', 'Branch', 'Department', 'Job Role', 'Check-In', 'Check-Out', 'Hrs', 'Status'];
const rows = roster.map((r: any) => [
formattedDate, dayName, r.employee_code, r.full_name, r.company_name || '-', r.branch_name || '-',
r.department_name || '-', r.designation || '-',
r.check_in ? r.check_in.substring(0, 5) : '-',
r.check_out ? r.check_out.substring(0, 5) : '-',
r.worked_hours, r.status
]);
// INCREASED Day column width to 70 to prevent "Wednesday" from wrapping
drawPdfTable(doc, headers, rows, [55, 70, 50, 90, 70, 60, 70, 80, 60, 65, 40, 60]);
doc.end();
await new Promise<void>((resolve) => doc.on('end', resolve));
const totalLength = chunks.reduce((acc, val) => acc + val.length, 0);
file_content = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
file_content.set(chunk, offset);
offset += chunk.length;
}
file_name = `Daily_Report_${req.date}.pdf`;
} else {
file_content = await generateExcelDailyReport(roster, formattedDate, dayName, generatedDate, summary);
file_name = `Daily_Report_${req.date}.xlsx`;
}
return { file_content, file_name };
};
export const exportAdminRangeReport = async (req: any) => {
const data = await getAdminRangeReport(req);
const report = data.report || [];
const fileType = req.fileType || 'excel';
let file_content: Uint8Array;
let file_name: string;
const formattedFromDate = new Date(req.fromDate + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
const formattedToDate = new Date(req.toDate + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' });
const generatedDate = new Date().toLocaleString('en-GB', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
const dates = getDaysArray(req.fromDate, req.toDate);
if (fileType === 'pdf') {
const doc = new PDFDocument({ margin: 30, size: 'A4', layout: 'landscape' });
const chunks: Uint8Array[] = [];
doc.on('data', (chunk: Uint8Array) => chunks.push(chunk));
// PDF Top Level Headings
doc.fontSize(10).fillColor('#000000').text('Report Name : Monthly Attendance Report', { align: 'left' });
doc.fontSize(10).fillColor('#000000').text(`Period : ${formattedFromDate} To ${formattedToDate}`, { align: 'left' });
doc.fontSize(10).fillColor('#000000').text(`Generated Date : ${generatedDate}`, { align: 'left' });
doc.moveDown();
const numDates = dates.length;
const fieldColWidth = 50;
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const dateColWidth = Math.max(18, (pageWidth - fieldColWidth) / numDates);
let y = doc.y;
report.forEach((emp: any) => {
const history = emp.attendance_history || [];
// Check page break
if (y + 80 > doc.page.height - doc.page.margins.bottom) {
doc.addPage();
y = doc.page.margins.top;
}
// --- Employee Summary Header ---
// Added "Name" and changed abbreviations to full words to match Excel logic
doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000');
const summaryText = `Emp Code: ${emp.employee_code} | Name: ${emp.first_name} ${emp.last_name} | Company: ${emp.company_name || '-'} | Branch: ${emp.branch_name || '-'} | Dept: ${emp.department_name || '-'} | Role: ${emp.designation || '-'} | Full: ${emp.range_metrics?.full_days || 0} | Half: ${emp.range_metrics?.half_days || 0} | Mispunches: ${emp.range_metrics?.mispunches || 0} | Late: ${emp.range_metrics?.late_arrivals || 0}`;
doc.fill('#F2F2F2').rect(doc.page.margins.left, y, pageWidth, 18).fill();
doc.fillColor('#000000').text(summaryText, doc.page.margins.left + 5, y + 4, { width: pageWidth - 10 });
y += 20;
// --- Table Header (Field | Date 1 | Date 2 | ...) ---
doc.font('Helvetica-Bold').fontSize(6);
let x = doc.page.margins.left;
doc.fill('#063B00').rect(x, y, fieldColWidth, 16).fill();
doc.fillColor('#FFFFFF').text('Field', x + 2, y + 4, { width: fieldColWidth - 4, align: 'center' });
doc.lineWidth(0.5).strokeColor('#000000').rect(x, y, fieldColWidth, 16).stroke();
x += fieldColWidth;
for (let i = 0; i < numDates; i++) {
const dateObj = new Date(dates[i] + 'T00:00:00');
const dayStr = dateObj.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'short' });
doc.fill('#063B00').rect(x, y, dateColWidth, 16).fill();
doc.fillColor('#FFFFFF').text(dayStr, x + 1, y + 2, { width: dateColWidth - 2, align: 'center' });
doc.fillColor('#FFFFFF').text(dayName, x + 1, y + 8, { width: dateColWidth - 2, align: 'center' });
doc.lineWidth(0.5).strokeColor('#000000').rect(x, y, dateColWidth, 16).stroke();
x += dateColWidth;
}
y += 16;
// --- Data Rows (Check-In, Check-Out, Hours, Status) ---
const fields = ['Check-In', 'Check-Out', 'Hours', 'Status'];
doc.font('Helvetica').fontSize(6);
fields.forEach((field) => {
if (y + 14 > doc.page.height - doc.page.margins.bottom) {
doc.addPage();
y = doc.page.margins.top;
}
let xRow = doc.page.margins.left;
doc.fillColor('#000000').text(field, xRow + 2, y + 3, { width: fieldColWidth - 4, align: 'left' });
doc.lineWidth(0.5).strokeColor('#000000').rect(xRow, y, fieldColWidth, 14).stroke();
xRow += fieldColWidth;
for (let i = 0; i < numDates; i++) {
const log = history.find((h: any) => h.work_date === dates[i]);
let text = '-';
let textColor = '#000000';
let bgColor = null;
if (log) {
if (field === 'Check-In') text = log.check_in ? log.check_in.substring(0, 5) : '-';
else if (field === 'Check-Out') text = log.check_out ? log.check_out.substring(0, 5) : '-';
else if (field === 'Hours') text = log.worked_hours ? Number(log.worked_hours).toFixed(2) : '0.00';
else if (field === 'Status') {
// Converted text to F, H, M, A, L while keeping the same colors
const statusStr = (log.final_status || '').toUpperCase();
if (statusStr === 'FULL_DAY') { text = 'F'; bgColor = '#C6EFCE'; textColor = '#006100'; }
else if (statusStr === 'ABSENT') { text = 'A'; bgColor = '#FFC7CE'; textColor = '#9C0006'; }
else if (statusStr === 'HALF_DAY') { text = 'H'; bgColor = '#FFEB9C'; textColor = '#9C6500'; }
else if (statusStr === 'MISPUNCH') { text = 'M'; bgColor = '#B1A0C7'; textColor = '#4B2E83'; }
else if (statusStr === 'LATE') { text = 'L'; bgColor = '#FFC000'; textColor = '#000000'; }
else { text = '-'; }
}
}
if (bgColor) {
doc.fill(bgColor).rect(xRow, y, dateColWidth, 14).fill();
}
doc.fillColor(textColor).text(text, xRow + 1, y + 3, { width: dateColWidth - 2, align: 'center' });
doc.lineWidth(0.5).strokeColor('#000000').rect(xRow, y, dateColWidth, 14).stroke();
xRow += dateColWidth;
}
y += 14;
});
y += 10;
});
doc.end();
await new Promise<void>((resolve) => doc.on('end', resolve));
const totalLength = chunks.reduce((acc, val) => acc + val.length, 0);
file_content = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
file_content.set(chunk, offset);
offset += chunk.length;
}
file_name = `Monthly_Report_${req.fromDate}_to_${req.toDate}.pdf`;
} else {
// Use the new Excel helper function for Matrix format
file_content = await generateExcelMonthlyReport(report, dates, formattedFromDate, formattedToDate, generatedDate);
file_name = `Monthly_Report_${req.fromDate}_to_${req.toDate}.xlsx`;
}
return { file_content, file_name };
};

View File

@ -32,7 +32,6 @@ export const getPendingRegularizations = async (user: any) => {
const employeeIds = [...new Set(rows.map((r: any) => r.employee_id))];
const roster = await getEmployeeRoster(employeeIds);
// FIX: Map using camelCase employeeId returned by gRPC
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
return rows.map((req: any) => {
@ -47,6 +46,36 @@ export const getPendingRegularizations = async (user: any) => {
});
};
export const getAllRegularizations = async () => {
const rows = await RegRepo.findAllRegularizations();
if (rows.length === 0) return [];
// Collect both employee_id and reviewed_by_id to fetch their names from EMS
const allIds = new Set<number>();
rows.forEach((r: any) => {
if (r.employee_id) allIds.add(Number(r.employee_id));
if (r.reviewed_by_id) allIds.add(Number(r.reviewed_by_id));
});
const roster = await getEmployeeRoster(Array.from(allIds));
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
return rows.map((req: any) => {
const emp = rosterMap.get(Number(req.employee_id));
const reviewer = rosterMap.get(Number(req.reviewed_by_id));
return {
...req,
employee_code: emp?.employeeCode || "N/A",
first_name: emp?.fullName?.split(" ")[0] || "",
last_name: emp?.fullName?.split(" ").slice(1).join(" ") || "",
branch_name: emp?.branchName || "N/A",
// FIX: EMS Roster returns 'fullName', not 'firstName'/'lastName'
reviewer_name: reviewer?.fullName || "Pending Review"
};
});
};
export const reviewRegularizationRequest = async (user: any, data: any) => {
if (!data.regularization_id || !["APPROVED", "REJECTED"].includes(data.action)) {
throw new Error("Invalid payload.");
@ -58,9 +87,17 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
const request = await RegRepo.findRegularizationById(data.regularization_id, connection);
if (!request) throw new Error("Regularization request not found.");
if (user.role !== AppRole.DIRECTOR && !user.managed_branches.includes(request.branch_id)) {
// AUTHORIZATION LOGIC: Allow Directors globally, but restrict HR Managers to their branches
if (user.role !== AppRole.DIRECTOR) {
if (user.role !== AppRole.HR_MANAGER) {
throw new Error("Access Denied: Only HR Managers and Directors can review requests.");
}
if (!user.managed_branches.includes(request.branch_id)) {
throw new Error("Access Denied: You do not manage this branch.");
}
}
if (request.status !== "PENDING") throw new Error("This request has already been processed.");
await RegRepo.updateRegularizationStatus(data.regularization_id, data.action, user.employee_id, connection);
@ -77,7 +114,7 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
const shift = await AttendanceRepo.findShiftDetails(emp.branchId, emp.companyId, connection);
if (request.requested_check_in && request.requested_check_out) {
// FIX: Safely convert Date objects to strings if necessary
// Safely convert Date objects to strings if necessary
rawCheckIn = request.requested_check_in instanceof Date
? request.requested_check_in.toISOString().replace("T", " ").substring(0, 19)
: request.requested_check_in;
@ -109,7 +146,7 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
}
}
// FIX: Use the safe string variables for the DB update
// Use the safe string variables for the DB update
if (request.attendance_id && rawCheckIn && rawCheckOut) {
await AttendanceRepo.updateProcessedAttendanceById(request.attendance_id, {
requested_check_in: rawCheckIn,

View File

@ -192,7 +192,9 @@ export const InternalServiceImplementation = {
companyName: emp.company_name || "",
departmentName: emp.department_name || "",
departmentId: Number(emp.department_id) || 0,
jobTitle: emp.job_title || ""
jobTitle: emp.job_title || "",
managerName: emp.manager_name || "No Manager Assigned",
managerId: Number(emp.manager_id) || 0
}));
return { success: true, data: mappedData };
} catch (error: any) {

View File

@ -3,9 +3,11 @@ import pool from "../db.ts";
export const findInternalRoster = async (ids?: number[]) => {
let query = `
SELECT e.employee_id, e.employee_code, e.is_active, CONCAT(p.first_name, ' ', p.last_name) AS full_name,
SELECT e.employee_id, e.employee_code, e.is_active, CONCAT_WS(' ', p.first_name, p.last_name) AS full_name,
b.branch_id, b.branch_name, c.company_id, c.name AS company_name,
d.name AS department_name, d.department_id, j.title AS job_title
d.name AS department_name, d.department_id, j.title AS job_title,
CONCAT_WS(' ', mgr_p.first_name, mgr_p.last_name) AS manager_name,
ea.reporting_to_id AS manager_id
FROM employees e
JOIN partners p ON e.partner_id = p.partner_id
JOIN companies c ON e.company_id = c.company_id
@ -13,6 +15,8 @@ export const findInternalRoster = async (ids?: number[]) => {
JOIN employee_assignments ea ON e.employee_id = ea.employee_id AND ea.is_current = TRUE
JOIN departments d ON ea.department_id = d.department_id
JOIN job_positions j ON ea.job_id = j.job_id
LEFT JOIN employees mgr_e ON ea.reporting_to_id = mgr_e.employee_id
LEFT JOIN partners mgr_p ON mgr_e.partner_id = mgr_p.partner_id
WHERE e.is_active = TRUE`;
const values: any[] = [];
@ -25,13 +29,3 @@ export const findInternalRoster = async (ids?: number[]) => {
const [rows] = await pool.query(query, values);
return rows;
};
export const findInternalManager = async (employeeId: number) => {
const [rows] = await pool.query(
`SELECT ea.reporting_to_id AS manager_id
FROM employee_assignments ea
WHERE ea.employee_id = ? AND ea.is_current = TRUE`,
[employeeId]
);
return (rows as any[])[0];
};

View File

@ -8,6 +8,7 @@ import { EmployeeServiceClient, LookupServiceClient, ContractServiceClient, Dash
import { AttendanceServiceClient, RegularizationServiceClient, DashboardServiceClient as AmsDashboardClient } from "../../../generated/ams.ts";
// LMS Clients
import { AdminServiceClient, EmployeeServiceClient as LmsEmployeeClient, LeaveServiceClient, ManagerServiceClient, ReportsServiceClient } from "../../../generated/lms.ts";
import { HRManagerServiceClient } from "../../../generated/lms.ts";
const creds = grpc.credentials.createInsecure();
@ -29,6 +30,7 @@ export const clients = {
leave: new LeaveServiceClient(config.urls.lms, creds),
manager: new ManagerServiceClient(config.urls.lms, creds),
reports: new ReportsServiceClient(config.urls.lms, creds),
hrManager: new HRManagerServiceClient(config.urls.lms, creds)
}
};

View File

@ -173,4 +173,60 @@ router.get("/api/ams/attendance/dashboard/metrics", requireRole([AppRole.DIRECTO
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
router.get("/api/ams/attendance/regularize/history", requireRole([AppRole.DIRECTOR, AppRole.HR_MANAGER]), async (ctx: any) => {
try {
const res: any = await callGrpc(clients.ams.regularization, "getAllRegularizations", {});
ctx.response.body = { success: res.success, count: res.data.length, data: res.data };
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
router.get("/api/ams/attendance/daily-report/export", requireRole([AppRole.DIRECTOR, AppRole.HR_MANAGER, AppRole.MANAGER]), async (ctx: any) => {
try {
const p = ctx.request.url.searchParams;
const req = {
date: p.get("date") || "",
companyId: p.get("company_id") ? Number(p.get("company_id")) : undefined,
branchId: p.get("branch_id") ? Number(p.get("branch_id")) : undefined,
departmentId: p.get("department_id") ? Number(p.get("department_id")) : undefined,
designation: p.get("designation") || undefined,
employeeName: p.get("employee_name") || undefined,
employeeCode: p.get("employee_code") || undefined,
fileType: p.get("file_type") || "excel" // NEW
};
const res: any = await callGrpc(clients.ams.attendance, "exportDailyReport", req);
ctx.response.headers.set('Content-Disposition', `attachment; filename="${res.fileName}"`);
ctx.response.headers.set('Content-Type', 'application/octet-stream');
ctx.response.body = res.fileContent;
} catch (e: any) {
ctx.response.status = 500;
ctx.response.body = { error: e.message };
}
});
router.get("/api/ams/attendance/admin-report/export", requireRole([AppRole.DIRECTOR, AppRole.HR_MANAGER, AppRole.MANAGER]), async (ctx: any) => {
try {
const p = ctx.request.url.searchParams;
const req = {
fromDate: p.get("from_date") || "",
toDate: p.get("to_date") || "",
branchId: p.get("branch_id") ? Number(p.get("branch_id")) : undefined,
departmentId: p.get("department_id") ? Number(p.get("department_id")) : undefined,
companyId: p.get("company_id") ? Number(p.get("company_id")) : undefined,
designation: p.get("designation") || undefined,
employeeName: p.get("employee_name") || undefined,
employeeCode: p.get("employee_code") || undefined,
fileType: p.get("file_type") || "excel" // NEW
};
const res: any = await callGrpc(clients.ams.attendance, "exportAdminRangeReport", req);
ctx.response.headers.set('Content-Disposition', `attachment; filename="${res.fileName}"`);
ctx.response.headers.set('Content-Type', 'application/octet-stream');
ctx.response.body = res.fileContent;
} catch (e: any) {
ctx.response.status = 500;
ctx.response.body = { error: e.message };
}
});
export default router;

View File

@ -126,6 +126,7 @@ router.get("/api/lms/holidays/valid-optional", async (ctx: any) => {
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
// Inside /api/lms/leaves/apply
router.post("/api/lms/leaves/apply", async (ctx: any) => {
try {
const body = await getJsonBody(ctx);
@ -137,7 +138,8 @@ router.post("/api/lms/leaves/apply", async (ctx: any) => {
dateFrom: body.date_from || "",
dateTo: body.date_to || "",
isHalfDay: !!body.is_half_day,
reason: body.reason || ""
reason: body.reason || "",
applicantRole: ctx.state.user.role // <--- ADD THIS LINE
};
const res: any = await callGrpc(clients.lms.leave, "applyForLeave", reqPayload);
ctx.response.status = 201;
@ -167,24 +169,28 @@ router.get("/api/lms/manager/pending", requireRole([AppRole.DIRECTOR, AppRole.HR
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
// Inside /api/lms/manager/leaves/:applicationId/approve
router.post("/api/lms/manager/leaves/:applicationId/approve", requireRole([AppRole.DIRECTOR, AppRole.HR_MANAGER, AppRole.MANAGER]), async (ctx: any) => {
try {
const reqPayload = {
managerId: Number(ctx.state.user.id),
applicationId: Number(ctx.params.applicationId)
applicationId: Number(ctx.params.applicationId),
userRole: ctx.state.user.role // INJECT ROLE
};
const res: any = await callGrpc(clients.lms.manager, "approveLeaveManager", reqPayload);
ctx.response.body = { success: res.success, message: res.message };
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
// Inside /api/lms/manager/leaves/:applicationId/reject
router.post("/api/lms/manager/leaves/:applicationId/reject", requireRole([AppRole.DIRECTOR, AppRole.HR_MANAGER, AppRole.MANAGER]), async (ctx: any) => {
try {
const body = await getJsonBody(ctx);
const reqPayload = {
managerId: Number(ctx.state.user.id),
applicationId: Number(ctx.params.applicationId),
reason: body.reason || ""
reason: body.reason || "",
userRole: ctx.state.user.role // INJECT ROLE
};
const res: any = await callGrpc(clients.lms.manager, "rejectLeaveManager", reqPayload);
ctx.response.body = { success: res.success, message: res.message };
@ -213,4 +219,13 @@ router.get("/api/lms/admin/reports/ob-cb", requireRole([AppRole.DIRECTOR, AppRol
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
// Add this route for HR to see all applications
router.get("/api/lms/hr/applications", requireRole([AppRole.HR_MANAGER, AppRole.DIRECTOR]), async (ctx: any) => {
try {
const res: any = await callGrpc(clients.lms.hrManager, "getAllLeaveApplications", {});
ctx.response.body = { success: res.success, count: res.data.length, data: res.data };
} catch (e: any) { ctx.response.status = 500; ctx.response.body = { error: e.message }; }
});
export default router;

View File

@ -0,0 +1,28 @@
import * as HRManagerService from "../services/hr_manager.service.ts";
export const HRManagerServiceImplementation = {
async getAllLeaveApplications(req: any) {
try {
const data = await HRManagerService.getAllApplications();
const mappedData = data.map((p: any) => ({
applicationId: Number(p.application_id) || 0,
employeeId: Number(p.employee_id) || 0,
leaveType: p.leave_type || "",
dateFrom: p.date_from || "",
dateTo: p.date_to || "",
numberOfDays: Number(p.number_of_days) || 0,
status: p.status || "",
reason: p.reason || "", // Ensure reason is mapped
employeeCode: p.employee_code || "",
firstName: p.first_name || "",
lastName: p.last_name || "",
departmentName: p.department_name || "",
jobTitle: p.job_title || "",
managerName: p.manager_name || "N/A" // Ensure managerName is mapped
}));
return { success: true, data: mappedData };
} catch (e: any) {
return { success: false, data: [] };
}
}
};

View File

@ -4,7 +4,6 @@ import * as LeaveService from "../services/leave.service.ts";
export const LeaveServiceImplementation = {
async applyForLeave(req: any) {
try {
// FIX: Map camelCase gRPC payload back to snake_case for the DB service layer
const dbPayload = {
employee_id: req.employeeId,
leave_type_id: req.leaveTypeId,
@ -14,6 +13,7 @@ export const LeaveServiceImplementation = {
date_to: req.dateTo,
is_half_day: req.isHalfDay,
reason: req.reason,
applicant_role: req.applicantRole // NEW
};
const result = await LeaveService.applyForLeave(dbPayload);

View File

@ -5,7 +5,7 @@ export const ManagerServiceImplementation = {
async getPendingManagerLeaves(req: any) {
try {
const user = { employee_id: req.employeeId, role: req.role };
const data = await ManagerService.getPendingManagerLeaves(user);
const data = await ManagerService.getPendingApprovals(user);
const mappedData = data.map((p: any) => ({
applicationId: Number(p.application_id) || 0,
employeeId: Number(p.employee_id) || 0,
@ -25,15 +25,15 @@ export const ManagerServiceImplementation = {
},
async approveLeaveManager(req: any) {
try {
const user = { employee_id: req.managerId };
await ManagerService.approveLeaveManager(user, Number(req.applicationId));
const user = { employee_id: req.managerId, role: req.userRole }; // Pass userRole
await ManagerService.approveLeave(user, Number(req.applicationId));
return { success: true, message: "Leave approved successfully." };
} catch (e: any) { return { success: false, message: e.message }; }
},
async rejectLeaveManager(req: any) {
try {
const user = { employee_id: req.managerId };
await ManagerService.rejectLeaveManager(user, Number(req.applicationId));
const user = { employee_id: req.managerId, role: req.userRole }; // Pass userRole
await ManagerService.rejectLeave(user, Number(req.applicationId));
return { success: true, message: "Leave rejected successfully." };
} catch (e: any) { return { success: false, message: e.message }; }
}

View File

@ -1,11 +1,12 @@
// services/lms/main.ts
import * as grpc from "@grpc/grpc-js";
import { AdminServiceService, EmployeeServiceService, LeaveServiceService, ManagerServiceService, ReportsServiceService } from "../../generated/lms.ts";
import { AdminServiceService, EmployeeServiceService, LeaveServiceService, ManagerServiceService, ReportsServiceService, HRManagerServiceService } from "../../generated/lms.ts";
import { AdminServiceImplementation } from "./handlers/admin.handler.ts";
import { EmployeeServiceImplementation } from "./handlers/employee.handler.ts";
import { LeaveServiceImplementation } from "./handlers/leave.handler.ts";
import { ManagerServiceImplementation } from "./handlers/manager.handler.ts";
import { ReportsServiceImplementation } from "./handlers/reports.handler.ts";
import { HRManagerServiceImplementation } from "./handlers/hr_manager.handler.ts";
const PORT = Number(Deno.env.get("LMS_PORT")) || 8003;
@ -30,6 +31,7 @@ server.addService(EmployeeServiceService, wrap(EmployeeServiceImplementation));
server.addService(LeaveServiceService, wrap(LeaveServiceImplementation));
server.addService(ManagerServiceService, wrap(ManagerServiceImplementation));
server.addService(ReportsServiceService, wrap(ReportsServiceImplementation));
server.addService(HRManagerServiceService, wrap(HRManagerServiceImplementation));
server.bindAsync(`0.0.0.0:${PORT}`, grpc.ServerCredentials.createInsecure(), (err, port) => {
if (err) { console.error("Failed to start gRPC server:", err); return; }

View File

@ -121,10 +121,11 @@ export const findMonthlyUsage = async (employeeId: number, leaveTypeId: number,
return Number(rows[0]?.used_this_month || 0);
};
// Update insertLeaveApplication
export const insertLeaveApplication = async (data: any, conn: any) => {
const [result] = await conn.execute(
`INSERT INTO leave_applications (employee_id, leave_type_id, company_id, date_from, date_to, number_of_days, reason, status, manager_approved_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[data.employee_id, data.leave_type_id, data.company_id, data.date_from, data.date_to, data.number_of_days, data.reason, data.status, data.manager_id]
`INSERT INTO leave_applications (employee_id, leave_type_id, company_id, date_from, date_to, number_of_days, reason, status, manager_approved_by, applicant_role, approver_role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[data.employee_id, data.leave_type_id, data.company_id, data.date_from, data.date_to, data.number_of_days, data.reason, data.status, data.manager_id, data.applicant_role, data.approver_role]
);
return result.insertId;
};
@ -170,14 +171,14 @@ export const updateAllocationUsedDays = async (employeeId: number, leaveTypeId:
await conn.execute(`UPDATE leave_allocations SET used_days = used_days + ? WHERE employee_id = ? AND leave_type_id = ? AND calendar_year = ?`, [days, employeeId, leaveTypeId, year]);
};
export const rejectLeaveApplication = async (applicationId: number, managerId: number) => {
const [result] = await pool.execute(
`UPDATE leave_applications SET status = 'REJECTED' WHERE application_id = ? AND manager_approved_by = ? AND status IN ('PENDING', 'PENDING_LOP')`,
[applicationId, managerId]
);
// Update rejectLeaveApplication to allow any authorized role to reject
export const rejectLeaveApplication = async (applicationId: number, approverRole: string) => {
let query = `UPDATE leave_applications SET status = 'REJECTED' WHERE application_id = ? AND status IN ('PENDING', 'PENDING_LOP') AND approver_role = ?`;
const [result] = await pool.execute(query, [applicationId, approverRole]);
return result.affectedRows > 0;
};
// ==========================================
// REPORTS
// ==========================================
@ -192,3 +193,53 @@ export const findLedgerReportData = async (year: number, month: number, companyI
);
return rows;
};
// ==========================================
// HR MANAGER ACTIONS
// ==========================================
export const findAllApplications = async () => {
const [rows] = await pool.execute(
`SELECT la.application_id, la.employee_id, lt.name AS leave_type,
DATE_FORMAT(la.date_from, '%Y-%m-%d') as date_from,
DATE_FORMAT(la.date_to, '%Y-%m-%d') as date_to,
la.number_of_days, la.status, la.reason
FROM leave_applications la
INNER JOIN leave_types lt ON la.leave_type_id = lt.leave_type_id
ORDER BY la.date_from DESC`
);
return rows;
};
// Replace findPendingManagerLeaves with dynamic routing
export const findPendingForApprover = async (approverId: number, approverRole: string) => {
let query = `SELECT la.application_id, la.employee_id, la.leave_type_id, lt.name AS leave_type, la.date_from, la.date_to, la.number_of_days, la.reason, la.status
FROM leave_applications la INNER JOIN leave_types lt ON la.leave_type_id = lt.leave_type_id WHERE la.status IN ('PENDING', 'PENDING_LOP')`;
const params: any[] = [];
if (approverRole === 'MANAGER') {
query += ` AND la.approver_role = 'MANAGER' AND la.manager_approved_by = ?`;
params.push(approverId);
} else if (approverRole === 'HR_MANAGER') {
query += ` AND la.approver_role = 'HR_MANAGER'`;
} else if (approverRole === 'DIRECTOR') {
query += ` AND la.approver_role = 'DIRECTOR'`;
} else {
return [];
}
query += ` ORDER BY la.date_from ASC`;
const [rows] = await pool.execute(query, params);
return rows;
};

View File

@ -0,0 +1,24 @@
import { getEmployeeRoster } from "core/internal-client.ts";
import * as LmsRepo from "../repositories/lms.repository.ts";
export const getAllApplications = async () => {
const rows = await LmsRepo.findAllApplications();
if (rows.length === 0) return [];
const employeeIds = [...new Set(rows.map((r: any) => r.employee_id))];
const roster = await getEmployeeRoster(employeeIds);
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
return rows.map((app: any) => {
const emp = rosterMap.get(app.employee_id);
return {
...app,
employee_code: emp?.employeeCode || "N/A",
first_name: emp?.fullName?.split(" ")[0] || "",
last_name: emp?.fullName?.split(" ").slice(1).join(" ") || "",
department_name: emp?.departmentName || "N/A",
job_title: emp?.jobTitle || "N/A",
manager_name: emp?.managerName || "N/A" // <--- ADD THIS LINE!
};
});
};

View File

@ -1,7 +1,6 @@
// services/lms/services/leave.service.ts
import { getEmployeeRoster } from './../../../packages/core/internal-client.ts';
import pool from "../db.ts";
import { calculateLeaveDays } from "core/calendar.service.ts";
import { getEmployeeManagerId } from "core/internal-client.ts";
import * as LmsRepo from "../repositories/lms.repository.ts";
export const applyForLeave = async (payload: any) => {
@ -32,6 +31,11 @@ export const applyForLeave = async (payload: any) => {
const rawBalance = Number(await LmsRepo.findRawBalance(payload.employee_id, payload.leave_type_id, currentYear) || 0);
const pendingDays = await LmsRepo.findPendingDays(payload.employee_id, payload.leave_type_id, currentYear);
const availableBalance = rawBalance - pendingDays;
if (availableBalance <= 0) {
throw new Error("You have insufficient leave balance. Cannot apply for leave.");
}
const usedThisMonth = await LmsRepo.findMonthlyUsage(payload.employee_id, payload.leave_type_id, currentMonth, currentYear);
// 4. Determine Paid vs LOP Split
@ -46,8 +50,29 @@ export const applyForLeave = async (payload: any) => {
lopDays = requestedDays - paidDays;
}
// 5. Fetch Manager ID via HTTP
const managerId = await getEmployeeManagerId(payload.employee_id);
// console.log(`[LMS] Applying leave for Employee ${payload.employee_id}. Applicant Role: ${payload.applicant_role}`);
let approverRole = 'MANAGER';
let managerId = null;
if (payload.applicant_role === 'EMPLOYEE') {
approverRole = 'MANAGER';
// NEW: Use getEmployeeRoster instead of getEmployeeManagerId
const roster = await getEmployeeRoster([payload.employee_id]);
if (roster.length === 0) throw new Error("Employee not found in EMS roster.");
managerId = roster[0].managerId;
// console.log(`[LMS] Fetched Manager ID from Roster for Employee: ${managerId}`);
if (!managerId) throw new Error("Could not determine reporting manager. Please assign a manager to this employee in EMS.");
} else if (payload.applicant_role === 'MANAGER') {
approverRole = 'HR_MANAGER';
} else if (payload.applicant_role === 'HR_MANAGER') {
approverRole = 'DIRECTOR';
} else if (payload.applicant_role === 'DIRECTOR') {
approverRole = 'HR_MANAGER';
}
// 6. Execute DB Transaction
const connection = await pool.getConnection();
@ -56,12 +81,26 @@ export const applyForLeave = async (payload: any) => {
const insertedIds = [];
if (paidDays > 0) {
const id = await LmsRepo.insertLeaveApplication({ ...payload, number_of_days: paidDays, status: 'PENDING', manager_id: managerId }, connection);
const id = await LmsRepo.insertLeaveApplication({
...payload,
number_of_days: paidDays,
status: 'PENDING',
manager_id: managerId,
applicant_role: payload.applicant_role,
approver_role: approverRole
}, connection);
insertedIds.push(id);
}
if (lopDays > 0) {
const id = await LmsRepo.insertLeaveApplication({ ...payload, number_of_days: lopDays, status: 'PENDING_LOP', manager_id: managerId }, connection);
const id = await LmsRepo.insertLeaveApplication({
...payload,
number_of_days: lopDays,
status: 'PENDING_LOP',
manager_id: managerId,
applicant_role: payload.applicant_role,
approver_role: approverRole
}, connection);
insertedIds.push(id);
}

View File

@ -72,3 +72,89 @@ export const rejectLeaveManager = async (user: any, applicationId: number) => {
if (!success) throw new Error("Leave not found, already processed, or you lack permission.");
return true;
};
export const getPendingApprovals = async (user: any) => {
const rows = await LmsRepo.findPendingForApprover(user.employee_id, user.role);
if (rows.length === 0) return [];
const employeeIds = [...new Set(rows.map((r: any) => r.employee_id))];
const roster = await getEmployeeRoster(employeeIds);
const rosterMap = new Map(roster.map((e: any) => [e.employeeId, e]));
return rows.map((app: any) => {
const emp = rosterMap.get(app.employee_id);
return {
...app,
employee_code: emp?.employeeCode || "N/A",
first_name: emp?.fullName?.split(" ")[0] || "",
last_name: emp?.fullName?.split(" ").slice(1).join(" ") || "",
};
});
};
export const approveLeave = async (user: any, applicationId: number) => {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const leave = await LmsRepo.findLeaveForUpdate(applicationId, connection);
if (!leave) throw new Error("Leave application not found.");
// Authorization Check
if (user.role === 'MANAGER' && (leave.approver_role !== 'MANAGER' || leave.manager_approved_by !== user.employee_id)) {
throw new Error("You are not authorized to approve this leave.");
}
if (user.role === 'HR_MANAGER' && leave.approver_role !== 'HR_MANAGER') {
throw new Error("You are not authorized to approve this leave.");
}
if (user.role === 'DIRECTOR' && leave.approver_role !== 'DIRECTOR') {
throw new Error("You are not authorized to approve this leave.");
}
const newStatus = leave.status === 'PENDING_LOP' ? 'APPROVED_LOP' : 'APPROVED';
await LmsRepo.updateLeaveStatus(applicationId, newStatus, connection);
// Record who approved it in the correct column
if (user.role === 'MANAGER') {
await connection.execute(`UPDATE leave_applications SET manager_approved_by = ? WHERE application_id = ?`, [user.employee_id, applicationId]);
} else {
await connection.execute(`UPDATE leave_applications SET hr_approved_by = ? WHERE application_id = ?`, [user.employee_id, applicationId]);
}
// Deduct balance only if paid
if (newStatus === 'APPROVED') {
const currentYear = new Date(leave.date_from).getFullYear();
const currentMonth = new Date(leave.date_from).getMonth() + 1;
const alloc = await LmsRepo.findAllocForUpdate(leave.employee_id, leave.leave_type_id, currentYear, connection);
if (alloc) {
const openingBalance = Number(alloc.granted_days) - Number(alloc.used_days);
const closingBalance = openingBalance - Number(leave.number_of_days);
await LmsRepo.insertLedgerTransaction({
employee_id: leave.employee_id, leave_type_id: leave.leave_type_id, application_id: applicationId,
year: currentYear, month: currentMonth, days: leave.number_of_days,
opening_balance: openingBalance, closing_balance: closingBalance,
remarks: `Approved by: ${user.role} ID: ${user.employee_id}`
}, connection);
await LmsRepo.updateAllocationUsedDays(leave.employee_id, leave.leave_type_id, currentYear, leave.number_of_days, connection);
}
}
await connection.commit();
return { new_status: newStatus };
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
export const rejectLeave = async (user: any, applicationId: number) => {
const success = await LmsRepo.rejectLeaveApplication(applicationId, user.role);
if (!success) throw new Error("Leave not found, already processed, or you lack permission.");
return true;
};