Compare commits
7 Commits
python-ema
...
main
Author | SHA1 | Date | |
---|---|---|---|
e64ef287a6 | |||
225f6bf8d4 | |||
d989076680 | |||
f742044350 | |||
08651c111f | |||
cc2a168a47 | |||
a0c83caa14 |
@ -3,17 +3,24 @@
|
||||
"SMPTSERVER": "smtp.gmail.com",
|
||||
"PORT": 587,
|
||||
"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": {
|
||||
"BASE_URL": "http://localhost:5032/api",
|
||||
"BASE_URL": "https://stageapi.marcoaiot.com/api",
|
||||
"USERNAME": "admin@marcoaiot.com",
|
||||
"PASSWORD": "User@123"
|
||||
"PASSWORD": "User@123",
|
||||
"TENANTID": "b3466e83-7e11-464c-b93a-daf047838b26"
|
||||
},
|
||||
"WEB":{
|
||||
"BASE_URL": "https://stageapp.marcoaiot.com"
|
||||
},
|
||||
"MONGODB":{
|
||||
"MONGO_CONNECTION_STRING": "mongodb://localhost:27017",
|
||||
"DATABASE_NAME": "MarcoBMS_Caches",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #b10000;
|
||||
background: #49bf3c;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
@ -57,6 +57,8 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
border: 1px solid #ddd;
|
||||
@ -67,10 +69,58 @@
|
||||
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;
|
||||
border-top: 1px solid #49bf3c;
|
||||
|
||||
}
|
||||
|
||||
.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 {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
@ -98,7 +148,7 @@
|
||||
}
|
||||
|
||||
.attendance {
|
||||
color: #b10000;
|
||||
color: #49bf3c;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
@ -136,7 +186,8 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #b10000;
|
||||
/* background: #b10000; */
|
||||
background: #49bf3c;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
@ -169,7 +220,7 @@
|
||||
.legend {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
@ -190,7 +241,7 @@
|
||||
}
|
||||
|
||||
.legend-red {
|
||||
background: #b10000;
|
||||
background: #e63946;
|
||||
}
|
||||
|
||||
.legend-blue {
|
||||
@ -205,19 +256,23 @@
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.legend-yellow {
|
||||
background: #ffc107;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.donut {
|
||||
--percentage: 65;
|
||||
--percentage: 0;
|
||||
/* Change this per chart */
|
||||
--danger: #e63946;
|
||||
--primary: #007bff;
|
||||
--warning: #ffc107;
|
||||
--success: #198754;
|
||||
--success: #28a745;
|
||||
/* Fill color */
|
||||
--track: #e9ecef;
|
||||
/* Background track */
|
||||
--size: 120px;
|
||||
--size: 200px;
|
||||
/* Default size */
|
||||
--thickness: 20px;
|
||||
/* Default thickness */
|
||||
@ -225,8 +280,9 @@
|
||||
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(--danger) calc(var(--percentage) * 1%),
|
||||
var(--track) 0); */
|
||||
background: conic-gradient(var(--track) 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -249,6 +305,13 @@
|
||||
.donut span {
|
||||
position: absolute;
|
||||
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 */
|
||||
@ -259,7 +322,7 @@
|
||||
|
||||
.donut.medium {
|
||||
--size: 120px;
|
||||
--thickness: 25px;
|
||||
--thickness: 15px;
|
||||
}
|
||||
|
||||
.donut.large {
|
||||
@ -271,25 +334,35 @@
|
||||
.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>
|
||||
@ -300,298 +373,246 @@
|
||||
<div class="header">
|
||||
<h1>Daily Progress Report</h1>
|
||||
<div class="project-info">
|
||||
<strong>Project:</strong> ANP ultimas wakad<br>
|
||||
<strong>Date:</strong> 17 September 2025
|
||||
<strong>Project:</strong> {{projectName}}<br>
|
||||
<strong>Date:</strong> {{date}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Note -->
|
||||
<div class="status-note">
|
||||
* Project Status Reported - Generated at 18-Sep-2025 03:30:03 UTC
|
||||
* Project Status Reported - Generated at {{timeStamp}} UTC
|
||||
</div>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="status-cards">
|
||||
<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>
|
||||
<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" style="--percentage: 66;">
|
||||
<span>20 / 30</span>
|
||||
<div class="donut thin donut-warning" style="--percentage: {{attendancePercentage}};">
|
||||
<span>
|
||||
<p style="color:#ffc107;">{{todaysAttendances}}</p><p>/</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-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
|
||||
<span class="legend-color legend-yellow"></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> Pending
|
||||
<span class="legend-color legend-gray"></span> Total Employees
|
||||
</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">
|
||||
<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>
|
||||
<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: 66;">
|
||||
<span>20 / 30</span>
|
||||
<div class="donut thin donut-primary" style="--percentage: {{taskPercentage}};">
|
||||
<span >
|
||||
<p style="color:#007bff;">{{totalCompletedTask}}</p><p>/</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-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
|
||||
<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> Pending
|
||||
<span class="legend-color legend-gray"></span> Planned Work
|
||||
</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">
|
||||
<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 style="text-align: end!important;">
|
||||
<p style="font-size: xx-small;color: #ccc;">*Today's Total Work</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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;">
|
||||
|
||||
<!-- Left Column -->
|
||||
<div style="width:50%; box-sizing:border-box;display:flex; justify-content:center; align-items:center;">
|
||||
<!-- Medium -->
|
||||
<div class="donut thin donut-success" style="--percentage: 73;">
|
||||
<span>73%</span>
|
||||
<div class="donut thin donut-success" style="--percentage: {{completionStatus}};">
|
||||
<span>
|
||||
<p style="color:#28a745;">{{totalCompletedWork}}</p> <p>/</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-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
|
||||
<span class="legend-color legend-green"></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> Pending
|
||||
<span class="legend-color legend-gray"></span> Planned Work
|
||||
</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 style="text-align: end!important;">
|
||||
<p style="font-size: xx-small;color: #ccc;">*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>
|
||||
<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>
|
||||
<h4 class="card-title">Activity Report Pending</h4>
|
||||
</div>
|
||||
<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-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>
|
||||
<table style="width: 100%;">
|
||||
<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 %}
|
||||
<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 -->
|
||||
<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;">Site Engineer</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>
|
||||
|
||||
<td style="text-align: left;">{{a.roleName}}</td>
|
||||
<td style="text-align: right;">{{a.numberofEmployees}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Activities -->
|
||||
<div class="activities">
|
||||
<h2>Activities (Tasks) Performed 17-Sep-2025</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>NAME</th>
|
||||
<th>JOB ROLE</th>
|
||||
<th>CHECK IN</th>
|
||||
<th>CHECK OUT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Siddharth Barde</td>
|
||||
<td>Site Engineer</td>
|
||||
<td>17-Sep-2025 11:47 AM</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Siddharth Barde</td>
|
||||
<td>Site Engineer</td>
|
||||
<td>17-Sep-2025 11:47 AM</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Siddharth Barde</td>
|
||||
<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
|
||||
<!-- 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 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> |
|
||||
</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>
|
||||
|
||||
<a href="#"><img src="https://cdn.marcoaiot.com/icons/brands/twitter.png" style="height: 15px;" /></a> |
|
||||
<div style="width: 50%; text-align: right;">
|
||||
<!-- <a href="#">Instagram</a> | -->
|
||||
<a href="https://www.linkedin.com/company/marco-aiot">Linkedin</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
|
||||
<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/youtube.png" style="height: 15px;" /></a> | <a
|
||||
href="#">LinkedIn</a> |
|
||||
<a href="#">YouTube</a> -->
|
||||
</div>
|
||||
</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>
|
||||
<div style="text-align: center;width: 100%;background-color: #fff;margin:10px;margin-bottom: 30px!important;font-size: small;color: #6c757d ;">
|
||||
You have received this email because it contains important information about your {{websiteName}} Account account.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -1,157 +0,0 @@
|
||||
<!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>
|
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)
|
148
mailling/project_statistics_report.py
Normal file
148
mailling/project_statistics_report.py
Normal file
@ -0,0 +1,148 @@
|
||||
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)
|
@ -1,157 +0,0 @@
|
||||
<!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>
|
File diff suppressed because one or more lines are too long
@ -1,10 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
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()
|
@ -1,35 +0,0 @@
|
||||
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()
|
@ -1,84 +0,0 @@
|
||||
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()
|
@ -1,11 +0,0 @@
|
||||
# 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
|
@ -1,114 +0,0 @@
|
||||
#!/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