Compare commits

..

No commits in common. "7d1b7609345728eb0a30382037430b0cbea9d0bd" and "de6d25b2566f57a326ebb24de149175318f4b87d" have entirely different histories.

28 changed files with 173 additions and 2975 deletions

968
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -102,7 +102,6 @@ export interface DailyReportRequest {
designation?: string | undefined;
employeeName?: string | undefined;
employeeCode?: string | undefined;
fileType: string;
}
export interface DailySummary {
@ -145,15 +144,23 @@ export interface RangeReportRequest {
fromDate: string;
toDate: string;
branchId?: number | undefined;
departmentId?: number | undefined;
companyId?: number | undefined;
designation?: string | undefined;
employeeName?: string | undefined;
employeeCode?:
departmentId?:
| number
| undefined;
/** NEW */
companyId?:
| number
| undefined;
/** NEW */
designation?:
| string
| undefined;
/** <--- ADD THIS LINE */
fileType: string;
/** NEW */
employeeName?:
| string
| undefined;
/** NEW */
employeeCode?: string | undefined;
}
export interface RangeMetrics {
@ -270,7 +277,6 @@ export interface PendingRegData {
firstName: string;
lastName: string;
branchName: string;
reviewerName: string;
}
export interface PendingRegResponse {
@ -286,17 +292,6 @@ 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 {};
}
@ -1321,7 +1316,6 @@ function createBaseDailyReportRequest(): DailyReportRequest {
designation: undefined,
employeeName: undefined,
employeeCode: undefined,
fileType: "",
};
}
@ -1348,9 +1342,6 @@ 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;
},
@ -1417,14 +1408,6 @@ 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;
@ -1463,11 +1446,6 @@ 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)
: "",
};
},
@ -1494,9 +1472,6 @@ export const DailyReportRequest: MessageFns<DailyReportRequest> = {
if (message.employeeCode !== undefined) {
obj.employeeCode = message.employeeCode;
}
if (message.fileType !== "") {
obj.fileType = message.fileType;
}
return obj;
},
@ -1512,7 +1487,6 @@ 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;
},
};
@ -2073,7 +2047,6 @@ function createBaseRangeReportRequest(): RangeReportRequest {
designation: undefined,
employeeName: undefined,
employeeCode: undefined,
fileType: "",
};
}
@ -2103,9 +2076,6 @@ 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;
},
@ -2180,14 +2150,6 @@ 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;
@ -2235,11 +2197,6 @@ 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)
: "",
};
},
@ -2269,9 +2226,6 @@ export const RangeReportRequest: MessageFns<RangeReportRequest> = {
if (message.employeeCode !== undefined) {
obj.employeeCode = message.employeeCode;
}
if (message.fileType !== "") {
obj.fileType = message.fileType;
}
return obj;
},
@ -2288,7 +2242,6 @@ 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;
},
};
@ -3869,7 +3822,6 @@ function createBasePendingRegData(): PendingRegData {
firstName: "",
lastName: "",
branchName: "",
reviewerName: "",
};
}
@ -3914,9 +3866,6 @@ 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;
},
@ -4031,14 +3980,6 @@ 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;
@ -4107,11 +4048,6 @@ 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)
: "",
};
},
@ -4156,9 +4092,6 @@ export const PendingRegData: MessageFns<PendingRegData> = {
if (message.branchName !== "") {
obj.branchName = message.branchName;
}
if (message.reviewerName !== "") {
obj.reviewerName = message.reviewerName;
}
return obj;
},
@ -4180,7 +4113,6 @@ export const PendingRegData: MessageFns<PendingRegData> = {
message.firstName = object.firstName ?? "";
message.lastName = object.lastName ?? "";
message.branchName = object.branchName ?? "";
message.reviewerName = object.reviewerName ?? "";
return message;
},
};
@ -4413,166 +4345,6 @@ 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
@ -4638,26 +4410,6 @@ 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 {
@ -4667,8 +4419,6 @@ 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 {
@ -4762,36 +4512,6 @@ 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(
@ -4832,25 +4552,12 @@ 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 {
@ -4899,22 +4606,6 @@ 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(
@ -4971,14 +4662,6 @@ 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,8 +307,6 @@ export interface RosterEmployee {
departmentName: string;
departmentId: number;
jobTitle: string;
managerName: string;
managerId: string;
}
export interface RosterResponse {
@ -4774,8 +4772,6 @@ function createBaseRosterEmployee(): RosterEmployee {
departmentName: "",
departmentId: 0,
jobTitle: "",
managerName: "",
managerId: "",
};
}
@ -4814,12 +4810,6 @@ 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;
},
@ -4918,22 +4908,6 @@ 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;
@ -5000,16 +4974,6 @@ 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)
: "",
};
},
@ -5048,12 +5012,6 @@ 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;
},
@ -5073,8 +5031,6 @@ 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,8 +174,6 @@ export interface LeaveApplicationPayload {
dateTo: string;
isHalfDay: boolean;
reason: string;
/** NEW */
applicantRole: string;
}
export interface ApplyLeaveResponse {
@ -195,11 +193,8 @@ export interface ApplyLeaveResponse {
export interface ManagerActionRequest {
managerId: number;
applicationId: number;
reason?:
| string
| undefined;
/** NEW */
userRole: string;
/** For rejection */
reason?: string | undefined;
}
export interface PendingLeaveData {
@ -251,31 +246,6 @@ 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: "" };
}
@ -2309,7 +2279,6 @@ function createBaseLeaveApplicationPayload(): LeaveApplicationPayload {
dateTo: "",
isHalfDay: false,
reason: "",
applicantRole: "",
};
}
@ -2339,9 +2308,6 @@ 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;
},
@ -2416,14 +2382,6 @@ 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;
@ -2471,11 +2429,6 @@ 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)
: "",
};
},
@ -2505,9 +2458,6 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
if (message.reason !== "") {
obj.reason = message.reason;
}
if (message.applicantRole !== "") {
obj.applicantRole = message.applicantRole;
}
return obj;
},
@ -2524,7 +2474,6 @@ export const LeaveApplicationPayload: MessageFns<LeaveApplicationPayload> = {
message.dateTo = object.dateTo ?? "";
message.isHalfDay = object.isHalfDay ?? false;
message.reason = object.reason ?? "";
message.applicantRole = object.applicantRole ?? "";
return message;
},
};
@ -2698,7 +2647,7 @@ export const ApplyLeaveResponse: MessageFns<ApplyLeaveResponse> = {
};
function createBaseManagerActionRequest(): ManagerActionRequest {
return { managerId: 0, applicationId: 0, reason: undefined, userRole: "" };
return { managerId: 0, applicationId: 0, reason: undefined };
}
export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
@ -2712,9 +2661,6 @@ 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;
},
@ -2749,14 +2695,6 @@ 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;
@ -2779,11 +2717,6 @@ 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)
: "",
};
},
@ -2798,9 +2731,6 @@ export const ManagerActionRequest: MessageFns<ManagerActionRequest> = {
if (message.reason !== undefined) {
obj.reason = message.reason;
}
if (message.userRole !== "") {
obj.userRole = message.userRole;
}
return obj;
},
@ -2812,7 +2742,6 @@ 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;
},
};
@ -3607,456 +3536,6 @@ 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
@ -4481,56 +3960,6 @@ 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', 'HALF_DAY', 'WFH_REQUEST', 'ON_DUTY') NOT NULL;
regularization_type ENUM('MISPUNCH', 'WFH_REQUEST', 'ON_DUTY') NOT NULL,
target_date DATE NOT NULL,
requested_check_in TIMESTAMP NULL,
requested_check_out TIMESTAMP NULL,

