Added new malling list
This commit is contained in:
parent
2696003a09
commit
a0c83caa14
@ -3,17 +3,24 @@
|
|||||||
"SMPTSERVER": "smtp.gmail.com",
|
"SMPTSERVER": "smtp.gmail.com",
|
||||||
"PORT": 587,
|
"PORT": 587,
|
||||||
"SENDER_EMAIL": "marcoioitsoft@gmail.com",
|
"SENDER_EMAIL": "marcoioitsoft@gmail.com",
|
||||||
"SENDER_PASSWORD": "qrtq wfuj hwpp fhqr"
|
"SENDER_PASSWORD": "qrtq wfuj hwpp fhqr",
|
||||||
|
"RECIPIENT_EMAILS": "ashutosh.nehete@marcoaiot.com,vikas@marcoaiot.com"
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"BASE_URL": "http://localhost:5032/api",
|
"BASE_URL": "https://stageapi.marcoaiot.com/api",
|
||||||
"USERNAME": "admin@marcoaiot.com",
|
"USERNAME": "admin@marcoaiot.com",
|
||||||
"PASSWORD": "User@123"
|
"PASSWORD": "User@123",
|
||||||
|
"TENANTID": "b3466e83-7e11-464c-b93a-daf047838b26"
|
||||||
|
},
|
||||||
|
"WEB":{
|
||||||
|
"BASE_URL": "https://stageapp.marcoaiot.com"
|
||||||
},
|
},
|
||||||
"MONGODB":{
|
"MONGODB":{
|
||||||
"MONGO_CONNECTION_STRING": "mongodb://localhost:27017",
|
"MONGO_CONNECTION_STRING": "mongodb://localhost:27017",
|
||||||
"DATABASE_NAME": "MarcoBMS_Caches",
|
"DATABASE_NAME": "MarcoBMS_Caches",
|
||||||
"COLLECTION_NAME": "ProjectReportMail"
|
"COLLECTION_NAME": "ProjectReportMail"
|
||||||
|
},
|
||||||
|
"UNIQUE_IDENTIFIER":{
|
||||||
|
"PROJECT_IDS":"2618eb89-2823-11f0-9d9e-bc241163f504"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
603
mailling/dpr.html
Normal file
603
mailling/dpr.html
Normal file
@ -0,0 +1,603 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Daily Progress Report</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #49bf3c;
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .project-info {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-note {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
padding: 15px 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cards {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
/* <-- added shadow */
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
border-top: 1px solid #e63946;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-link {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #49bf3c;
|
||||||
|
/* arrow color */
|
||||||
|
background: #f5f5f5;
|
||||||
|
/* badge background */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-link:hover {
|
||||||
|
background: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-link:focus {
|
||||||
|
outline: 2px solid #9ca3af;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-link .arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-link {
|
||||||
|
display: inline-flex;
|
||||||
|
/* <— show only on this card */
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attendance {
|
||||||
|
color: #49bf3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activities {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activities h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
/* background: #b10000; */
|
||||||
|
background: #49bf3c;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .project-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cards {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-red {
|
||||||
|
background: #49bf3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-blue {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-green {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-gray {
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.donut {
|
||||||
|
--percentage: 0;
|
||||||
|
/* Change this per chart */
|
||||||
|
--danger: #e63946;
|
||||||
|
--primary: #007bff;
|
||||||
|
--warning: #ffc107;
|
||||||
|
--success: #198754;
|
||||||
|
/* Fill color */
|
||||||
|
--track: #e9ecef;
|
||||||
|
/* Background track */
|
||||||
|
--size: 120px;
|
||||||
|
/* Default size */
|
||||||
|
--thickness: 20px;
|
||||||
|
/* Default thickness */
|
||||||
|
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: 50%;
|
||||||
|
/* background: conic-gradient(var(--danger) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0); */
|
||||||
|
background: conic-gradient(var(--track) 100%);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: calc(var(--size) - var(--thickness));
|
||||||
|
height: calc(var(--size) - var(--thickness));
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
/* Inner cut-out */
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut span {
|
||||||
|
position: absolute;
|
||||||
|
font-size: calc(var(--size) / 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.donut.thin {
|
||||||
|
--size: 80px;
|
||||||
|
--thickness: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut.medium {
|
||||||
|
--size: 120px;
|
||||||
|
--thickness: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut.large {
|
||||||
|
--size: 180px;
|
||||||
|
--thickness: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color variants */
|
||||||
|
.donut-success {
|
||||||
|
background: conic-gradient(var(--success) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0);
|
||||||
|
color: var(--success)
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-warning {
|
||||||
|
background: conic-gradient(var(--warning) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0);
|
||||||
|
color: var(--warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-danger {
|
||||||
|
background: conic-gradient(var(--danger) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0);
|
||||||
|
color: var(--danger)
|
||||||
|
}
|
||||||
|
|
||||||
|
.donut-primary {
|
||||||
|
background: conic-gradient(var(--primary) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0);
|
||||||
|
color: var(--primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
.values {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values p {
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h1>Daily Progress Report</h1>
|
||||||
|
<div class="project-info">
|
||||||
|
<strong>Project:</strong>{{projectName}}<br>
|
||||||
|
<strong>Date:</strong> {{date}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Note -->
|
||||||
|
<div class="status-note">
|
||||||
|
* Project Status Reported - Generated at {{timeStamp}} UTC
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Cards -->
|
||||||
|
<div class="status-cards">
|
||||||
|
<div class="card">
|
||||||
|
<a class="card-link" href={{webUrl}} aria-label="Open original website"
|
||||||
|
title="Open website" target="_blank" rel="noopener">
|
||||||
|
<span class="arrow">↗</span>
|
||||||
|
</a>
|
||||||
|
<h4 class="card-title">TODAY'S ATTENDANCE</h4>
|
||||||
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
|
<!-- Medium -->
|
||||||
|
<div class="donut thin donut-primary" style="--percentage: {{attendancePercentage}};">
|
||||||
|
<span class="values">
|
||||||
|
<p style="color:#007bff;">{{todaysAttendances}}</p> / <p style="color:#ccc;">{{totalEmployees}}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
||||||
|
<span class="legend-color legend-blue"></span> Today's Attendance
|
||||||
|
</div>
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
||||||
|
<span class="legend-color legend-gray"></span> Total Employees
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<a class="card-link" href={{webUrl}} aria-label="Open original website"
|
||||||
|
title="Open website" target="_blank" rel="noopener">
|
||||||
|
<span class="arrow">↗</span>
|
||||||
|
</a>
|
||||||
|
<h4 class="card-title">DAILY TASKS COMPLETED</h4>
|
||||||
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
|
<!-- Medium -->
|
||||||
|
<div class="donut thin donut-primary" style="--percentage: {{taskPercentage}};">
|
||||||
|
<span class="values">
|
||||||
|
<p style="color:#007bff;">{{totalCompletedTask}}</p> / <p style="color:#ccc;">{{totalPlannedTask}}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
||||||
|
<span class="legend-color legend-blue"></span> Completed Work
|
||||||
|
</div>
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
||||||
|
<span class="legend-color legend-gray"></span> Planned Work
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style="font-size: xx-small;">*Today's total work</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="card">
|
||||||
|
<h4 class="card-title">DAILY TASKS COMPLETED</h4>
|
||||||
|
<p class="value tasks">20 / 30</p>
|
||||||
|
<p>Team member present</p>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><span class="legend-color legend-blue"></span> Completed</div>
|
||||||
|
<div class="legend-item"><span class="legend-color legend-green"></span> In Progress</div>
|
||||||
|
<div class="legend-item"><span class="legend-color legend-gray"></span> Pending</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<a class="card-link" href={{webUrl}} aria-label="Open original website"
|
||||||
|
title="Open website" target="_blank" rel="noopener">
|
||||||
|
<span class="arrow">↗</span>
|
||||||
|
</a>
|
||||||
|
<h4 class="card-title">PROJECT COMPLETION STATUS</h4>
|
||||||
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
|
<!-- Medium -->
|
||||||
|
<div class="donut thin donut-primary" style="--percentage: {{completionStatus}};">
|
||||||
|
<span>
|
||||||
|
<p style="color:#007bff;">{{totalCompletedWork}}</p> / <p style="color:#ccc;">{{totalPlannedWork}}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
||||||
|
<span class="legend-color legend-blue"></span> Completed Work
|
||||||
|
</div>
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
||||||
|
<span class="legend-color legend-gray"></span> Planned Work
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style="font-size: xx-small;">*Project's total work</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<a class="card-link" href={{webUrl}} aria-label="Open original website"
|
||||||
|
title="Open website" target="_blank" rel="noopener">
|
||||||
|
<span class="arrow">↗</span>
|
||||||
|
</a>
|
||||||
|
<h4 class="card-title">Regularization Pending</h4>
|
||||||
|
<p class="value tasks">{{regularizationPending}}</p>
|
||||||
|
|
||||||
|
<h4 class="card-title">Checkout Pending</h4>
|
||||||
|
<p class="value tasks">{{checkoutPending}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<a class="card-link" href={{webUrl}} aria-label="Open original website"
|
||||||
|
title="Open website" target="_blank" rel="noopener">
|
||||||
|
<span class="arrow">↗</span>
|
||||||
|
</a>
|
||||||
|
<h4 class="card-title">Activity Report Pending</h4>
|
||||||
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
|
<!-- Medium -->
|
||||||
|
<span class="values">
|
||||||
|
<p style="color:#007bff;">{{reportPending}}</p> / <p style="color:#ccc;">{{todaysAssignTasks}}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
||||||
|
<span class="legend-color legend-blue"></span> Total Pending Tasks
|
||||||
|
</div>
|
||||||
|
<div class="legend-item"
|
||||||
|
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
||||||
|
<span class="legend-color legend-gray"></span> Today's Assigned Tasks
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if teamOnSite and teamOnSite|length > 0 %}
|
||||||
|
<div class="card">
|
||||||
|
<!-- Row 1: Header -->
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">Team Strength on Site</h4>
|
||||||
|
</div>
|
||||||
|
<table style="width: 100%;">
|
||||||
|
{% for a in teamOnSite %}
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">{{a.roleName}}</td>
|
||||||
|
<td style="text-align: right;">{{a.numberofEmployees}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Activities
|
||||||
|
{% if performedTasks and performedTasks|length > 0 %}
|
||||||
|
<div class="activities">
|
||||||
|
<h2>Activities (Tasks) Performed {{date}}</h2>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Activity/Location</th>
|
||||||
|
<th>Assigned Today/Pending</th>
|
||||||
|
<th>Completed Today</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Team Members</th>
|
||||||
|
<th>Comment</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in performedTasks %}
|
||||||
|
<tr>
|
||||||
|
<td>{{a.activity}} {{a.location}}</td>
|
||||||
|
<td>{{a.assignedToday}} / {{a.pending}}</td>
|
||||||
|
<td>{{a.completedToday}}</td>
|
||||||
|
<td>{{date}}</td>
|
||||||
|
<td></td>
|
||||||
|
<td>{{a.comment}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %} -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="footer" style="display:flex; flex-wrap:wrap;">
|
||||||
|
<div style="width: 50%;text-align: left;">
|
||||||
|
Contact Us: contact[at]marcoaiot.com<br>
|
||||||
|
Marco AIoT technologies Pvt. Ltd. ©2025 All Rights Reserved
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width: 50%; text-align: right;">
|
||||||
|
<!-- <a href="#">Instagram</a> | -->
|
||||||
|
<a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/google.png" style="height: 15px;" /></a> |
|
||||||
|
|
||||||
|
<a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/twitter.png" style="height: 15px;" /></a> |
|
||||||
|
|
||||||
|
<a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/facebook.png" style="height: 15px;" /></a> |
|
||||||
|
<a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/instagram.png" style="height: 15px;" /></a>
|
||||||
|
<!-- <a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/youtube.png" style="height: 15px;" /></a> | <a
|
||||||
|
href="#">LinkedIn</a> |
|
||||||
|
<a href="#">YouTube</a> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;width: 100%;background-color: #fff;margin:10px;font-size: small;color: #6c757d ;">
|
||||||
|
You have received this email because it contains important information about your Marco PMS Account account.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
324
mailling/monthly_attendance_report.py
Normal file
324
mailling/monthly_attendance_report.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import json
|
||||||
|
import smtplib
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
from email.message import EmailMessage
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||||
|
|
||||||
|
|
||||||
|
# --- Config and color map ---
|
||||||
|
color_map = {
|
||||||
|
"all_ok": "4CAF50", #Green present
|
||||||
|
"act4_true": "009688", # teal Regularization is accepted
|
||||||
|
"act1_nullOut": "FFC107", #Amber Check out pending
|
||||||
|
"act2": "2196F3", # Bule Regularization pending
|
||||||
|
"act5": "B71C1C", #Dark Red Regularization rejected
|
||||||
|
"all_null": "F6635C", #Red Absent
|
||||||
|
"sunday":"FF0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add Legend sheet to the workbook
|
||||||
|
legend_data = {
|
||||||
|
"Color Description": [
|
||||||
|
"Present (All Ok)",
|
||||||
|
"Regularization Accepted",
|
||||||
|
"Check-out Pending",
|
||||||
|
"Regularization Pending",
|
||||||
|
"Regularization Rejected",
|
||||||
|
"Absent",
|
||||||
|
"Sundays"
|
||||||
|
],
|
||||||
|
"Hex Color": [
|
||||||
|
color_map["all_ok"],
|
||||||
|
color_map["act4_true"],
|
||||||
|
color_map["act1_nullOut"],
|
||||||
|
color_map["act2"],
|
||||||
|
color_map["act5"],
|
||||||
|
color_map["all_null"],
|
||||||
|
color_map["sunday"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def login_api():
|
||||||
|
payload = {"username": API_USERNAME, "password": API_PASSWORD}
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/auth/login", json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()["data"]
|
||||||
|
jwt = data["token"]
|
||||||
|
print("API login successful.")
|
||||||
|
return jwt
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Login API error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def select_tenant(jwt):
|
||||||
|
headers = {"Authorization": f"Bearer {jwt}", "Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/auth/select-tenant/{API_TENANT}", headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()["data"]
|
||||||
|
jwt = data["token"]
|
||||||
|
print("Tenant selected successful.")
|
||||||
|
return jwt
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Select tenant error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def attendance_report(jwt,is_current_month):
|
||||||
|
headers = {"Authorization": f"Bearer {jwt}", "Content-Type": "application/json"}
|
||||||
|
response = requests.get(f"{BASE_URL}/report/report-attendance?isCurrentMonth={is_current_month}", headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
projects = data.get('data', [])
|
||||||
|
|
||||||
|
# Get current date and time
|
||||||
|
today = datetime.datetime.now()
|
||||||
|
|
||||||
|
if not is_current_month:
|
||||||
|
first_day_current_month = today.replace(day=1)
|
||||||
|
last_month_last_day = first_day_current_month - datetime.timedelta(days=1)
|
||||||
|
month_name = last_month_last_day.strftime("%B")
|
||||||
|
year = last_month_last_day.year
|
||||||
|
else:
|
||||||
|
month_name = today.strftime("%B")
|
||||||
|
year = today.year
|
||||||
|
|
||||||
|
excel_file = f"{month_name}-{year}_attendance_report.xlsx"
|
||||||
|
writer = pd.ExcelWriter(excel_file, engine="openpyxl")
|
||||||
|
|
||||||
|
# Border style
|
||||||
|
thin = Side(border_style="thin", color="000000")
|
||||||
|
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||||
|
|
||||||
|
for proj_idx, project in enumerate(projects):
|
||||||
|
project_name = project.get('projectName', 'UnknownProject')[:31] # Sheet name max 31 chars
|
||||||
|
attendances = project.get('projectAttendance', [])
|
||||||
|
|
||||||
|
# Collect all unique dates in this project
|
||||||
|
all_dates = set()
|
||||||
|
for user in attendances:
|
||||||
|
for att in user.get('attendances', []):
|
||||||
|
all_dates.add(att.get('attendanceDate', '').split('T')[0])
|
||||||
|
all_dates = sorted(all_dates)
|
||||||
|
|
||||||
|
# Prepare rows
|
||||||
|
rows = []
|
||||||
|
for user in attendances:
|
||||||
|
row = {'Name': f"{user.get('firstName','')} {user.get('lastName','')}".strip()}
|
||||||
|
date_map = {a.get('attendanceDate', '').split('T')[0]: a for a in user.get('attendances', [])}
|
||||||
|
for date in all_dates:
|
||||||
|
att = date_map.get(date, {})
|
||||||
|
# Store also needed fields for coloring below
|
||||||
|
row[f"{date}_checkin"] = ''
|
||||||
|
row[f"{date}_checkout"] = ''
|
||||||
|
|
||||||
|
if att.get('checkIn'):
|
||||||
|
# Extract time part only, e.g. "09:30:00"
|
||||||
|
row[f"{date}_checkin"] = att.get('checkIn').split('T')[1]
|
||||||
|
|
||||||
|
if att.get('checkOut'):
|
||||||
|
# Extract time part only
|
||||||
|
row[f"{date}_checkout"] = att.get('checkOut').split('T')[1]
|
||||||
|
row[f"{date}_activity"] = att.get('activity', None)
|
||||||
|
row[f"{date}_isapproved"] = att.get('isApproved', None)
|
||||||
|
row['CheckInCheckOutDone'] = user.get('checkInCheckOutDone', 0)
|
||||||
|
row['CheckOutPending'] = user.get('checkOutPending', 0)
|
||||||
|
row['CheckInDone'] = user.get('checkInDone', 0)
|
||||||
|
row['AbsentAttendance'] = user.get('absentAttendance', 0)
|
||||||
|
row['RejectedRegularize'] = user.get('rejectedRegularize', 0)
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
legend_df = pd.DataFrame(legend_data)
|
||||||
|
|
||||||
|
# Use the same writer to add sheet after all sheets written
|
||||||
|
legend_df.to_excel(writer, sheet_name="Legend", index=False)
|
||||||
|
|
||||||
|
# Access Legend sheet for coloring the color sample cells
|
||||||
|
ws_legend = writer.book["Legend"]
|
||||||
|
|
||||||
|
# Assuming data starts from row 2 (header in row 1)
|
||||||
|
for row_idx in range(2, 2 + len(legend_data["Color Description"])):
|
||||||
|
# Color sample cell: B column (2nd col)
|
||||||
|
cell = ws_legend.cell(row=row_idx, column=2)
|
||||||
|
hex_color = cell.value
|
||||||
|
fill = PatternFill(start_color=hex_color, fill_type="solid")
|
||||||
|
cell.fill = fill
|
||||||
|
|
||||||
|
# Optional: bold and center description column
|
||||||
|
desc_cell = ws_legend.cell(row=row_idx, column=1)
|
||||||
|
desc_cell.font = Font(bold=True)
|
||||||
|
desc_cell.alignment = Alignment(horizontal="left", vertical="center")
|
||||||
|
|
||||||
|
# Optionally adjust headers and column width for visual clarity
|
||||||
|
for col in [1, 2]:
|
||||||
|
ws_legend.column_dimensions[get_column_letter(col)].width = 30
|
||||||
|
|
||||||
|
ws_legend["A1"].font = Font(bold=True)
|
||||||
|
ws_legend["B1"].font = Font(bold=True)
|
||||||
|
ws_legend["A1"].alignment = ws_legend["B1"].alignment = Alignment(horizontal="center")
|
||||||
|
|
||||||
|
# Columns: Name, checkin/checkout pairs for dates, and summary columns
|
||||||
|
columns = ['Name']
|
||||||
|
for date in all_dates:
|
||||||
|
columns.extend([f"{date}_checkin", f"{date}_checkout"])
|
||||||
|
columns.extend(['CheckInCheckOutDone', 'CheckInDone','CheckOutPending', 'AbsentAttendance', 'RejectedRegularize'])
|
||||||
|
|
||||||
|
df = pd.DataFrame(rows, columns=columns)
|
||||||
|
|
||||||
|
# Write to sheet
|
||||||
|
df.to_excel(writer, sheet_name=project_name, index=False, startrow=2)
|
||||||
|
|
||||||
|
# Post-process sheet using openpyxl
|
||||||
|
wb = writer.book
|
||||||
|
ws = wb[project_name]
|
||||||
|
|
||||||
|
# Merged header with date and subheaders
|
||||||
|
ws["A3"].value = "Name"
|
||||||
|
ws["A3"].font = Font(bold=True)
|
||||||
|
ws["A3"].alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
|
||||||
|
col = 2
|
||||||
|
for date in all_dates:
|
||||||
|
col_letter_1 = get_column_letter(col)
|
||||||
|
col_letter_2 = get_column_letter(col + 1)
|
||||||
|
ws.merge_cells(f"{col_letter_1}2:{col_letter_2}2")
|
||||||
|
ws[f"{col_letter_1}2"].value = pd.to_datetime(date).strftime("%d-%m-%Y")
|
||||||
|
ws[f"{col_letter_1}2"].font = Font(bold=True)
|
||||||
|
ws[f"{col_letter_1}2"].alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
ws[f"{col_letter_1}3"].value = "Check-in"
|
||||||
|
ws[f"{col_letter_2}3"].value = "Check-out"
|
||||||
|
ws[f"{col_letter_1}3"].alignment = ws[f"{col_letter_2}3"].alignment = Alignment(horizontal="center")
|
||||||
|
col += 2
|
||||||
|
|
||||||
|
# Add headers for summary columns
|
||||||
|
summary_headers = ['CheckIn-CheckOut Done', 'CheckIn Done', 'CheckOut Pending', 'Absent Attendance', 'Rejected Regularize']
|
||||||
|
summary_start_col = col
|
||||||
|
for i, header in enumerate(summary_headers):
|
||||||
|
col_letter = get_column_letter(summary_start_col + i)
|
||||||
|
ws.merge_cells(f"{col_letter}2:{col_letter}3")
|
||||||
|
ws[f"{col_letter}2"].value = header
|
||||||
|
ws[f"{col_letter}2"].font = Font(bold=True)
|
||||||
|
ws[f"{col_letter}2"].alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
|
||||||
|
# Apply borders and coloring
|
||||||
|
max_row = ws.max_row
|
||||||
|
max_col = ws.max_column
|
||||||
|
|
||||||
|
for row_idx in range(3, max_row + 1): # Starting from 3 to include header rows
|
||||||
|
for col_idx in range(1, max_col + 1):
|
||||||
|
cell = ws.cell(row=row_idx, column=col_idx)
|
||||||
|
cell.border = border
|
||||||
|
|
||||||
|
# Coloring according to rules for check-in/out pairs
|
||||||
|
for row_idx, user_row in enumerate(rows, start=4):
|
||||||
|
for date_idx, date in enumerate(all_dates):
|
||||||
|
checkin_col = 2 + date_idx * 2
|
||||||
|
checkout_col = checkin_col + 1
|
||||||
|
|
||||||
|
activity = user_row.get(f"{date}_activity")
|
||||||
|
isapproved = user_row.get(f"{date}_isapproved")
|
||||||
|
c_in = user_row.get(f"{date}_checkin")
|
||||||
|
c_out = user_row.get(f"{date}_checkout")
|
||||||
|
|
||||||
|
# --- Sunday RED coloring ---
|
||||||
|
dt = datetime.datetime.strptime(date, "%Y-%m-%d")
|
||||||
|
if dt.weekday() == 6: # Sunday
|
||||||
|
fill = PatternFill(start_color="FF0000", fill_type="solid")
|
||||||
|
ws.cell(row=row_idx, column=checkin_col).fill = fill
|
||||||
|
ws.cell(row=row_idx, column=checkout_col).fill = fill
|
||||||
|
continue # Skip further coloring for this date
|
||||||
|
|
||||||
|
fill_color = None
|
||||||
|
if not c_in and not c_out:
|
||||||
|
fill_color = color_map["all_null"]
|
||||||
|
elif c_in and c_out and activity == 4 and not bool(isapproved):
|
||||||
|
fill_color = color_map["all_ok"]
|
||||||
|
elif activity == 4 and bool(isapproved):
|
||||||
|
fill_color = color_map["act4_true"]
|
||||||
|
elif activity == 1 and not c_out:
|
||||||
|
fill_color = color_map["act1_nullOut"]
|
||||||
|
elif activity == 2:
|
||||||
|
fill_color = color_map["act2"]
|
||||||
|
elif activity == 5:
|
||||||
|
fill_color = color_map["act5"]
|
||||||
|
|
||||||
|
if fill_color:
|
||||||
|
fill = PatternFill(start_color=fill_color, fill_type="solid")
|
||||||
|
ws.cell(row=row_idx, column=checkin_col).fill = fill
|
||||||
|
ws.cell(row=row_idx, column=checkout_col).fill = fill
|
||||||
|
|
||||||
|
writer.close()
|
||||||
|
print(f"Excel '{excel_file}' generated with sheets per project, summary columns and conditional coloring.")
|
||||||
|
|
||||||
|
# --- 2. Compose the Email ----
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg["Subject"] = f"Attendance Report for {month_name}-{year}"
|
||||||
|
msg["From"] = SENDER_EMAIL
|
||||||
|
msg["To"] = RECIPIENT_EMAILS
|
||||||
|
msg.set_content("Please find the attached Excel file.")
|
||||||
|
|
||||||
|
# Add the Excel attachment
|
||||||
|
with open(excel_file, "rb") as f:
|
||||||
|
file_data = f.read()
|
||||||
|
file_name = f.name
|
||||||
|
msg.add_attachment(file_data, maintype="application", subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=file_name)
|
||||||
|
|
||||||
|
# --- 3. Send the Email ----
|
||||||
|
with smtplib.SMTP(SMPTSERVER, PORT) as smtp:
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.starttls()
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(SENDER_EMAIL, SENDER_PASSWORD)
|
||||||
|
smtp.send_message(msg)
|
||||||
|
|
||||||
|
# After all operations (e.g., emailing) are done, delete the file
|
||||||
|
if os.path.exists(excel_file):
|
||||||
|
os.remove(excel_file)
|
||||||
|
print(f"Deleted file: {excel_file}")
|
||||||
|
else:
|
||||||
|
print("File not found, cannot delete.")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main execution logic ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Load your real config before calling any API
|
||||||
|
GLOBAL_CONFIG_PATH = "config.json"
|
||||||
|
try:
|
||||||
|
with open(GLOBAL_CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load config: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
API_CONFIG = config.get("API", {})
|
||||||
|
BASE_URL = API_CONFIG.get("BASE_URL")
|
||||||
|
API_USERNAME = API_CONFIG.get("USERNAME")
|
||||||
|
API_PASSWORD = API_CONFIG.get("PASSWORD")
|
||||||
|
API_TENANT = API_CONFIG.get("TENANTID")
|
||||||
|
|
||||||
|
SMPT_CONFIG = config.get("SMPT", {})
|
||||||
|
SMPTSERVER = SMPT_CONFIG.get("SMPTSERVER")
|
||||||
|
PORT = SMPT_CONFIG.get("PORT")
|
||||||
|
SENDER_EMAIL = SMPT_CONFIG.get("SENDER_EMAIL")
|
||||||
|
SENDER_PASSWORD = SMPT_CONFIG.get("SENDER_PASSWORD")
|
||||||
|
RECIPIENT_EMAILS = SMPT_CONFIG.get("RECIPIENT_EMAILS")
|
||||||
|
|
||||||
|
token = login_api()
|
||||||
|
if not token:
|
||||||
|
print("Login failed, aborting.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
jwt_token = select_tenant(token)
|
||||||
|
if not jwt_token:
|
||||||
|
print("Tenant selection failed, aborting.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
attendance_report(jwt_token, False)
|
119
mailling/project_statistics_report.py
Normal file
119
mailling/project_statistics_report.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import smtplib
|
||||||
|
import datetime
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
from numbers import Real
|
||||||
|
from email.message import EmailMessage
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
|
# Load your real config before calling any API
|
||||||
|
GLOBAL_CONFIG_PATH = "config.json"
|
||||||
|
try:
|
||||||
|
with open(GLOBAL_CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load config: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
WEB_CONFIG = config.get("WEB", {})
|
||||||
|
WEB_BASE_URL = WEB_CONFIG.get("BASE_URL")
|
||||||
|
|
||||||
|
API_CONFIG = config.get("API", {})
|
||||||
|
BASE_URL = API_CONFIG.get("BASE_URL")
|
||||||
|
|
||||||
|
SMPT_CONFIG = config.get("SMPT", {})
|
||||||
|
SMPTSERVER = SMPT_CONFIG.get("SMPTSERVER")
|
||||||
|
PORT = SMPT_CONFIG.get("PORT")
|
||||||
|
SENDER_EMAIL = SMPT_CONFIG.get("SENDER_EMAIL")
|
||||||
|
SENDER_PASSWORD = SMPT_CONFIG.get("SENDER_PASSWORD")
|
||||||
|
RECIPIENT_EMAILS = SMPT_CONFIG.get("RECIPIENT_EMAILS")
|
||||||
|
|
||||||
|
UNIQUE_IDENTIFIER_CONFIG = config.get("UNIQUE_IDENTIFIER", {})
|
||||||
|
PROJECT_IDS = UNIQUE_IDENTIFIER_CONFIG.get("PROJECT_IDS")
|
||||||
|
|
||||||
|
|
||||||
|
def render_template_from_file(template_name,context):
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(searchpath=str(base_dir)),
|
||||||
|
autoescape=select_autoescape(["html", "xml"])
|
||||||
|
)
|
||||||
|
tmpl = env.get_template(template_name)
|
||||||
|
return tmpl.render(**context)
|
||||||
|
|
||||||
|
def fetch_Project_report(project_id):
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/market/get/project/report/{project_id}", headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()["data"]
|
||||||
|
print("Project report fetched successfully.")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Select tenant error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_percentage(part, whole, decimals: int = 2):
|
||||||
|
if not isinstance(part, Real) or not isinstance(whole, Real):
|
||||||
|
raise TypeError("part and whole must be numbers")
|
||||||
|
if whole == 0:
|
||||||
|
return 0.0
|
||||||
|
return round((part / whole) * 100.0, decimals)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
template_name = "dpr.html"
|
||||||
|
project_ids = [p.strip() for p in PROJECT_IDS.split(",") if p.strip()]
|
||||||
|
|
||||||
|
for project_id in project_ids:
|
||||||
|
|
||||||
|
data = fetch_Project_report(project_id)
|
||||||
|
attendance_percentage = get_percentage(data["todaysAttendances"], data["totalEmployees"], 2)
|
||||||
|
task_percentage = get_percentage(data["totalCompletedTask"], data["totalPlannedTask"], 2)
|
||||||
|
web_url = f"{WEB_BASE_URL}/auth/login"
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"webUrl":web_url,
|
||||||
|
"date": data["date"],
|
||||||
|
"projectName": data["projectName"],
|
||||||
|
"timeStamp": data["timeStamp"],
|
||||||
|
"todaysAttendances": data["todaysAttendances"],
|
||||||
|
"totalEmployees": data["totalEmployees"],
|
||||||
|
"attendancePercentage":data["attendancePercentage"],
|
||||||
|
"taskPercentage":data["taskPercentage"],
|
||||||
|
"regularizationPending": data["regularizationPending"],
|
||||||
|
"checkoutPending": data["checkoutPending"],
|
||||||
|
"totalPlannedWork": data["totalPlannedWork"],
|
||||||
|
"totalCompletedWork": data["totalCompletedWork"],
|
||||||
|
"totalPlannedTask": data["totalPlannedTask"],
|
||||||
|
"totalCompletedTask": data["totalCompletedTask"],
|
||||||
|
"completionStatus": data["completionStatus"],
|
||||||
|
"reportPending": data["reportPending"],
|
||||||
|
"todaysAssignTasks": data["todaysAssignTasks"],
|
||||||
|
"teamOnSite": data["teamOnSite"],
|
||||||
|
"performedTasks": data["performedTasks"],
|
||||||
|
"performedAttendance": data["performedAttendance"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# print(context)
|
||||||
|
project_name = data["projectName"]
|
||||||
|
html = render_template_from_file(template_name,context)
|
||||||
|
|
||||||
|
# print(html)
|
||||||
|
today = datetime.datetime.now()
|
||||||
|
formatted = today.strftime("%d-%b-%Y")
|
||||||
|
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg["Subject"] = f"DPR - {formatted} - {project_name}"
|
||||||
|
msg["From"] = SENDER_EMAIL
|
||||||
|
msg["To"] = RECIPIENT_EMAILS
|
||||||
|
msg.set_content("HTML version attached as alternative.")
|
||||||
|
msg.add_alternative(html, subtype="html")
|
||||||
|
|
||||||
|
with smtplib.SMTP(SMPTSERVER, PORT) as smtp:
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.starttls()
|
||||||
|
smtp.ehlo()
|
||||||
|
smtp.login(SENDER_EMAIL, SENDER_PASSWORD)
|
||||||
|
smtp.send_message(msg)
|
Loading…
x
Reference in New Issue
Block a user