Compare commits
2 Commits
main
...
python-ema
Author | SHA1 | Date | |
---|---|---|---|
023691d062 | |||
3c379a9bf2 |
@ -3,24 +3,17 @@
|
|||||||
"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,amol@marcosolutions.co.in,vinod@marcofire.in,umesh@marcoaiot.com"
|
|
||||||
},
|
},
|
||||||
"API": {
|
"API": {
|
||||||
"BASE_URL": "https://stageapi.marcoaiot.com/api",
|
"BASE_URL": "http://localhost:5032/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,08dda31f-25c6-4ad7-8252-14a64ba96fce,08dda508-e7df-4e90-86a5-4c2d30e32069,08dda7db-1f08-4db4-863a-c494201a1156,08dda8cd-0522-47d8-82ef-407276490b68,08dda8cd-8dc2-4e0a-8638-f16f63e16afe,2618f2ef-2823-11f0-9d9e-bc241163f504"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
157
mailling/mail-templates/charts.html
Normal file
157
mailling/mail-templates/charts.html
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CSS Donut Variants</title>
|
||||||
|
<style>
|
||||||
|
.donut {
|
||||||
|
--percentage: 65; /* Change this per chart */
|
||||||
|
--primary: #e63946; /* 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(--primary) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0.51rem; /* Default height */
|
||||||
|
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 0.375rem; /* Default size */
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress.thin {
|
||||||
|
height: 0.7rem;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
.progress.medium {
|
||||||
|
height: 1rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress.large {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
background-color: #0d6efd; /* default = Bootstrap primary */
|
||||||
|
transition: width 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color variants */
|
||||||
|
.progress-bar-success {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.progress-bar-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.progress-bar-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="display:block; gap:40px; align-items:center; justify-content:center; min-height:100vh;">
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
">
|
||||||
|
<!-- Thin -->
|
||||||
|
<div class="donut thin" style="--percentage: 45;">
|
||||||
|
<span>45%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Medium -->
|
||||||
|
<div class="donut medium" style="--percentage: 73;">
|
||||||
|
<span>73%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Large -->
|
||||||
|
<div class="donut large" style="--percentage: 90;">
|
||||||
|
<span>90%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
margin-top: 100px;
|
||||||
|
">
|
||||||
|
<div class="progress" style="width:200px">
|
||||||
|
<div class="progress-bar" style="width: 25%;">25%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress thin " style="width:200px">
|
||||||
|
<div class="progress-bar progress-bar-success" style="width: 50%;">50%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress medium" style="width:200px">
|
||||||
|
<div class="progress-bar progress-bar-warning" style="width: 75%;">75%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress large" style="width:50%">
|
||||||
|
<div class="progress-bar progress-bar-danger" style="width: 90%;">90%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: #49bf3c;
|
background: #b10000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -57,8 +57,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@ -69,58 +67,10 @@
|
|||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
/* <-- added shadow */
|
/* <-- added shadow */
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
border-top: 1px solid #49bf3c;
|
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;
|
|
||||||
/* arrow color */
|
|
||||||
background: #f5f5f5;
|
|
||||||
/* badge background */
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-link-success {
|
|
||||||
color: #49bf3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-link-warning {
|
|
||||||
color: #ffc107;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-link-primary {
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
.card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
@ -148,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.attendance {
|
.attendance {
|
||||||
color: #49bf3c;
|
color: #b10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tasks {
|
.tasks {
|
||||||
@ -186,8 +136,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
/* background: #b10000; */
|
background: #b10000;
|
||||||
background: #49bf3c;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@ -220,7 +169,7 @@
|
|||||||
.legend {
|
.legend {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -241,7 +190,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.legend-red {
|
.legend-red {
|
||||||
background: #e63946;
|
background: #b10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-blue {
|
.legend-blue {
|
||||||
@ -256,23 +205,19 @@
|
|||||||
background: #ccc;
|
background: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-yellow {
|
|
||||||
background: #ffc107;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.donut {
|
.donut {
|
||||||
--percentage: 0;
|
--percentage: 65;
|
||||||
/* Change this per chart */
|
/* Change this per chart */
|
||||||
--danger: #e63946;
|
--danger: #e63946;
|
||||||
--primary: #007bff;
|
--primary: #007bff;
|
||||||
--warning: #ffc107;
|
--warning: #ffc107;
|
||||||
--success: #28a745;
|
--success: #198754;
|
||||||
/* Fill color */
|
/* Fill color */
|
||||||
--track: #e9ecef;
|
--track: #e9ecef;
|
||||||
/* Background track */
|
/* Background track */
|
||||||
--size: 200px;
|
--size: 120px;
|
||||||
/* Default size */
|
/* Default size */
|
||||||
--thickness: 20px;
|
--thickness: 20px;
|
||||||
/* Default thickness */
|
/* Default thickness */
|
||||||
@ -280,9 +225,8 @@
|
|||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
/* background: conic-gradient(var(--danger) calc(var(--percentage) * 1%),
|
background: conic-gradient(var(--danger) calc(var(--percentage) * 1%),
|
||||||
var(--track) 0); */
|
var(--track) 0);
|
||||||
background: conic-gradient(var(--track) 100%);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -305,13 +249,6 @@
|
|||||||
.donut span {
|
.donut span {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: calc(var(--size) / 6);
|
font-size: calc(var(--size) / 6);
|
||||||
width:80%;
|
|
||||||
box-sizing:border-box;
|
|
||||||
display:flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content:center;
|
|
||||||
align-items:center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Variants */
|
/* Variants */
|
||||||
@ -322,7 +259,7 @@
|
|||||||
|
|
||||||
.donut.medium {
|
.donut.medium {
|
||||||
--size: 120px;
|
--size: 120px;
|
||||||
--thickness: 15px;
|
--thickness: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut.large {
|
.donut.large {
|
||||||
@ -334,35 +271,25 @@
|
|||||||
.donut-success {
|
.donut-success {
|
||||||
background: conic-gradient(var(--success) calc(var(--percentage) * 1%),
|
background: conic-gradient(var(--success) calc(var(--percentage) * 1%),
|
||||||
var(--track) 0);
|
var(--track) 0);
|
||||||
|
color: var(--success)
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut-warning {
|
.donut-warning {
|
||||||
background: conic-gradient(var(--warning) calc(var(--percentage) * 1%),
|
background: conic-gradient(var(--warning) calc(var(--percentage) * 1%),
|
||||||
var(--track) 0);
|
var(--track) 0);
|
||||||
|
color: var(--warning)
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut-danger {
|
.donut-danger {
|
||||||
background: conic-gradient(var(--danger) calc(var(--percentage) * 1%),
|
background: conic-gradient(var(--danger) calc(var(--percentage) * 1%),
|
||||||
var(--track) 0);
|
var(--track) 0);
|
||||||
|
color: var(--danger)
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut-primary {
|
.donut-primary {
|
||||||
background: conic-gradient(var(--primary) calc(var(--percentage) * 1%),
|
background: conic-gradient(var(--primary) calc(var(--percentage) * 1%),
|
||||||
var(--track) 0);
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -373,246 +300,298 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Daily Progress Report</h1>
|
<h1>Daily Progress Report</h1>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<strong>Project:</strong> {{projectName}}<br>
|
<strong>Project:</strong> ANP ultimas wakad<br>
|
||||||
<strong>Date:</strong> {{date}}
|
<strong>Date:</strong> 17 September 2025
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Note -->
|
<!-- Status Note -->
|
||||||
<div class="status-note">
|
<div class="status-note">
|
||||||
* Project Status Reported - Generated at {{timeStamp}} UTC
|
* Project Status Reported - Generated at 18-Sep-2025 03:30:03 UTC
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Cards -->
|
<!-- Status Cards -->
|
||||||
<div class="status-cards">
|
<div class="status-cards">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a class="card-link card-link-warning" 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>
|
<h4 class="card-title">TODAY'S ATTENDANCE</h4>
|
||||||
<div style="display:flex; flex-wrap:wrap;">
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
<!-- Left Column -->
|
<!-- Left Column -->
|
||||||
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
<!-- Medium -->
|
<!-- Medium -->
|
||||||
<div class="donut thin donut-warning" style="--percentage: {{attendancePercentage}};">
|
<div class="donut thin" style="--percentage: 66;">
|
||||||
<span>
|
<span>20 / 30</span>
|
||||||
<p style="color:#ffc107;">{{todaysAttendances}}</p><p>/</p><p style="color:#ccc;">{{totalEmployees}}</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
<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-green"></span> Completed
|
||||||
|
</div>
|
||||||
<div class="legend-item"
|
<div class="legend-item"
|
||||||
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
||||||
<span class="legend-color legend-yellow"></span> Today's Attendance
|
<span class="legend-color legend-blue"></span> In Progress
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item"
|
<div class="legend-item"
|
||||||
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
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
|
<span class="legend-color legend-gray"></span> Pending
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="padding:10px; text-align:center;">
|
||||||
|
<p class="text-muted">Team members present on the site</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a class="card-link card-link-primary" 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>
|
<h4 class="card-title">DAILY TASKS COMPLETED</h4>
|
||||||
<div style="display:flex; flex-wrap:wrap;">
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
<!-- Left Column -->
|
<!-- Left Column -->
|
||||||
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
<!-- Medium -->
|
<!-- Medium -->
|
||||||
<div class="donut thin donut-primary" style="--percentage: {{taskPercentage}};">
|
<div class="donut thin donut-primary" style="--percentage: 66;">
|
||||||
<span >
|
<span>20 / 30</span>
|
||||||
<p style="color:#007bff;">{{totalCompletedTask}}</p><p>/</p><p style="color:#ccc;">{{totalPlannedTask}}</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
<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-green"></span> Completed
|
||||||
|
</div>
|
||||||
<div class="legend-item"
|
<div class="legend-item"
|
||||||
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
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
|
<span class="legend-color legend-blue"></span> In Progress
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item"
|
<div class="legend-item"
|
||||||
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
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
|
<span class="legend-color legend-gray"></span> Pending
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="padding:10px; text-align:center;">
|
||||||
|
<p class="text-muted">Team members present on the site</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: end!important;">
|
</div>
|
||||||
<p style="font-size: xx-small;color: #ccc;">*Today's Total Work</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a class="card-link card-link-success" 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>
|
<h4 class="card-title">PROJECT COMPLETION STATUS</h4>
|
||||||
|
<p class=" value completion">20 / 30</p>
|
||||||
|
<p>Team member present</p>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><span class="legend-color legend-green"></span> Completed</div>
|
||||||
|
<div class="legend-item"><span class="legend-color legend-blue"></span> In Progress</div>
|
||||||
|
<div class="legend-item"><span class="legend-color legend-gray"></span> Pending</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h4 class="card-title">Regularization Pending</h4>
|
||||||
|
<p class="value tasks">28/32</p>
|
||||||
|
<p class="text-muted">Regularization Pending</p>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><span class="legend-color legend-green"></span> Completed</div>
|
||||||
|
<div class="legend-item"><span class="legend-color legend-blue"></span> In Progress</div>
|
||||||
|
<div class="legend-item"><span class="legend-color legend-gray"></span> Pending</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<!-- Row 1: Header -->
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">Checkout Pending</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Two Columns -->
|
||||||
<div style="display:flex; flex-wrap:wrap;">
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
|
|
||||||
<!-- Left Column -->
|
<!-- Left Column -->
|
||||||
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
<!-- Medium -->
|
<!-- Medium -->
|
||||||
<div class="donut thin donut-success" style="--percentage: {{completionStatus}};">
|
<div class="donut thin donut-success" style="--percentage: 73;">
|
||||||
<span>
|
<span>73%</span>
|
||||||
<p style="color:#28a745;">{{totalCompletedWork}}</p> <p>/</p> <p style="color:#ccc;">{{totalPlannedWork}}</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="legend" style="width:50%; padding:15px; box-sizing:border-box;">
|
<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-green"></span> Completed
|
||||||
|
</div>
|
||||||
<div class="legend-item"
|
<div class="legend-item"
|
||||||
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
style="margin-bottom:10px;text-align: left; display:left; justify-content:left; align-items:left!important;; ">
|
||||||
<span class="legend-color legend-green"></span> Completed Work
|
<span class="legend-color legend-blue"></span> In Progress
|
||||||
</div>
|
</div>
|
||||||
<div class="legend-item"
|
<div class="legend-item"
|
||||||
style="margin-bottom:10px; text-align: left; display:left; justify-content:left; align-items:left!important;;">
|
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
|
<span class="legend-color legend-gray"></span> Pending
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3: Full Width -->
|
||||||
|
<div>
|
||||||
|
<div style="padding:10px; text-align:center;">
|
||||||
|
<p class="text-muted">Team members present on the site</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: end!important;">
|
|
||||||
<p style="font-size: xx-small;color: #ccc;">*Project's Total Work</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<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>
|
|
||||||
<div>
|
|
||||||
<h4 class="card-title">Pending Attendance</h4>
|
|
||||||
</div>
|
|
||||||
<table style="width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: left;">Regularization Pending</td>
|
|
||||||
<td style="text-align: right;">{{regularizationPending}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: left;">Checkout Pending</td>
|
|
||||||
<td style="text-align: right;">{{checkoutPending}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</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>
|
|
||||||
<div>
|
<div>
|
||||||
<h4 class="card-title">Activity Report Pending</h4>
|
<h4 class="card-title">Activity Report Pending</h4>
|
||||||
</div>
|
</div>
|
||||||
<table style="width: 100%;">
|
<div style="display:flex; flex-wrap:wrap;">
|
||||||
<tr>
|
|
||||||
<td style="text-align: left;">Total Pending Tasks</td>
|
|
||||||
<td style="text-align: right;">{{reportPending}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: left;">Today's Assigned Tasks</td>
|
|
||||||
<td style="text-align: right;">{{todaysAssignTasks}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: left;">Today's Completed Tasks</td>
|
|
||||||
<td style="text-align: right;">{{todaysCompletedTasks}}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if teamOnSite and teamOnSite|length > 0 %}
|
<!-- Left Column -->
|
||||||
|
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||||
|
<!-- Medium -->
|
||||||
|
<div class="donut thin donut-warning" style="--percentage: 73;">
|
||||||
|
<span>73%</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-green"></span> Completed
|
||||||
|
</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-blue"></span> In Progress
|
||||||
|
</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> Pending
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="padding:10px; text-align:center;">
|
||||||
|
<p class="text-muted">Team members present on the site</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<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>
|
|
||||||
<!-- Row 1: Header -->
|
<!-- Row 1: Header -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="card-title">Team Strength on Site</h4>
|
<h4 class="card-title">Team Strength on Site</h4>
|
||||||
</div>
|
</div>
|
||||||
<table style="width: 100%;">
|
<table style="width: 100%;">
|
||||||
{% for a in teamOnSite %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="text-align: left;">{{a.roleName}}</td>
|
<td style="text-align: left;">Site Engineer</td>
|
||||||
<td style="text-align: right;">{{a.numberofEmployees}}</td>
|
<td style="text-align: right;">1</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Weilder</td>
|
||||||
|
<td style="text-align: right;">15</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Helper</td>
|
||||||
|
<td style="text-align: right;">2</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: left;">Painter</td>
|
||||||
|
<td style="text-align: right;">1</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
|
||||||
<!-- Activities
|
<!-- Activities -->
|
||||||
{% if performedTasks and performedTasks|length > 0 %}
|
<div class="activities">
|
||||||
<div class="activities">
|
<h2>Activities (Tasks) Performed 17-Sep-2025</h2>
|
||||||
<h2>Activities (Tasks) Performed {{date}}</h2>
|
<table class="table">
|
||||||
<table class="table">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>NAME</th>
|
||||||
<th>Activity/Location</th>
|
<th>JOB ROLE</th>
|
||||||
<th>Assigned Today/Pending</th>
|
<th>CHECK IN</th>
|
||||||
<th>Completed Today</th>
|
<th>CHECK OUT</th>
|
||||||
<th>Date</th>
|
</tr>
|
||||||
<th>Team Members</th>
|
</thead>
|
||||||
<th>Comment</th>
|
<tbody>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<td>Siddharth Barde</td>
|
||||||
<tbody>
|
<td>Site Engineer</td>
|
||||||
{% for a in performedTasks %}
|
<td>17-Sep-2025 11:47 AM</td>
|
||||||
<tr>
|
<td>-</td>
|
||||||
<td>{{a.activity}} {{a.location}}</td>
|
</tr>
|
||||||
<td>{{a.assignedToday}} / {{a.pending}}</td>
|
<tr>
|
||||||
<td>{{a.completedToday}}</td>
|
<td>Siddharth Barde</td>
|
||||||
<td>{{date}}</td>
|
<td>Site Engineer</td>
|
||||||
<td></td>
|
<td>17-Sep-2025 11:47 AM</td>
|
||||||
<td>{{a.comment}}</td>
|
<td>-</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
<tr>
|
||||||
</tbody>
|
<td>Siddharth Barde</td>
|
||||||
</table>
|
<td>Site Engineer</td>
|
||||||
|
<td>17-Sep-2025 11:47 AM</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</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>
|
||||||
{% endif %} -->
|
|
||||||
|
|
||||||
</div>
|
<div style="width: 50%; text-align: right;">
|
||||||
</div>
|
<!-- <a href="#">Instagram</a> | -->
|
||||||
<!-- Footer -->
|
<a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/google.png" style="height: 15px;" /></a> |
|
||||||
<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="#"><img src="https://cdn.marcoaiot.com/icons/brands/twitter.png" style="height: 15px;" /></a> |
|
||||||
<!-- <a href="#">Instagram</a> | -->
|
|
||||||
<a href="https://www.linkedin.com/company/marco-aiot">Linkedin</a> |
|
|
||||||
|
|
||||||
<a href="https://x.com/marcoaiot"><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/facebook.png" style="height: 15px;" /></a> |
|
<!-- <a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/youtube.png" style="height: 15px;" /></a> | <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> |
|
href="#">LinkedIn</a> |
|
||||||
<a href="#">YouTube</a> -->
|
<a href="#">YouTube</a> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;width: 100%;background-color: #fff;margin:10px;margin-bottom: 30px!important;font-size: small;color: #6c757d ;">
|
<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 {{websiteName}} Account account.
|
You have received this email because it contains important information about your Marco PMS Account account.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
@ -1,324 +0,0 @@
|
|||||||
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)
|
|
@ -1,148 +0,0 @@
|
|||||||
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(f"Project report for project \"{data["projectName"]}\" 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)
|
|
||||||
|
|
||||||
def value_minization(minimize_value):
|
|
||||||
if minimize_value >= 1000:
|
|
||||||
minimized_value = round((minimize_value/1000),2)
|
|
||||||
result = f"{minimized_value}K"
|
|
||||||
else:
|
|
||||||
result = f"{minimize_value}"
|
|
||||||
return result
|
|
||||||
|
|
||||||
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)
|
|
||||||
if "attendancePercentage" in data:
|
|
||||||
attendance_percentage = data["attendancePercentage"]
|
|
||||||
else:
|
|
||||||
attendance_percentage = get_percentage(data["todaysAttendances"], data["totalEmployees"], 2)
|
|
||||||
|
|
||||||
if "taskPercentage" in data:
|
|
||||||
task_percentage = data["taskPercentage"]
|
|
||||||
else:
|
|
||||||
task_percentage = get_percentage(data["totalCompletedTask"], data["totalPlannedTask"], 2)
|
|
||||||
|
|
||||||
if "todaysCompletedTasks" in data:
|
|
||||||
todays_completed_tasks = data["todaysCompletedTasks"]
|
|
||||||
else:
|
|
||||||
todays_completed_tasks = 0
|
|
||||||
|
|
||||||
web_url = f"{WEB_BASE_URL}/auth/login"
|
|
||||||
|
|
||||||
dt = datetime.datetime.strptime(data["date"], "%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
api_formatted_date = dt.strftime("%d-%b-%Y")
|
|
||||||
|
|
||||||
total_planned_work = value_minization(data["totalPlannedWork"])
|
|
||||||
total_completed_work = value_minization(data["totalCompletedWork"])
|
|
||||||
total_planned_task = value_minization(data["totalPlannedTask"])
|
|
||||||
total_completed_task = value_minization(data["totalCompletedTask"])
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"webUrl":web_url,
|
|
||||||
"date": api_formatted_date,
|
|
||||||
"projectName": data["projectName"],
|
|
||||||
"timeStamp": data["timeStamp"],
|
|
||||||
"todaysAttendances": data["todaysAttendances"],
|
|
||||||
"totalEmployees": data["totalEmployees"],
|
|
||||||
"attendancePercentage":attendance_percentage,
|
|
||||||
"taskPercentage":task_percentage,
|
|
||||||
"regularizationPending": data["regularizationPending"],
|
|
||||||
"checkoutPending": data["checkoutPending"],
|
|
||||||
"totalPlannedWork": total_planned_work,
|
|
||||||
"totalCompletedWork": total_completed_work,
|
|
||||||
"totalPlannedTask": total_planned_task,
|
|
||||||
"totalCompletedTask": total_completed_task,
|
|
||||||
"completionStatus": round(data["completionStatus"],2),
|
|
||||||
"reportPending": data["reportPending"],
|
|
||||||
"todaysAssignTasks": data["todaysAssignTasks"],
|
|
||||||
"todaysCompletedTasks": todays_completed_tasks,
|
|
||||||
"websiteName":"OnFieldWork.com",
|
|
||||||
"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)
|
|
||||||
|
|
||||||
msg = EmailMessage()
|
|
||||||
msg["Subject"] = f"DPR - {api_formatted_date} - {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)
|
|
157
mailling/test mail sender/body.txt
Normal file
157
mailling/test mail sender/body.txt
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CSS Donut Variants</title>
|
||||||
|
<style>
|
||||||
|
.donut {
|
||||||
|
--percentage: 65; /* Change this per chart */
|
||||||
|
--primary: #e63946; /* 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(--primary) calc(var(--percentage) * 1%),
|
||||||
|
var(--track) 0
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0.51rem; /* Default height */
|
||||||
|
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 0.375rem; /* Default size */
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress.thin {
|
||||||
|
height: 0.7rem;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
.progress.medium {
|
||||||
|
height: 1rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress.large {
|
||||||
|
height: 1.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
background-color: #0d6efd; /* default = Bootstrap primary */
|
||||||
|
transition: width 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color variants */
|
||||||
|
.progress-bar-success {
|
||||||
|
background-color: #198754;
|
||||||
|
}
|
||||||
|
.progress-bar-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.progress-bar-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="display:block; gap:40px; align-items:center; justify-content:center; min-height:100vh;">
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
">
|
||||||
|
<!-- Thin -->
|
||||||
|
<div class="donut thin" style="--percentage: 45;">
|
||||||
|
<span>45%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Medium -->
|
||||||
|
<div class="donut medium" style="--percentage: 73;">
|
||||||
|
<span>73%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Large -->
|
||||||
|
<div class="donut large" style="--percentage: 90;">
|
||||||
|
<span>90%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="
|
||||||
|
margin-top: 100px;
|
||||||
|
">
|
||||||
|
<div class="progress" style="width:200px">
|
||||||
|
<div class="progress-bar" style="width: 25%;">25%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress thin " style="width:200px">
|
||||||
|
<div class="progress-bar progress-bar-success" style="width: 50%;">50%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress medium" style="width:200px">
|
||||||
|
<div class="progress-bar progress-bar-warning" style="width: 75%;">75%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress large" style="width:50%">
|
||||||
|
<div class="progress-bar progress-bar-danger" style="width: 90%;">90%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
599
python-email-sender/body.txt
Normal file
599
python-email-sender/body.txt
Normal file
File diff suppressed because one or more lines are too long
10
python-email-sender/config.json
Normal file
10
python-email-sender/config.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"EMAIL_CONFIGURATION": {
|
||||||
|
"EMAIL_HOST": "mail.marcoaiot.com",
|
||||||
|
"EMAIL_PORT": 587,
|
||||||
|
"EMAIL_USER": "admin@marcoaiot.com",
|
||||||
|
"EMAIL_PASS": "xxx",
|
||||||
|
"EMAIL_SUBJECT": "Database backup process",
|
||||||
|
"EMAIL_RECEIVERS": "vikas@marcoaiot.com,umesh@marcoaiot.com"
|
||||||
|
}
|
||||||
|
}
|
84
python-email-sender/mail-sender-with-charts.py
Normal file
84
python-email-sender/mail-sender-with-charts.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.image import MIMEImage
|
||||||
|
import os
|
||||||
|
import cairosvg
|
||||||
|
|
||||||
|
def generate_donut_svg(percentage, color="#0d6efd", size=120, thickness=4):
|
||||||
|
"""
|
||||||
|
Generate an inline SVG donut chart.
|
||||||
|
"""
|
||||||
|
svg = f"""
|
||||||
|
<svg width="{size}" height="{size}" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Track -->
|
||||||
|
<path
|
||||||
|
d="M18 2.0845
|
||||||
|
a 15.9155 15.9155 0 0 1 0 31.831
|
||||||
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e9ecef"
|
||||||
|
stroke-width="{thickness}"
|
||||||
|
/>
|
||||||
|
<!-- Progress -->
|
||||||
|
<path
|
||||||
|
d="M18 2.0845
|
||||||
|
a 15.9155 15.9155 0 0 1 0 31.831
|
||||||
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||||
|
fill="none"
|
||||||
|
stroke="{color}"
|
||||||
|
stroke-width="{thickness}"
|
||||||
|
stroke-dasharray="{percentage}, 100"
|
||||||
|
/>
|
||||||
|
<!-- Label -->
|
||||||
|
<text x="18" y="20.35" fill="#333" font-size="5" text-anchor="middle">{percentage}%</text>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
return svg
|
||||||
|
|
||||||
|
# Sender and receiver
|
||||||
|
sender_email = "marcoioitsoft@gmail.com"
|
||||||
|
receiver_emails = ["vikasnale@gmail.com", "vikas@marcoaiot.com", "umeshvdesai@outlook.com"]
|
||||||
|
password = "qrtq wfuj hwpp fhqr" # Use Gmail App Password here
|
||||||
|
|
||||||
|
# Read HTML body from file
|
||||||
|
file_path = os.path.join(os.path.dirname(__file__), "body.txt")
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
body = f.read()
|
||||||
|
|
||||||
|
# Create the email
|
||||||
|
message = MIMEMultipart("related")
|
||||||
|
message["From"] = sender_email
|
||||||
|
message["To"] = ", ".join(receiver_emails)
|
||||||
|
message["Subject"] = "Test HTML Email with Donut Charts"
|
||||||
|
|
||||||
|
# Generate SVGs and convert to PNGs
|
||||||
|
donut_svgs = [
|
||||||
|
generate_donut_svg(45, "#e63946"),
|
||||||
|
generate_donut_svg(73, "#0d6efd"),
|
||||||
|
generate_donut_svg(90, "#198754")
|
||||||
|
]
|
||||||
|
|
||||||
|
# Attach donuts as inline images
|
||||||
|
for i, svg in enumerate(donut_svgs, start=1):
|
||||||
|
png_bytes = cairosvg.svg2png(bytestring=svg.encode("utf-8"))
|
||||||
|
img = MIMEImage(png_bytes, "png")
|
||||||
|
cid = f"donut{i}"
|
||||||
|
img.add_header("Content-ID", f"<{cid}>")
|
||||||
|
message.attach(img)
|
||||||
|
body += f'<br><img src="cid:{cid}" alt="Donut{i}">'
|
||||||
|
|
||||||
|
# Attach the final HTML body
|
||||||
|
message.attach(MIMEText(f"<html><body>{body}</body></html>", "html"))
|
||||||
|
|
||||||
|
# Send the email
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP("smtp.gmail.com", 587)
|
||||||
|
server.starttls()
|
||||||
|
server.login(sender_email, password)
|
||||||
|
server.sendmail(sender_email, receiver_emails, message.as_string())
|
||||||
|
print("✅ Email sent successfully with embedded donut PNGs!")
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Error:", e)
|
||||||
|
finally:
|
||||||
|
server.quit()
|
35
python-email-sender/test-mail-sender-body-file.py
Normal file
35
python-email-sender/test-mail-sender-body-file.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Sender and receiver
|
||||||
|
sender_email = "marcoioitsoft@gmail.com"
|
||||||
|
receiver_emails = ["vikasnale@gmail.com", "vikas@marcoaiot.com"]
|
||||||
|
password = "qrtq wfuj hwpp fhqr" # Use Gmail App Password here
|
||||||
|
|
||||||
|
# Read body from text file (e.g., body.txt in the same folder)
|
||||||
|
file_path = os.path.join(os.path.dirname(__file__), "body.txt")
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
body = f.read()
|
||||||
|
|
||||||
|
# Create the email
|
||||||
|
message = MIMEMultipart()
|
||||||
|
message["From"] = sender_email
|
||||||
|
message["To"] =", ".join(receiver_emails)
|
||||||
|
message["Subject"] = "Test Email from Python"
|
||||||
|
|
||||||
|
# Attach body
|
||||||
|
message.attach(MIMEText(body, "html"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to Gmail SMTP server
|
||||||
|
server = smtplib.SMTP("smtp.gmail.com", 587)
|
||||||
|
server.starttls() # Secure the connection
|
||||||
|
server.login(sender_email, password)
|
||||||
|
server.sendmail(sender_email, receiver_emails, message.as_string())
|
||||||
|
print("✅ Email sent successfully!")
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Error:", e)
|
||||||
|
finally:
|
||||||
|
server.quit()
|
84
python-email-sender/test-mail-sender.py
Normal file
84
python-email-sender/test-mail-sender.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.image import MIMEImage
|
||||||
|
import os
|
||||||
|
import cairosvg
|
||||||
|
|
||||||
|
def generate_donut_svg(percentage, color="#0d6efd", size=120, thickness=4):
|
||||||
|
"""
|
||||||
|
Generate an inline SVG donut chart.
|
||||||
|
"""
|
||||||
|
svg = f"""
|
||||||
|
<svg width="{size}" height="{size}" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Track -->
|
||||||
|
<path
|
||||||
|
d="M18 2.0845
|
||||||
|
a 15.9155 15.9155 0 0 1 0 31.831
|
||||||
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e9ecef"
|
||||||
|
stroke-width="{thickness}"
|
||||||
|
/>
|
||||||
|
<!-- Progress -->
|
||||||
|
<path
|
||||||
|
d="M18 2.0845
|
||||||
|
a 15.9155 15.9155 0 0 1 0 31.831
|
||||||
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||||
|
fill="none"
|
||||||
|
stroke="{color}"
|
||||||
|
stroke-width="{thickness}"
|
||||||
|
stroke-dasharray="{percentage}, 100"
|
||||||
|
/>
|
||||||
|
<!-- Label -->
|
||||||
|
<text x="18" y="20.35" fill="#333" font-size="5" text-anchor="middle">{percentage}%</text>
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
return svg
|
||||||
|
|
||||||
|
# Sender and receiver
|
||||||
|
sender_email = "marcoioitsoft@gmail.com"
|
||||||
|
receiver_emails = ["vikasnale@gmail.com", "vikas@marcoaiot.com", "umeshvdesai@outlook.com"]
|
||||||
|
password = "qrtq wfuj hwpp fhqr" # Use Gmail App Password here
|
||||||
|
|
||||||
|
# Read HTML body from file
|
||||||
|
file_path = os.path.join(os.path.dirname(__file__), "body.txt")
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
body = f.read()
|
||||||
|
|
||||||
|
# Create the email
|
||||||
|
message = MIMEMultipart("related")
|
||||||
|
message["From"] = sender_email
|
||||||
|
message["To"] = ", ".join(receiver_emails)
|
||||||
|
message["Subject"] = "Test HTML Email with Donut Charts"
|
||||||
|
|
||||||
|
# Generate SVGs and convert to PNGs
|
||||||
|
donut_svgs = [
|
||||||
|
generate_donut_svg(45, "#e63946"),
|
||||||
|
generate_donut_svg(73, "#0d6efd"),
|
||||||
|
generate_donut_svg(90, "#198754")
|
||||||
|
]
|
||||||
|
|
||||||
|
# Attach donuts as inline images
|
||||||
|
for i, svg in enumerate(donut_svgs, start=1):
|
||||||
|
png_bytes = cairosvg.svg2png(bytestring=svg.encode("utf-8"))
|
||||||
|
img = MIMEImage(png_bytes, "png")
|
||||||
|
cid = f"donut{i}"
|
||||||
|
img.add_header("Content-ID", f"<{cid}>")
|
||||||
|
message.attach(img)
|
||||||
|
body += f'<br><img src="cid:{cid}" alt="Donut{i}">'
|
||||||
|
|
||||||
|
# Attach the final HTML body
|
||||||
|
message.attach(MIMEText(f"<html><body>{body}</body></html>", "html"))
|
||||||
|
|
||||||
|
# Send the email
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP("smtp.gmail.com", 587)
|
||||||
|
server.starttls()
|
||||||
|
server.login(sender_email, password)
|
||||||
|
server.sendmail(sender_email, receiver_emails, message.as_string())
|
||||||
|
print("✅ Email sent successfully with embedded donut PNGs!")
|
||||||
|
except Exception as e:
|
||||||
|
print("❌ Error:", e)
|
||||||
|
finally:
|
||||||
|
server.quit()
|
11
python-process-monitor/.env
Normal file
11
python-process-monitor/.env
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Services & Containers
|
||||||
|
SERVICES=nginx,rocketchat,mongod,mysql
|
||||||
|
CONTAINERS=redmine-app,mediawiki-app,sonarqube,postgres-sonar,ecc269bb3ba3
|
||||||
|
|
||||||
|
# Mail settings
|
||||||
|
SENDER_EMAIL=marcoioitsoft@gmail.com
|
||||||
|
RECEIVER_EMAIL=umesh@marcoaiot.com,vikas@marcoaiot.com
|
||||||
|
SMTP_SERVER=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=marcoioitsoft@gmail.com
|
||||||
|
SMTP_PASSWORD=qrtq wfuj hwpp fhqr
|
114
python-process-monitor/python-process-monitor.py
Normal file
114
python-process-monitor/python-process-monitor.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import smtplib
|
||||||
|
import os
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# ----------------- CONFIG -----------------
|
||||||
|
services = os.getenv("SERVICES", "").split(",")
|
||||||
|
containers = os.getenv("CONTAINERS", "").split(",") # update with your container names
|
||||||
|
|
||||||
|
sender_email = os.getenv("SENDER_EMAIL", "")
|
||||||
|
receiver_email =os.getenv("RECEIVER_EMAIL", "").split(",")
|
||||||
|
smtp_server = os.getenv("SMTP_SERVER", "")
|
||||||
|
smtp_port = os.getenv("SMTP_PORT", "")
|
||||||
|
smtp_user = os.getenv("SMTP_USER", "")
|
||||||
|
smtp_password = os.getenv("SMTP_PASSWORD", "")
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
|
def run_cmd(cmd):
|
||||||
|
return subprocess.getoutput(cmd)
|
||||||
|
|
||||||
|
def check_service(service):
|
||||||
|
status = subprocess.run(["systemctl", "is-active", service], capture_output=True, text=True)
|
||||||
|
if status.returncode == 0:
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
for attempt in range(2):
|
||||||
|
print("Attempting to start: " + service)
|
||||||
|
subprocess.run(["systemctl", "restart", service])
|
||||||
|
status = subprocess.run(["systemctl", "is-active", service], capture_output=True, text=True)
|
||||||
|
if status.returncode == 0:
|
||||||
|
return True, ""
|
||||||
|
logs = run_cmd(f"journalctl -u {service} -n 20 --no-pager")
|
||||||
|
return False, logs
|
||||||
|
|
||||||
|
def check_container(container):
|
||||||
|
status = run_cmd(f"docker inspect -f '{{{{.State.Running}}}}' {container}")
|
||||||
|
if status.strip() == "true":
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
for attempt in range(2):
|
||||||
|
run_cmd(f"docker restart {container}")
|
||||||
|
status = run_cmd(f"docker inspect -f '{{{{.State.Running}}}}' {container}")
|
||||||
|
if status.strip() == "true":
|
||||||
|
return True, ""
|
||||||
|
logs = run_cmd(f"docker logs --tail 20 {container}")
|
||||||
|
return False, logs
|
||||||
|
|
||||||
|
def send_email(failures):
|
||||||
|
if not failures:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = MIMEMultipart("alternative")
|
||||||
|
msg["From"] = sender_email
|
||||||
|
msg["To"] = ", ".join(receiver_email)
|
||||||
|
msg["Subject"] = "Service/Container Failure Report"
|
||||||
|
|
||||||
|
# HTML Email Body
|
||||||
|
html = """
|
||||||
|
<html>
|
||||||
|
<body style="font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;">
|
||||||
|
<h2 style="color: #dc3545;">⚠️ Failure Report</h2>
|
||||||
|
<p>The following services/containers failed even after restart attempts:</p>
|
||||||
|
<hr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
for name, logs in failures.items():
|
||||||
|
html += f"""
|
||||||
|
<div style="margin-bottom: 20px; padding: 10px; background-color: #fff3cd; border-left: 5px solid #dc3545;">
|
||||||
|
<h3 style="margin: 0; color: #721c24;">❌ {name} Failed</h3>
|
||||||
|
<p><b>Last 20 log lines:</b></p>
|
||||||
|
<pre style="background-color: #f1f1f1; padding: 10px; border-radius: 5px; max-height: 300px; overflow-y: auto; font-size: 12px; color: #212529;">{logs}</pre>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html += """
|
||||||
|
<hr>
|
||||||
|
<p style="color: #6c757d;">This is an automated alert. Please check the server immediately.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg.attach(MIMEText(html, "html"))
|
||||||
|
|
||||||
|
with smtplib.SMTP(smtp_server, smtp_port) as server:
|
||||||
|
server.starttls()
|
||||||
|
server.login(smtp_user, smtp_password)
|
||||||
|
server.sendmail(sender_email, receiver_email, msg.as_string())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
failures = {}
|
||||||
|
print("inside __main__")
|
||||||
|
for service in services:
|
||||||
|
print("looping services: " + service)
|
||||||
|
ok, logs = check_service(service)
|
||||||
|
print(ok)
|
||||||
|
print(logs)
|
||||||
|
if not ok:
|
||||||
|
failures[service] = logs
|
||||||
|
|
||||||
|
for container in containers:
|
||||||
|
ok, logs = check_container(container)
|
||||||
|
print("looping containers: " + container)
|
||||||
|
print(ok)
|
||||||
|
print(logs)
|
||||||
|
if not ok:
|
||||||
|
failures[f"Docker: {container}"] = logs
|
||||||
|
print(failures)
|
||||||
|
send_email(failures)
|
Loading…
x
Reference in New Issue
Block a user