View File

@ -58,8 +58,6 @@ 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,15 +12,12 @@ 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 {
@ -100,7 +97,6 @@ message DailyReportRequest {
optional string designation = 5; // Job Title
optional string employee_name = 6;
optional string employee_code = 7;
string file_type = 8;
}
message DailySummary {
@ -140,11 +136,10 @@ message RangeReportRequest {
string to_date = 2;
optional int32 branch_id = 3;
optional int32 department_id = 4;
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
optional int32 company_id = 5; // NEW
optional string designation = 6; // NEW
optional string employee_name = 7; // NEW
optional string employee_code = 8; // NEW
}
message RangeMetrics {
@ -255,7 +250,6 @@ message PendingRegData {
string first_name = 11;
string last_name = 12;
string branch_name = 13;
string reviewer_name = 14;
}
message PendingRegResponse {
@ -270,18 +264,3 @@ 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,8 +320,6 @@ 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,7 +173,6 @@ message LeaveApplicationPayload {
string date_to = 6;
bool is_half_day = 7;
string reason = 8;
string applicant_role = 9; // NEW
}
message ApplyLeaveResponse {
@ -191,8 +190,7 @@ message ApplyLeaveResponse {
message ManagerActionRequest {
int32 manager_id = 1;
int32 application_id = 2;
optional string reason = 3;
string user_role = 4; // NEW
optional string reason = 3; // For rejection
}
message PendingLeaveData {
@ -241,36 +239,3 @@ 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,25 +178,5 @@ 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,30 +69,5 @@ 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,13 +40,3 @@ 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,337 +2,15 @@
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) {
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.designation) filtered = filtered.filter(e => e.jobTitle?.toLowerCase().includes(filters.designation.toLowerCase()) ?? false);
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;
@ -358,6 +36,10 @@ 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) {
@ -375,6 +57,7 @@ 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]);
@ -386,6 +69,7 @@ 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) => {
@ -397,17 +81,58 @@ 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;
@ -419,16 +144,19 @@ 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);
@ -440,8 +168,12 @@ 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";
}
@ -449,7 +181,7 @@ export const processDailyAttendance = async (body: any) => {
} else if (punches.length === 1) {
check_in = punches[0];
check_in_status = "MISPUNCH";
check_in_status = "MISPUNCH"; // Excluded from late penalty logic
final_status = "MISPUNCH";
}
@ -497,6 +229,7 @@ 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 };
@ -537,6 +270,7 @@ 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: [] };
@ -588,7 +322,7 @@ export const getSingleEmployeeRangeReport = async (employeeId: number, fromDate:
return {
employee: {
employee_id: empProfile.employeeId,
employee_id: empProfile.employeeId,
employee_code: empProfile.employeeCode,
first_name: empProfile.fullName?.split(" ")[0] || "",
last_name: empProfile.fullName?.split(" ").slice(1).join(" ") || ""
@ -602,6 +336,7 @@ 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);
@ -618,203 +353,3 @@ 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,6 +32,7 @@ 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) => {
@ -46,36 +47,6 @@ 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.");
@ -87,17 +58,9 @@ 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.");
// 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 (user.role !== AppRole.DIRECTOR && !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);
@ -114,7 +77,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) {
// Safely convert Date objects to strings if necessary
// FIX: 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;
@ -146,7 +109,7 @@ export const reviewRegularizationRequest = async (user: any, data: any) => {
}
}
// Use the safe string variables for the DB update
// FIX: 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

@ -27,12 +27,12 @@ export const EmployeeServiceImplementation = {
}));
return { success: true, count: Number(mappedData.length), data: mappedData };
} catch (error: any) {
console.error("gRPC getEmployees Error:", error);
console.error("gRPC getEmployees Error:", error);
return { success: false, count: 0, data: [] };
}
},
async getEmployeeById(req: any) {
async getEmployeeById(req: any) {
try {
const emp = await EmployeeService.getEmployeeById(Number(req.id));
@ -140,7 +140,7 @@ export const EmployeeServiceImplementation = {
}
},
async updateEmployee(req: any) {
async updateEmployee(req: any) {
try {
const dbPayload = {
firstName: req.data?.firstName,
@ -177,7 +177,7 @@ export const EmployeeServiceImplementation = {
};
export const InternalServiceImplementation = {
async getRoster(req: any) {
async getRoster(req: any) {
try {
const ids = req.ids || [];
const data = await InternalService.getRoster(ids.length > 0 ? ids.join(",") : undefined);
@ -192,9 +192,7 @@ export const InternalServiceImplementation = {
companyName: emp.company_name || "",
departmentName: emp.department_name || "",
departmentId: Number(emp.department_id) || 0,
jobTitle: emp.job_title || "",
managerName: emp.manager_name || "No Manager Assigned",
managerId: Number(emp.manager_id) || 0
jobTitle: emp.job_title || ""
}));
return { success: true, data: mappedData };
} catch (error: any) {

View File

@ -3,11 +3,9 @@ import pool from "../db.ts";
export const findInternalRoster = async (ids?: number[]) => {
let query = `
SELECT e.employee_id, e.employee_code, e.is_active, CONCAT_WS(' ', p.first_name, p.last_name) AS full_name,
SELECT e.employee_id, e.employee_code, e.is_active, CONCAT(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,
CONCAT_WS(' ', mgr_p.first_name, mgr_p.last_name) AS manager_name,
ea.reporting_to_id AS manager_id
d.name AS department_name, d.department_id, j.title AS job_title
FROM employees e
JOIN partners p ON e.partner_id = p.partner_id
JOIN companies c ON e.company_id = c.company_id
@ -15,8 +13,6 @@ 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[] = [];
@ -29,3 +25,13 @@ 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,7 +8,6 @@ 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();
@ -30,7 +29,6 @@ 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,60 +173,4 @@ 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,7 +126,6 @@ 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);
@ -138,8 +137,7 @@ 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 || "",
applicantRole: ctx.state.user.role // <--- ADD THIS LINE
reason: body.reason || ""
};
const res: any = await callGrpc(clients.lms.leave, "applyForLeave", reqPayload);
ctx.response.status = 201;
@ -169,28 +167,24 @@ 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),
userRole: ctx.state.user.role // INJECT ROLE
applicationId: Number(ctx.params.applicationId)
};
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 || "",
userRole: ctx.state.user.role // INJECT ROLE
reason: body.reason || ""
};
const res: any = await callGrpc(clients.lms.manager, "rejectLeaveManager", reqPayload);
ctx.response.body = { success: res.success, message: res.message };
@ -219,13 +213,4 @@ 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

@ -1,28 +0,0 @@
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

@ -3,7 +3,8 @@ import * as LeaveService from "../services/leave.service.ts";
export const LeaveServiceImplementation = {
async applyForLeave(req: any) {
try {
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,
@ -13,7 +14,6 @@ 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.getPendingApprovals(user);
const data = await ManagerService.getPendingManagerLeaves(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, role: req.userRole }; // Pass userRole
await ManagerService.approveLeave(user, Number(req.applicationId));
const user = { employee_id: req.managerId };
await ManagerService.approveLeaveManager(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, role: req.userRole }; // Pass userRole
await ManagerService.rejectLeave(user, Number(req.applicationId));
const user = { employee_id: req.managerId };
await ManagerService.rejectLeaveManager(user, Number(req.applicationId));
return { success: true, message: "Leave rejected successfully." };
} catch (e: any) { return { success: false, message: e.message }; }
}

View File

@ -1,12 +1,11 @@
// services/lms/main.ts
import * as grpc from "@grpc/grpc-js";
import { AdminServiceService, EmployeeServiceService, LeaveServiceService, ManagerServiceService, ReportsServiceService, HRManagerServiceService } from "../../generated/lms.ts";
import { AdminServiceService, EmployeeServiceService, LeaveServiceService, ManagerServiceService, ReportsServiceService } 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;
@ -31,7 +30,6 @@ 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,11 +121,10 @@ 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, 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]
`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]
);
return result.insertId;
};
@ -171,14 +170,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]);
};
// 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]);
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]
);
return result.affectedRows > 0;
};
// ==========================================
// REPORTS
// ==========================================
@ -193,53 +192,3 @@ 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

@ -1,24 +0,0 @@
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,6 +1,7 @@
import { getEmployeeRoster } from './../../../packages/core/internal-client.ts';
// services/lms/services/leave.service.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) => {
@ -31,11 +32,6 @@ 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
@ -50,29 +46,8 @@ export const applyForLeave = async (payload: any) => {
lopDays = requestedDays - paidDays;
}
// 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';
}
// 5. Fetch Manager ID via HTTP
const managerId = await getEmployeeManagerId(payload.employee_id);
// 6. Execute DB Transaction
const connection = await pool.getConnection();
@ -81,26 +56,12 @@ 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,
applicant_role: payload.applicant_role,
approver_role: approverRole
}, connection);
const id = await LmsRepo.insertLeaveApplication({ ...payload, number_of_days: paidDays, status: 'PENDING', manager_id: managerId }, connection);
insertedIds.push(id);
}
if (lopDays > 0) {
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);
const id = await LmsRepo.insertLeaveApplication({ ...payload, number_of_days: lopDays, status: 'PENDING_LOP', manager_id: managerId }, connection);
insertedIds.push(id);
}

View File

@ -72,89 +72,3 @@ 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;
};