[UPDT] HORILLA_THEME: Added the style for Report module
This commit is contained in:
@@ -3099,3 +3099,214 @@ span.oh-activity-sidebar__a img {
|
||||
background: #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* REPORT MODULE STYLE */
|
||||
|
||||
#pivot-container {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
#pivot-container .pvtUiCell {
|
||||
background-color: #fff;
|
||||
}
|
||||
#pivot-container .pvtUiCell .pvtRenderer,
|
||||
#pivot-container .pvtUiCell .pvtAggregator {
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.pvtRenderer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.pvtRenderer :focus {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
.pvtRenderer :focus {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
#pivot-container .pvtColLabel,
|
||||
#pivot-container .pvtRowLabel {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable tbody tr th,
|
||||
#pivot-container .pvtTable thead tr th {
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #e9edf1 !important;
|
||||
}
|
||||
|
||||
.pvtAxisContainer li {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pvtAxisContainer li span.pvtAttr {
|
||||
border-radius: 0 !important;
|
||||
flex: 1 1 auto;
|
||||
background-color: #f9f9f9 !important;
|
||||
position: relative;
|
||||
}
|
||||
.pvtAxisContainer li span.pvtAttr .pvtTriangle {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.pvtHorizList .ui-sortable-handle .pvtAttr {
|
||||
padding: 2px 26px 2px 8px !important;
|
||||
}
|
||||
.pvtAxisContainer,
|
||||
.pvtVals {
|
||||
border: 1px solid #e9edf1 !important;
|
||||
}
|
||||
|
||||
.pvtRendererArea {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#pivot-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
#pivot-container .pvtTable {
|
||||
min-width: 1200px;
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 16px;
|
||||
background-color: #ffffff;
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.pvtUi {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#pivot-container .pvtUiCell .pvtAggregator,
|
||||
#pivot-container .pvtUiCell .pvtRenderer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.pvtAttrDropdown {
|
||||
border: 1px solid #eaeaea !important;
|
||||
}
|
||||
|
||||
.pvtSearch {
|
||||
border: 1px solid hsl(213, 22%, 84%) !important;
|
||||
border-radius: 0rem !important;
|
||||
padding: 0.8rem 1.25rem !important;
|
||||
color: hsl(0, 0%, 11%) !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.pvtFilterBox {
|
||||
padding: 16px;
|
||||
width: auto !important;
|
||||
border: 1px solid #e9edf1 !important;
|
||||
}
|
||||
.pvtFilterBox h4, .pvtFilterBox .h4 {
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
margin-left: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.pvtFilterBox p {
|
||||
margin-bottom: 0 !important;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.pvtFilterBox p :nth-child(1) {
|
||||
width: 100%;
|
||||
border-radius: 0 !important;
|
||||
background-color: hsl(8, 77%, 56%);
|
||||
color: hsl(0, 0%, 100%);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border: none;
|
||||
}
|
||||
.pvtFilterBox p :nth-child(2) {
|
||||
width: 100%;
|
||||
border-radius: 0 !important;
|
||||
background-color: #f0f0f0;
|
||||
color: hsl(0, 0%, 16%);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.65rem 1rem;
|
||||
border: none;
|
||||
}
|
||||
.pvtFilterBox p input {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.pvtFilterBox p button {
|
||||
border: none !important;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pvtFilterBox .pvtCheckContainer p :nth-child(1) {
|
||||
background-color: unset !important;
|
||||
width: auto;
|
||||
padding: 0.5rem 0 !important;
|
||||
}
|
||||
.pvtFilterBox .pvtCheckContainer p :nth-child(2) {
|
||||
background-color: transparent !important;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable td {
|
||||
border: 1px solid #e9edf1 !important;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable tr:nth-child(2n) td {
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #e9edf1 !important;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable th {
|
||||
background-color: #007bff;
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable td {
|
||||
padding: 12px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 16px;
|
||||
background-color: #fdfdfd;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable tr:nth-child(even) td {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
#pivot-container .pvtRowLabel,
|
||||
#pivot-container .pvtColLabel {
|
||||
background-color: #e9ecef;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTotalLabel {
|
||||
background-color: #6c757d;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#pivot-container .pvtGrandTotal {
|
||||
background-color: #343a40;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#pivot-container .pvtTable tbody tr th,
|
||||
#pivot-container .pvtTable thead tr th {
|
||||
background-color: #e6eeee;
|
||||
border: 2px solid #cdcdcd;
|
||||
font-size: 10pt;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
rel="stylesheet"
|
||||
href="/static/horilla_theme/assets/css/v1_styles.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="{% static 'build/css/pivottable.min.css' %}" />
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-secondary-50 font-[Inter,_sans-serif]">
|
||||
|
||||
383
horilla_theme/templates/report/asset_report.html
Normal file
383
horilla_theme/templates/report/asset_report.html
Normal file
@@ -0,0 +1,383 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load widget_tweaks %}
|
||||
{% load assets_custom_filter %}
|
||||
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Asset Reports" %}
|
||||
</h3>
|
||||
|
||||
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Asset" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_name.id_for_label}}">{% trans "Asset Name" %}</label>
|
||||
{{asset_filter_form.asset_name}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_tracking_id.id_for_label}}">{% trans "Tracking Id" %}</label>
|
||||
{{asset_filter_form.asset_tracking_id}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_purchase_date.id_for_label}}">{% trans "Purchase Date" %}</label>
|
||||
{{asset_filter_form.asset_purchase_date |attr:"type:date"}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_purchase_cost.id_for_label}}">{% trans "Purchase Cost" %}</label>
|
||||
{{asset_filter_form.asset_purchase_cost}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_lot_number_id.id_for_label}}">{% trans "Asset Batch Number" %}</label>
|
||||
{{asset_filter_form.asset_lot_number_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_category_id.id_for_label}}">{% trans "Category" %}</label>
|
||||
{{asset_filter_form.asset_category_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{asset_filter_form.asset_status.id_for_label}}">{% trans "Status" %}</label>
|
||||
{{asset_filter_form.asset_status}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// Function to load filtered pivot data
|
||||
function loadFilteredPivotData() {
|
||||
// Get filter form data
|
||||
var formData = $("#filterForm").serialize(); // Serializing the form
|
||||
|
||||
$.ajax({
|
||||
url: "asset-pivot", // Replace with your URL to fetch filtered data
|
||||
method: "GET",
|
||||
data: formData,
|
||||
success: function (data) {
|
||||
// Render the pivot table with the filtered data
|
||||
$("#pivot-container").pivotUI(data, {
|
||||
rows: ["Asset Name","Category","Tracking ID","Batch Number","Status","Asset Cost","Asset User","Phone"],
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table", // Default view as Table
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
},
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
$.pivotUtilities.plotly_renderers // Adding Plotly renderers
|
||||
)
|
||||
});
|
||||
},
|
||||
error: function (error) {
|
||||
console.log("Error fetching filtered data: ", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Initialize the pivot table on page load
|
||||
$.getJSON("asset-pivot", function (data) {
|
||||
$("#pivot-container").pivotUI(data, {
|
||||
rows: ["Asset Name","Category","Tracking ID","Batch Number","Status","Asset Cost","Asset User","Phone"],
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table", // Default view as Table
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
},
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
$.pivotUtilities.plotly_renderers // Adding Plotly renderers
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
// When the filter form is submitted, prevent default action and load filtered data
|
||||
$("#filterForm").submit(function (event) {
|
||||
event.preventDefault();
|
||||
loadFilteredPivotData(); // Call the function to load filtered data
|
||||
});
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
exportTableToExcel("pivot-container", "pivot_report.xlsx");
|
||||
});
|
||||
});
|
||||
|
||||
// Export FunctionN
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3;
|
||||
|
||||
// Table rendering
|
||||
const cellMap = {};
|
||||
const allRows = Array.from(table.rows);
|
||||
const lastRowIndex = allRows.length - 1;
|
||||
|
||||
allRows.forEach((row, rowIndex) => {
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
const isHeader = rowIndex === 0;
|
||||
const isLastRow = rowIndex === lastRowIndex;
|
||||
|
||||
excelCell.font = {
|
||||
bold: isHeader || isLastRow,
|
||||
size: isHeader ? 12 : 11,
|
||||
color: {
|
||||
argb: isHeader ? 'FFFFFFFF' :
|
||||
isLastRow ? 'FF000000' :
|
||||
'FF000000'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: {
|
||||
argb: isHeader ? 'FF545454' :
|
||||
isLastRow ? 'FFFFE599' : // light yellow
|
||||
'FFF5F5F5'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
|
||||
|
||||
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
|
||||
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 2;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 3;
|
||||
});
|
||||
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
526
horilla_theme/templates/report/attendance_report.html
Normal file
526
horilla_theme/templates/report/attendance_report.html
Normal file
@@ -0,0 +1,526 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
{% include 'filter_tags.html' %}
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Attendance Reports" %}
|
||||
</h3>
|
||||
|
||||
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Attendance" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Employee" %}</label>
|
||||
{{f.form.employee_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Department" %}</label>
|
||||
{{f.form.employee_id__employee_work_info__department_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Shift" %}</label>
|
||||
{{f.form.shift_id}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Company" %}</label>
|
||||
{{f.form.employee_id__employee_work_info__company_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Job Position" %}</label>
|
||||
{{f.form.employee_id__employee_work_info__job_position_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Work Type" %}</label>
|
||||
{{f.form.work_type_id}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Attendance Date" %}</label>
|
||||
{{f.form.attendance_date}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "In Time" %}</label>
|
||||
{{f.form.attendance_clock_in}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Min Hour" %}</label>
|
||||
{{f.form.minimum_hour}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Batch" %}</label>
|
||||
{{f.form.batch_attendance_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Out Time" %}</label>
|
||||
{{f.form.attendance_clock_out}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Attendance From" %}</label>
|
||||
{{f.form.attendance_date__gte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "In From" %}</label>
|
||||
{{f.form.attendance_clock_in__lte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Out From" %}</label>
|
||||
{{f.form.attendance_clock_out__lte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "At Work Greater or Equal" %}</label>
|
||||
{{f.form.at_work_second__gte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "OT Greater or Equal" %}</label>
|
||||
{{f.form.overtime_second__gte}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Attendance Till" %}</label>
|
||||
{{f.form.attendance_date__lte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "In Till" %}</label>
|
||||
{{f.form.attendance_clock_in__lte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Out Till" %}</label>
|
||||
{{f.form.attendance_clock_out__lte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "At Work Less Than or Equal" %}</label>
|
||||
{{f.form.at_work_second__lte}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "OT Less Than or Equal" %}</label>
|
||||
{{f.form.overtime_second__lte}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
// Function to load filtered pivot data
|
||||
function loadFilteredPivotData() {
|
||||
// Get filter form data
|
||||
var formData = $("#filterForm").serialize(); // Serializing the form
|
||||
|
||||
$.ajax({
|
||||
url: "attendance-pivot", // Replace with your URL to fetch filtered data
|
||||
method: "GET",
|
||||
data: formData,
|
||||
success: function (data) {
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#pivot-container").pivotUI(data, {
|
||||
rows: ["Name","Phone","Department","Shift","Attendance Date","Attendance Day","Worked Hour"], // Default rows
|
||||
cols: [], // Default columns
|
||||
aggregatorName: "Count", // Default aggregator
|
||||
rendererName: "Table", // Default view as Table
|
||||
|
||||
// Hide decimal fields from rows and columns but keep them available for aggregation
|
||||
hiddenFromDragDrop: [
|
||||
"Clock-in Decimal",
|
||||
"Clock-out Decimal",
|
||||
"At Work Decimal",
|
||||
"Worked Hour Decimal",
|
||||
"Minimum Hour Decimal",
|
||||
"Overtime Decimal"
|
||||
],
|
||||
unusedAttrsVertical: true, // Keep available in the left-side panel for aggregation
|
||||
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
plotlyRenderers // Adding Plotly renderers
|
||||
),
|
||||
|
||||
// Hide specific fields from selection dropdown
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (
|
||||
currentRenderer === "Table" ||
|
||||
currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" ||
|
||||
currentRenderer === "Row Heatmap" ||
|
||||
currentRenderer === "Col Heatmap"
|
||||
) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
|
||||
// Hide fields from dropdown but keep them available in the table
|
||||
let hiddenFields = [
|
||||
"Clock-in",
|
||||
"Clock-out",
|
||||
"Worked Hour",
|
||||
"Minimum Hour",
|
||||
"Overtime",
|
||||
"At Work"
|
||||
];
|
||||
|
||||
setTimeout(function () {
|
||||
$(".pvtAttrDropdown option").each(function () {
|
||||
if (hiddenFields.includes($(this).text())) {
|
||||
$(this).remove(); // Remove from selection
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
error: function (error) {
|
||||
console.log("Error fetching filtered data: ", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Initialize the pivot table on page load
|
||||
$.getJSON("attendance-pivot", function (data) {
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#pivot-container").pivotUI(data, {
|
||||
rows: ["Name","Phone","Department","Shift","Attendance Date","Attendance Day","Worked Hour"], // Default rows
|
||||
cols: [], // Default columns
|
||||
aggregatorName: "Count", // Default aggregator
|
||||
rendererName: "Table", // Default view as Table
|
||||
|
||||
// Hide decimal fields from rows and columns but keep them available for aggregation
|
||||
hiddenFromDragDrop: [
|
||||
"Clock-in Decimal",
|
||||
"Clock-out Decimal",
|
||||
"At Work Decimal",
|
||||
"Worked Hour Decimal",
|
||||
"Minimum Hour Decimal",
|
||||
"Overtime Decimal"
|
||||
],
|
||||
unusedAttrsVertical: true, // Keep available in the left-side panel for aggregation
|
||||
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
plotlyRenderers // Adding Plotly renderers
|
||||
),
|
||||
|
||||
// Hide specific fields from selection dropdown
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (
|
||||
currentRenderer === "Table" ||
|
||||
currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" ||
|
||||
currentRenderer === "Row Heatmap" ||
|
||||
currentRenderer === "Col Heatmap"
|
||||
) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
|
||||
// Hide fields from dropdown but keep them available in the table
|
||||
let hiddenFields = [
|
||||
"Clock-in",
|
||||
"Clock-out",
|
||||
"Worked Hour",
|
||||
"Minimum Hour",
|
||||
"Overtime",
|
||||
"At Work"
|
||||
];
|
||||
|
||||
setTimeout(function () {
|
||||
$(".pvtAttrDropdown option").each(function () {
|
||||
if (hiddenFields.includes($(this).text())) {
|
||||
$(this).remove(); // Remove from selection
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// When the filter form is submitted, prevent default action and load filtered data
|
||||
$("#filterForm").submit(function (event) {
|
||||
event.preventDefault();
|
||||
loadFilteredPivotData(); // Call the function to load filtered data
|
||||
});
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
exportTableToExcel("pivot-container", "pivot_report.xlsx");
|
||||
});
|
||||
});
|
||||
|
||||
// Export FunctionN
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3;
|
||||
|
||||
// Table rendering
|
||||
const cellMap = {};
|
||||
const allRows = Array.from(table.rows);
|
||||
const lastRowIndex = allRows.length - 1;
|
||||
|
||||
allRows.forEach((row, rowIndex) => {
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
const isHeader = rowIndex === 0;
|
||||
const isLastRow = rowIndex === lastRowIndex;
|
||||
|
||||
excelCell.font = {
|
||||
bold: isHeader || isLastRow,
|
||||
size: isHeader ? 12 : 11,
|
||||
color: {
|
||||
argb: isHeader ? 'FFFFFFFF' :
|
||||
isLastRow ? 'FF000000' :
|
||||
'FF000000'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: {
|
||||
argb: isHeader ? 'FF545454' :
|
||||
isLastRow ? 'FFFFE599' : // light yellow
|
||||
'FFF5F5F5'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
|
||||
|
||||
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
|
||||
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 2;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 3;
|
||||
});
|
||||
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
413
horilla_theme/templates/report/employee_report.html
Normal file
413
horilla_theme/templates/report/employee_report.html
Normal file
@@ -0,0 +1,413 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Employee Reports" %}
|
||||
</h3>
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Employee" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_first_name.id_for_label}}">{% trans "First Name" %}</label>
|
||||
{{f.form.employee_first_name}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.email.id_for_label}}">{% trans "Email" %}</label>
|
||||
{{f.form.email}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.country.id_for_label}}">{% trans "Country" %}</label>
|
||||
{{f.form.country}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_last_name.id_for_label}}">{% trans "Last Name" %}</label>
|
||||
{{f.form.employee_last_name}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.phone.id_for_label}}">{% trans "Phone" %}</label>
|
||||
{{f.form.phone}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.gender.id_for_label}}">{% trans "Gender" %}</label>
|
||||
{{f.form.gender}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__company_id.id_for_label}}">{% trans "Company" %}</label>
|
||||
{{f.form.employee_work_info__company_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__department_id.id_for_label}}">{% trans "Department" %}</label>
|
||||
{{f.form.employee_work_info__department_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__job_role_id.id_for_label}}">{% trans "Job Role" %}</label>
|
||||
{{f.form.employee_work_info__job_role_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__shift_id.id_for_label}}">{% trans "Shift" %}</label>
|
||||
{{f.form.employee_work_info__shift_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__reporting_manager_id.id_for_label}}">{% trans "Reporting Manager" %}</label>
|
||||
{{f.form.employee_work_info__reporting_manager_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__job_position_id.id_for_label}}">{% trans "Job Position" %}</label>
|
||||
{{f.form.employee_work_info__job_position_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__work_type_id.id_for_label}}">{% trans "Work Type" %}</label>
|
||||
{{f.form.employee_work_info__work_type_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_work_info__employee_type_id.id_for_label}}">{% trans "Employee Type" %}</label>
|
||||
{{f.form.employee_work_info__employee_type_id}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// Function to load filtered pivot data
|
||||
function loadFilteredPivotData() {
|
||||
// Get filter form data
|
||||
var formData = $("#filterForm").serialize(); // Serializing the form
|
||||
|
||||
$.ajax({
|
||||
url: "employee-pivot", // Replace with your URL to fetch filtered data
|
||||
method: "GET",
|
||||
data: formData,
|
||||
success: function (data) {
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#pivot-container").pivotUI(data, {
|
||||
rows: ["Department","Job Position","Job Role","Name","Email","Phone"],
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table", // Default view as Table
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
},
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
plotlyRenderers // Adding Plotly renderers
|
||||
)
|
||||
});
|
||||
|
||||
},
|
||||
error: function (error) {
|
||||
console.log("Error fetching filtered data: ", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Initialize the pivot table on page load
|
||||
$.getJSON("employee-pivot", function (data) {
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#pivot-container").pivotUI(data, {
|
||||
rows: ["Department","Job Position","Job Role","Name","Email","Phone"],
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table", // Default view as Table
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
},
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
plotlyRenderers // Adding Plotly renderers
|
||||
)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// When the filter form is submitted, prevent default action and load filtered data
|
||||
$("#filterForm").submit(function (event) {
|
||||
event.preventDefault();
|
||||
loadFilteredPivotData(); // Call the function to load filtered data
|
||||
});
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
exportTableToExcel("pivot-container", "pivot_report.xlsx");
|
||||
});
|
||||
});
|
||||
|
||||
// Export FunctionN
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3;
|
||||
|
||||
// Table rendering
|
||||
const cellMap = {};
|
||||
const allRows = Array.from(table.rows);
|
||||
const lastRowIndex = allRows.length - 1;
|
||||
|
||||
allRows.forEach((row, rowIndex) => {
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
const isHeader = rowIndex === 0;
|
||||
const isLastRow = rowIndex === lastRowIndex;
|
||||
|
||||
excelCell.font = {
|
||||
bold: isHeader || isLastRow,
|
||||
size: isHeader ? 12 : 11,
|
||||
color: {
|
||||
argb: isHeader ? 'FFFFFFFF' :
|
||||
isLastRow ? 'FF000000' :
|
||||
'FF000000'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: {
|
||||
argb: isHeader ? 'FF545454' :
|
||||
isLastRow ? 'FFFFE599' : // light yellow
|
||||
'FFF5F5F5'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
|
||||
|
||||
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
|
||||
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 2;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 3;
|
||||
});
|
||||
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
593
horilla_theme/templates/report/leave_report.html
Normal file
593
horilla_theme/templates/report/leave_report.html
Normal file
@@ -0,0 +1,593 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% include 'filter_tags.html' %}
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Leave Reports" %}
|
||||
</h3>
|
||||
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Leave Request" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<input type="hidden" value="{{request.GET.id}}" name="id">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id.id_for_label }}">{% trans "Employees" %}</label>
|
||||
{{form.employee_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.leave_type_id.id_for_label }}">{% trans "Leave Type" %}</label>
|
||||
{{form.leave_type_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.start_date.id_for_label }}">{% trans "Start Date" %}</label>
|
||||
{{form.start_date}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.end_date.id_for_label }}">{% trans "End Date" %}</label>
|
||||
{{form.end_date}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.status.id_for_label }}">{% trans "Status" %}</label>
|
||||
{{form.status}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.from_date.id_for_label }}">{% trans "From Date" %}</label>
|
||||
{{form.from_date}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.to_date.id_for_label }}">{% trans "To Date" %}</label>
|
||||
{{form.to_date}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id__employee_work_info__company_id.id_for_label }}">{% trans "Company" %}</label>
|
||||
{{form.employee_id__employee_work_info__company_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id__employee_work_info__reporting_manager_id.id_for_label }}">{% trans "Reporting Manager" %}</label>
|
||||
{{form.employee_id__employee_work_info__reporting_manager_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id__employee_work_info__department_id.id_for_label }}">{% trans "Department" %}</label>
|
||||
{{form.employee_id__employee_work_info__department_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id__employee_work_info__job_position_id.id_for_label }}">{% trans "Job Position" %}</label>
|
||||
{{form.employee_id__employee_work_info__job_position_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id__employee_work_info__shift_id.id_for_label }}">{% trans "Shift" %}</label>
|
||||
{{form.employee_id__employee_work_info__shift_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{ form.employee_id__employee_work_info__work_type_id.id_for_label }}">{% trans "Work Type" %}</label>
|
||||
{{form.employee_id__employee_work_info__work_type_id}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Available Leave" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Employees" %}</label>
|
||||
{{f.form.employee_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Leave Type" %}</label>
|
||||
{{f.form.leave_type_id}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Available Days" %}</label>
|
||||
{{f.form.available_days}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Carryforward Days" %}</label>
|
||||
{{f.form.carryforward_days}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Total Leave Days" %}</label>
|
||||
{{f.form.total_leave_days}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Assigned Date" %}</label>
|
||||
{{f.form.assigned_date}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label"
|
||||
>{% trans "Available Days Greater or Equal" %}</label
|
||||
>
|
||||
{{f.form.available_days__gte}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label"
|
||||
>{% trans "Available Days Less Than or Equal" %}</label
|
||||
>
|
||||
{{f.form.available_days__lte}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label"
|
||||
>{% trans "Carryforward Days Greater or Equal" %}</label
|
||||
>
|
||||
{{f.form.carryforward_days__gte}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label"
|
||||
>{% trans "Carryforward Days Less Than or Equal" %}</label
|
||||
>
|
||||
{{f.form.carryforward_days__lte}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label"
|
||||
>{% trans "Total Leave Days Greater or Equal" %}</label
|
||||
>
|
||||
{{f.form.total_leave_days__gte}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label"
|
||||
>{% trans "Total Leave Days Less Than or Equal" %}</label
|
||||
>
|
||||
{{f.form.total_leave_days__lte}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Model Selection Dropdown -->
|
||||
<div style="width: 350px; display:flex;">
|
||||
<span class="mt-1 text-md font-semibold" style="font-size:16px; width:200px;">{% trans "Choose Report" %} : </span>
|
||||
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
|
||||
<option value="leave_request">{% trans "Leave Request" %}</option>
|
||||
<option value="available_leave">{% trans "Available Leave" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
|
||||
<div id="pivot-leave" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
<div id="pivot-availableleave" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
$(function () {
|
||||
// Function to load pivot data dynamically
|
||||
function loadPivotData(model) {
|
||||
let url = `leave-pivot?model=${model}`;
|
||||
|
||||
// Hide all container
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (model === "leave_request"){
|
||||
containerId = "pivot-leave";
|
||||
rowsConfig = ["Name","Department","Shift", "Leave Type","Requested Days","Start Date","End Date","Status"]
|
||||
}else if (model === "available_leave"){
|
||||
containerId = "pivot-availableleave";
|
||||
rowsConfig = ["Name","Department", "Leave Type","Assigned Date","Total Leave Days","Available Days","Carryforward Days"]
|
||||
}
|
||||
|
||||
|
||||
// Show relevant container
|
||||
$("#" + containerId).show();
|
||||
|
||||
// Fetch data dynamically based on model
|
||||
$.getJSON(url, function (data) {
|
||||
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table", // Default view as Table
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (
|
||||
currentRenderer === "Table" ||
|
||||
currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" ||
|
||||
currentRenderer === "Row Heatmap" ||
|
||||
currentRenderer === "Col Heatmap"
|
||||
) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
},
|
||||
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers), // Add Plotly renderers
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.loadFilteredPivotData =function loadFilteredPivotData() {
|
||||
const selectedModel = $("#model-select").val();
|
||||
const formData = $("#filterForm").serialize();
|
||||
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (selectedModel === "leave_request"){
|
||||
containerId = "pivot-leave";
|
||||
rowsConfig = ["Name","Department","Shift", "Leave Type","Requested Days","Start Date","End Date","Status"]
|
||||
}else if (selectedModel === "available_leave"){
|
||||
containerId = "pivot-availableleave";
|
||||
rowsConfig = ["Name","Department", "Leave Type","Assigned Date","Total Leave Days","Available Days","Carryforward Days"]
|
||||
}
|
||||
|
||||
$("#" + containerId).show();
|
||||
|
||||
$.getJSON(`leave-pivot?model=${selectedModel}&${formData}`, function (data) {
|
||||
|
||||
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table",
|
||||
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
|
||||
onRefresh: function (config) {
|
||||
const currentRenderer = config.rendererName;
|
||||
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
|
||||
$("#export-btn").show();
|
||||
} else {
|
||||
$("#export-btn").hide();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load with all models
|
||||
loadPivotData("leave_request");
|
||||
|
||||
// Model selection change event
|
||||
$("#model-select").on("change", function () {
|
||||
let selectedModel = $(this).val();
|
||||
loadPivotData(selectedModel); // Reload pivot data with selected model
|
||||
});
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
|
||||
|
||||
if (visiblePivot.length) {
|
||||
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Export Function
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3; // Leave some rows before the table
|
||||
|
||||
// ------------------------
|
||||
// Render pivot table
|
||||
// ------------------------
|
||||
const cellMap = {};
|
||||
const allRows = Array.from(table.rows);
|
||||
const lastRowIndex = allRows.length - 1;
|
||||
|
||||
allRows.forEach((row, rowIndex) => {
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
const isHeader = rowIndex === 0;
|
||||
const isLastRow = rowIndex === lastRowIndex;
|
||||
|
||||
excelCell.font = {
|
||||
bold: isHeader || isLastRow,
|
||||
size: isHeader ? 12 : 11,
|
||||
color: {
|
||||
argb: isHeader ? 'FFFFFFFF' :
|
||||
isLastRow ? 'FF000000' :
|
||||
'FF000000'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: {
|
||||
argb: isHeader ? 'FF545454' :
|
||||
isLastRow ? 'FFFFE599' : // light yellow
|
||||
'FFF5F5F5'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
// Merge
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
|
||||
|
||||
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
|
||||
|
||||
// Auto-adjust column widths
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 2;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 3;
|
||||
});
|
||||
|
||||
// Save
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
|
||||
|
||||
587
horilla_theme/templates/report/payroll_report.html
Normal file
587
horilla_theme/templates/report/payroll_report.html
Normal file
@@ -0,0 +1,587 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Payroll Reports" %}
|
||||
</h3>
|
||||
|
||||
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Payslip" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
{% if perms.payroll.view_payslip %}
|
||||
<div class="col-sm-12">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.employee_id.id_for_label}}">{% trans "Employee" %}</label>
|
||||
{{ f.form.employee_id }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.status.id_for_label}}">{% trans "Status" %}</label>
|
||||
{{ f.form.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.group_name.id_for_label}}">{% trans "Batch" %}</label>
|
||||
{{ f.form.group_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.start_date_from.id_for_label}}">{% trans "Start Date From" %}</label>
|
||||
{{ f.form.start_date_from }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.start_date_till.id_for_label}}">{% trans "Start Date Till" %}</label>
|
||||
{{ f.form.start_date_till }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.end_date_from.id_for_label}}">{% trans "End Date From" %}</label>
|
||||
{{ f.form.end_date_from }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.end_date_till.id_for_label}}">{% trans "End Date Till" %}</label>
|
||||
{{ f.form.end_date_till }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.gross_pay__lte.id_for_label}}"
|
||||
>{% trans "Gross Pay Less Than or Equal" %}</label
|
||||
>
|
||||
{{ f.form.gross_pay__lte }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.gross_pay__gte.id_for_label}}"
|
||||
>{% trans "Gross Pay Greater or Equal" %}</label
|
||||
>
|
||||
{{ f.form.gross_pay__gte }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.deduction__lte.id_for_label}}"
|
||||
>{% trans "Deduction Less Than or Equal" %}</label
|
||||
>
|
||||
{{ f.form.deduction__lte }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.deduction__gte.id_for_label}}"
|
||||
>{% trans "Deduction Greater or Equal" %}</label
|
||||
>
|
||||
{{ f.form.deduction__gte }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.net_pay__lte.id_for_label}}"
|
||||
>{% trans "Net Pay Less Than or Equal" %}</label
|
||||
>
|
||||
{{ f.form.net_pay__lte }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.net_pay__gte.id_for_label}}"
|
||||
>{% trans "Net Pay Greater or Equal" %}</label
|
||||
>
|
||||
{{ f.form.net_pay__gte }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Allowance & Deduction" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.allowance_amount_gte.id_for_label}}">{% trans "Allowance Greater than" %}</label>
|
||||
{{ f.form.allowance_amount_gte }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.allowance_amount_lte.id_for_label}}">{% trans "Allowance Less than" %}</label>
|
||||
{{ f.form.allowance_amount_lte }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.deduction_amount_gte.id_for_label}}">{% trans "Deduction Greater than" %}</label>
|
||||
{{ f.form.deduction_amount_gte }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.deduction_amount_lte.id_for_label}}">{% trans "Deduction Less than" %}</label>
|
||||
{{ f.form.deduction_amount_lte }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.start_date_from.id_for_label}}">{% trans "Start Date From" %}</label>
|
||||
{{ f.form.start_date_from }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.start_date_till.id_for_label}}">{% trans "Start Date Till" %}</label>
|
||||
{{ f.form.start_date_till }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.end_date_from.id_for_label}}">{% trans "End Date From" %}</label>
|
||||
{{ f.form.end_date_from }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{f.form.end_date_till.id_for_label}}">{% trans "End Date Till" %}</label>
|
||||
{{ f.form.end_date_till }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection Dropdown -->
|
||||
<div style="width: 350px; display:flex;">
|
||||
<span class="mt-1 text-md font-semibold" style="font-size:16px; width:200px;">{% trans "Choose Report" %} : </span>
|
||||
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
|
||||
<option value="payslip">{% trans "Payslip" %}</option>
|
||||
<option value="allowance">{% trans "Allowance & Deduction" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
|
||||
<div id="pivot-payslip" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
<div id="pivot-allowance" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
// Function to load pivot data dynamically
|
||||
function loadPivotData(model) {
|
||||
let url = `payroll-pivot?model=${model}`;
|
||||
|
||||
// Hide all containers first
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
// Determine current container and row config
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (model === "payslip") {
|
||||
containerId = "pivot-payslip";
|
||||
rowsConfig = ["Employee","Basic Salary","Gross Pay","Net Pay","Status"];
|
||||
} else if (model === "allowance") {
|
||||
containerId = "pivot-allowance";
|
||||
rowsConfig = ["Employee","Allowance & Deduction","Allowance & Deduction Title","Allowance & Deduction Amount"];
|
||||
}
|
||||
|
||||
// Show relevant container
|
||||
$("#" + containerId).show();
|
||||
|
||||
|
||||
$.getJSON(url, function (data) {
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
|
||||
rows: rowsConfig,
|
||||
cols: [], // Default columns
|
||||
aggregatorName: "Count", // Default aggregator
|
||||
rendererName: "Table", // Default view as Table
|
||||
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
plotlyRenderers // Adding Plotly renderers
|
||||
),
|
||||
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (
|
||||
currentRenderer === "Table" ||
|
||||
currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" ||
|
||||
currentRenderer === "Row Heatmap" ||
|
||||
currentRenderer === "Col Heatmap"
|
||||
) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
|
||||
// ✅ Hide fields from the dropdown but keep them visible in the table
|
||||
let hiddenFields = ["Allowance Amount", "Deduction Amount"];
|
||||
|
||||
setTimeout(function () {
|
||||
$(".pvtAttrDropdown option").each(function () {
|
||||
if (hiddenFields.includes($(this).text())) {
|
||||
$(this).remove(); // Remove from selection
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
},
|
||||
});
|
||||
|
||||
window.loadFilteredPivotData =function loadFilteredPivotData() {
|
||||
const selectedModel = $("#model-select").val();
|
||||
const formData = $("#filterForm").serialize();
|
||||
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (model === "payslip") {
|
||||
containerId = "pivot-payslip";
|
||||
rowsConfig = ["Employee","Basic Salary","Gross Pay","Net Pay","Status"];
|
||||
} else if (model === "allowance") {
|
||||
containerId = "pivot-allowance";
|
||||
rowsConfig = ["Employee","Allowance & Deduction","Allowance & Deduction Title","Allowance & Deduction Amount"];
|
||||
}
|
||||
|
||||
$("#" + containerId).show();
|
||||
|
||||
$.getJSON(`payroll-pivot?model=${selectedModel}&${formData}`, function (data) {
|
||||
|
||||
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table",
|
||||
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
|
||||
onRefresh: function (config) {
|
||||
const currentRenderer = config.rendererName;
|
||||
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
|
||||
$("#export-btn").show();
|
||||
} else {
|
||||
$("#export-btn").hide();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
|
||||
|
||||
if (visiblePivot.length) {
|
||||
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Export Function
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3; // Leave some rows before the table
|
||||
|
||||
// ------------------------
|
||||
// Render pivot table
|
||||
// ------------------------
|
||||
const cellMap = {};
|
||||
const allRows = Array.from(table.rows);
|
||||
const lastRowIndex = allRows.length - 1;
|
||||
|
||||
allRows.forEach((row, rowIndex) => {
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
const isHeader = rowIndex === 0;
|
||||
const isLastRow = rowIndex === lastRowIndex;
|
||||
|
||||
excelCell.font = {
|
||||
bold: isHeader || isLastRow,
|
||||
size: isHeader ? 12 : 11,
|
||||
color: {
|
||||
argb: isHeader ? 'FFFFFFFF' :
|
||||
isLastRow ? 'FF000000' :
|
||||
'FF000000'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: {
|
||||
argb: isHeader ? 'FF545454' :
|
||||
isLastRow ? 'FFFFE599' : // light yellow
|
||||
'FFF5F5F5'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
// Merge
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
|
||||
|
||||
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
|
||||
|
||||
// Auto-adjust column widths
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 2;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 3;
|
||||
});
|
||||
|
||||
// Save
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Initial load with all models
|
||||
loadPivotData("payslip");
|
||||
|
||||
// Model selection change event
|
||||
$("#model-select").on("change", function () {
|
||||
let selectedModel = $(this).val();
|
||||
loadPivotData(selectedModel); // Reload pivot data with selected model
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
530
horilla_theme/templates/report/pms_report.html
Normal file
530
horilla_theme/templates/report/pms_report.html
Normal file
@@ -0,0 +1,530 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Performance Reports" %}
|
||||
</h3>
|
||||
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Objective" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for=""
|
||||
>{% trans "Managers" %}</label
|
||||
>
|
||||
{{objective_filer_form.managers}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Key Result" %}</label>
|
||||
{{objective_filer_form.employee_objective__key_result_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Assignees" %}</label>
|
||||
{{objective_filer_form.assignees}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Duration" %} </label>
|
||||
{{objective_filer_form.duration}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Employee Objective" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for=""
|
||||
>{% trans "Employees" %}</label
|
||||
>
|
||||
{{emp_obj_form.form.employee_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Start Date From" %}</label>
|
||||
{{emp_obj_form.form.start_date_from}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Key Result" %}</label>
|
||||
{{emp_obj_form.form.key_result_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Start Date Till" %} </label>
|
||||
{{emp_obj_form.form.start_date_till}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for=""
|
||||
>{% trans "End Date From" %}</label
|
||||
>
|
||||
{{emp_obj_form.form.end_date_from}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "Status" %}</label>
|
||||
{{emp_obj_form.form.status}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="">{% trans "End Date Till" %}</label>
|
||||
{{emp_obj_form.form.end_date_till}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Feedback" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.review_cycle.id_for_label}}">{% trans "Feedback Title" %}</label>
|
||||
{{feedback_filter_form.review_cycle}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.status.id_for_label}}">{% trans "Status" %} </label>
|
||||
{{feedback_filter_form.status}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.employee_id.id_for_label}}">{% trans "Employee" %}</label>
|
||||
{{feedback_filter_form.employee_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.manager_id.id_for_label}}">{% trans "Manager" %} </label>
|
||||
{{feedback_filter_form.manager_id}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.colleague_id.id_for_label}}">{% trans "Colleague" %} </label>
|
||||
{{feedback_filter_form.colleague_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.start_date.id_for_label}}">{% trans "Start Date" %}</label>
|
||||
{{feedback_filter_form.start_date}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.subordinate_id.id_for_label}}">{% trans "Subordinate" %}</label>
|
||||
{{feedback_filter_form.subordinate_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="{{feedback_filter_form.end_date.id_for_label}}">{% trans "End Date" %}</label>
|
||||
{{feedback_filter_form.end_date}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection Dropdown -->
|
||||
<div style="width: 350px; display:flex;">
|
||||
<span class="mt-1 text-md font-semibold" style="font-size:16px; width:200px;">{% trans "Choose Report" %} : </span>
|
||||
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
|
||||
<option value="objective">{% trans "Objectives" %}</option>
|
||||
<option value="employeeobjective">{% trans "Employee Objective" %}</option>
|
||||
<option value="feedback">{% trans "Feedback" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
|
||||
<div id="pivot-feedback" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
<div id="pivot-objective" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
<div id="pivot-employeeobjective" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
$(function () {
|
||||
|
||||
function loadPivotData(model) {
|
||||
// Hide all containers first
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
// Determine current container and row config
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (model === "feedback") {
|
||||
containerId = "pivot-feedback";
|
||||
rowsConfig = ["Title","Employee","Manager","Answerable Employees","Answered Employees","Questions","Answer"];
|
||||
} else if (model === "objective") {
|
||||
containerId = "pivot-objective";
|
||||
rowsConfig = ["Objective","Key Results","Manager","Assignees"];
|
||||
} else if (model === "employeeobjective") {
|
||||
containerId = "pivot-employeeobjective";
|
||||
rowsConfig = ["Employee", "Objective","Employee Keyresult","Keyresult Target Value","Keyresult Current Value"];
|
||||
}
|
||||
|
||||
// Show relevant container
|
||||
$("#" + containerId).show();
|
||||
|
||||
// Fetch and render data in its own container
|
||||
$.getJSON(`pms-pivot?model=${model}`, function (data) {
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table",
|
||||
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
|
||||
onRefresh: function (config) {
|
||||
const currentRenderer = config.rendererName;
|
||||
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
|
||||
$("#export-btn").show();
|
||||
} else {
|
||||
$("#export-btn").hide();
|
||||
}
|
||||
|
||||
$(".pvtTotal, .pvtTotalLabel, .pvtGrandTotal, .pvtAggregator").hide();
|
||||
|
||||
setTimeout(() => {
|
||||
$(".pvtAttrDropdown option").each(function () {
|
||||
if (hiddenFields.includes($(this).text())) {
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
window.loadFilteredPivotData =function loadFilteredPivotData() {
|
||||
const selectedModel = $("#model-select").val();
|
||||
const formData = $("#filterForm").serialize();
|
||||
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (selectedModel === "feedback") {
|
||||
containerId = "pivot-feedback";
|
||||
rowsConfig = ["Title","Employee", "Manager", "Answerable Employees", "Answered Employees", "Questions", "Answer"];
|
||||
} else if (selectedModel === "objective") {
|
||||
containerId = "pivot-objective";
|
||||
rowsConfig = ["Objective", "Key Results", "Manager", "Assignees"];
|
||||
} else if (selectedModel === "employeeobjective") {
|
||||
containerId = "pivot-employeeobjective";
|
||||
rowsConfig = ["Employee", "Objective", "Employee Keyresult", "Keyresult Target Value", "Keyresult Current Value"];
|
||||
}
|
||||
|
||||
$("#" + containerId).show();
|
||||
|
||||
$.getJSON(`pms-pivot?model=${selectedModel}&${formData}`, function (data) {
|
||||
|
||||
console.log("Filtered data:", data);
|
||||
|
||||
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table",
|
||||
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
|
||||
onRefresh: function (config) {
|
||||
const currentRenderer = config.rendererName;
|
||||
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
|
||||
$("#export-btn").show();
|
||||
} else {
|
||||
$("#export-btn").hide();
|
||||
}
|
||||
|
||||
$(".pvtTotal, .pvtTotalLabel, .pvtGrandTotal, .pvtAggregator").hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Listen to dropdown
|
||||
$("#model-select").on("change", function () {
|
||||
let selectedModel = $(this).val();
|
||||
loadPivotData(selectedModel);
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadPivotData("objective");
|
||||
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
|
||||
|
||||
if (visiblePivot.length) {
|
||||
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
|
||||
}
|
||||
});
|
||||
|
||||
// Export Function
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3; // Leave some rows before the table
|
||||
|
||||
// ------------------------
|
||||
// Render pivot table
|
||||
// ------------------------
|
||||
const cellMap = {};
|
||||
Array.from(table.rows).forEach((row, rowIndex) => {
|
||||
if (
|
||||
row.classList.contains("pvtTotal") ||
|
||||
row.classList.contains("pvtGrandTotal")
|
||||
) return;
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
if (
|
||||
cell.classList.contains("pvtTotal") ||
|
||||
cell.classList.contains("pvtTotalLabel") ||
|
||||
cell.classList.contains("pvtAggregator") ||
|
||||
cell.classList.contains("pvtGrandTotal")
|
||||
) return;
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
excelCell.font = {
|
||||
bold: rowIndex === 0,
|
||||
size: rowIndex === 0 ? 12 : 11,
|
||||
color: { argb: rowIndex === 0 ? 'FFFFFFFF' : 'FF000000' }
|
||||
};
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: rowIndex === 0 ? 'FF545454' : 'FFF5F5F5' }
|
||||
};
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
// Merge
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
// Header row height
|
||||
worksheet.getRow(currentRow).height = 30;
|
||||
|
||||
// Auto-adjust column widths
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 5;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 0;
|
||||
});
|
||||
|
||||
// Save
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
598
horilla_theme/templates/report/recruitment_report.html
Normal file
598
horilla_theme/templates/report/recruitment_report.html
Normal file
@@ -0,0 +1,598 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="oh-wrapper mt-3">
|
||||
|
||||
<div class="d-flex mt-3 mb-3" style='justify-content: space-between;
|
||||
align-items: center;'>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{% trans "Candidate Reports" %}
|
||||
</h3>
|
||||
<div style="display:inline-flex;">
|
||||
<!-- Filter section -->
|
||||
<form id="filterForm" onsubmit="event.preventDefault(); loadFilteredPivotData();" class="me-3" style="margin-top: 10px;">
|
||||
|
||||
<div class="oh-main__titlebar oh-main__titlebar--right">
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="px-5 py-2 h-full bg-[white] rounded-md text-xs flex items-center gap-2 border border-primary-500 hover:border-primary-600 transition duration-300 cursor-pointer"
|
||||
@click="open = !open"
|
||||
onclick="event.preventDefault()"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15" class="mt-[-1px]">
|
||||
{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
style="display: none; width: 395px;"
|
||||
>
|
||||
<div class="oh-dropdown__filter-body">
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Candidate" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Name' %}</label>
|
||||
{{ f.form.candidate_name }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Phone' %}</label>
|
||||
{{ f.form.mobile }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Country' %}</label>
|
||||
<select name="country" class="oh-select-2 w-100 country" id="country">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Recruitment' %}</label>
|
||||
{{ f.form.recruitment_id }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Job Position' %}</label>
|
||||
{{ f.form.job_position_id }}
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Is Hired' %}?</label>
|
||||
{{ f.form.hired }}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 mb-2">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Offer Status" %}</label>
|
||||
{{f.form.offer_letter_status}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Email' %}</label>
|
||||
{{ f.form.email }}
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Gender' %}</label>
|
||||
{{ f.form.gender }}
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'State' %}</label>
|
||||
<select name="state" class="oh-select-2 w-100 country" id="state">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Department' %}</label>
|
||||
{{ f.form.job_position_id__department_id }}
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Stage Type' %}</label>
|
||||
{{ f.form.stage_id__stage_type }}
|
||||
</div>
|
||||
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Is Canceled' %}?</label>
|
||||
{{ f.form.canceled }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Recruitment" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Recruitment" %}</label>
|
||||
{{fr.form.title}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Managers" %}</label>
|
||||
{{fr.form.recruitment_managers}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Start Date" %}</label>
|
||||
{{fr.form.start_date}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Is Closed" %}</label>
|
||||
<select name="closed" id="closed" class="oh-select oh-select-2 w-100">
|
||||
<option value="unknown">{% trans "Unknown" %}</option>
|
||||
<option value="true">{% trans "True" %}</option>
|
||||
<option value="false" selected>{% trans "False" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Job Position" %}</label>
|
||||
{{fr.form.open_positions}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Company" %}</label>
|
||||
{{fr.form.company_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "End Date" %}</label>
|
||||
{{fr.form.end_date}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Is Published" %}</label>
|
||||
{{fr.form.is_published}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Start Date From" %}</label>
|
||||
{{fr.form.start_from}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Is Active" %}?</label>
|
||||
{{fr.form.is_active}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Till End Date" %}</label>
|
||||
{{fr.form.end_till}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oh-accordion">
|
||||
<div class="oh-accordion-header">{% trans "Onboarding" %}</div>
|
||||
<div class="oh-accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans "Recruitment" %}</label>
|
||||
{{fo.form.recruitment_id}}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Stage' %}</label>
|
||||
{{ fo.form.stage_title }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Task' %}</label>
|
||||
{{ fo.form.onboarding_task__task_title }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Company' %}</label>
|
||||
{{ fo.form.recruitment_id__company_id }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Candidates' %}</label>
|
||||
{{ fo.form.onboarding_task__candidates }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Stage Manager' %}</label>
|
||||
{{ fo.form.employee_id }}
|
||||
</div>
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label">{% trans 'Task Manager' %}</label>
|
||||
{{ fo.form.onboarding_task__employee_id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100 filterButton"
|
||||
id="objective-filter-form-submit"
|
||||
onclick="loadFilteredPivotData();"
|
||||
>
|
||||
<img src="{% static 'horilla_theme/assets/img/icons/sort.svg' %}" alt="{% trans 'Filter' %}" width="15"
|
||||
class="mt-[-1px]">{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Export Button -->
|
||||
<button id="export-btn" class="oh-btn oh-btn--secondary" style="margin-top: 10px;">
|
||||
<ion-icon name="download-outline" class="mr-1 md hydrated" role="img" aria-label="download"></ion-icon>
|
||||
{% trans "Export Table" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection Dropdown -->
|
||||
<div style="width: 350px; display:flex;">
|
||||
<span class="mt-1 text-md font-semibold" style="font-size:16px; width:200px;">{% trans "Choose Report" %} : </span>
|
||||
<select id="model-select" class="oh-select oh-select--sm ml-2 mb-2" style="width: 200px;">
|
||||
<option value="candidate">{% trans "Candidate" %}</option>
|
||||
<option value="recruitment">{% trans "Recruitment" %}</option>
|
||||
<option value="onboarding">{% trans "Onboarding" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pivot Container -->
|
||||
<div id="pivot-container" class="mb-5" style="width: 100%; overflow-x: auto;">
|
||||
<div id="pivot-candidate" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
<div id="pivot-recruitment" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
<div id="pivot-onboarding" class="pivot-wrapper" style="display:none;width: 100%; overflow-x: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
// Function to load pivot data dynamically
|
||||
function loadPivotData(model) {
|
||||
let url = `recruitment-pivot?model=${model}`;
|
||||
|
||||
// Hide all containers first
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
// Determine current container and row config
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (model === "candidate") {
|
||||
containerId = "pivot-candidate";
|
||||
rowsConfig = ["Recruitment","Job Position","Department","Candidate","Gender","Email"];
|
||||
} else if (model === "recruitment") {
|
||||
containerId = "pivot-recruitment";
|
||||
rowsConfig = ["Recruitment","Vacancy","Manager","Job Position","Start Date","End Date"];
|
||||
} else if (model === "onboarding") {
|
||||
containerId = "pivot-onboarding";
|
||||
rowsConfig = ["Recruitment","Candidates","Stage","Stage Manager","Task","Task Manager","Company"];
|
||||
}
|
||||
|
||||
// Show relevant container
|
||||
$("#" + containerId).show();
|
||||
|
||||
$.getJSON(url, function (data) {
|
||||
// Add Plotly renderers correctly
|
||||
let plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
// Initialize pivot table with Plotly enabled
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table", // Default view as Table
|
||||
onRefresh: function (config) {
|
||||
let currentRenderer = config.rendererName;
|
||||
if (currentRenderer === "Table" || currentRenderer === "Table Barchart" ||
|
||||
currentRenderer === "Heatmap" || currentRenderer === "Row Heatmap" || currentRenderer === "Col Heatmap" ) {
|
||||
$("#export-btn").show(); // Show button for tables
|
||||
} else {
|
||||
$("#export-btn").hide(); // Hide button for charts
|
||||
}
|
||||
// Hide fields from dropdown but keep them available in the table
|
||||
let hiddenFields = [
|
||||
"Vacancy",
|
||||
];
|
||||
|
||||
$(".pvtTotal, .pvtTotalLabel, .pvtGrandTotal, .pvtAggregator").hide();
|
||||
|
||||
setTimeout(function () {
|
||||
$(".pvtAttrDropdown option").each(function () {
|
||||
if (hiddenFields.includes($(this).text())) {
|
||||
$(this).remove(); // Remove from selection
|
||||
}
|
||||
});
|
||||
}, 10);
|
||||
},
|
||||
renderers: $.extend(
|
||||
$.pivotUtilities.renderers,
|
||||
plotlyRenderers // Adding Plotly renderers
|
||||
)
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
window.loadFilteredPivotData =function loadFilteredPivotData() {
|
||||
const selectedModel = $("#model-select").val();
|
||||
const formData = $("#filterForm").serialize();
|
||||
|
||||
$(".pivot-wrapper").hide();
|
||||
|
||||
let containerId = "";
|
||||
let rowsConfig = [];
|
||||
|
||||
if (selectedModel === "candidate") {
|
||||
containerId = "pivot-candidate";
|
||||
rowsConfig = ["Recruitment","Job Position","Department","Candidate","Gender","Email"];
|
||||
} else if (selectedModel === "recruitment") {
|
||||
containerId = "pivot-recruitment";
|
||||
rowsConfig = ["Recruitment","Vacancy","Manager","Job Position","Start Date","End Date"];
|
||||
} else if (selectedModel === "onboarding") {
|
||||
containerId = "pivot-onboarding";
|
||||
rowsConfig = ["Recruitment","Candidates","Stage","Stage Manager","Task","Task Manager","Company"];
|
||||
}
|
||||
|
||||
$("#" + containerId).show();
|
||||
|
||||
$.getJSON(`recruitment-pivot?model=${selectedModel}&${formData}`, function (data) {
|
||||
|
||||
const plotlyRenderers = $.pivotUtilities.plotly_renderers;
|
||||
|
||||
$("#" + containerId).pivotUI(data, {
|
||||
rows: rowsConfig,
|
||||
cols: [],
|
||||
aggregatorName: "Count",
|
||||
rendererName: "Table",
|
||||
renderers: $.extend($.pivotUtilities.renderers, plotlyRenderers),
|
||||
onRefresh: function (config) {
|
||||
const currentRenderer = config.rendererName;
|
||||
if (["Table", "Table Barchart", "Heatmap", "Row Heatmap", "Col Heatmap"].includes(currentRenderer)) {
|
||||
$("#export-btn").show();
|
||||
} else {
|
||||
$("#export-btn").hide();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initial load with all models
|
||||
loadPivotData("candidate");
|
||||
|
||||
// Model selection change event
|
||||
$("#model-select").on("change", function () {
|
||||
let selectedModel = $(this).val();
|
||||
loadPivotData(selectedModel); // Reload pivot data with selected model
|
||||
});
|
||||
|
||||
// Export to Excel on button click
|
||||
$("#export-btn").on("click", function () {
|
||||
let visiblePivot = $(".pivot-wrapper:visible .pvtTable").closest(".pivot-wrapper");
|
||||
|
||||
if (visiblePivot.length) {
|
||||
exportTableToExcel(visiblePivot.attr("id"), "pivot_report.xlsx");
|
||||
}
|
||||
});
|
||||
|
||||
// Export Function
|
||||
async function exportTableToExcel(containerId, filename) {
|
||||
let table = document.querySelector(`#${containerId} .pvtTable`);
|
||||
if (!table) {
|
||||
alert("No table found to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Pivot Data");
|
||||
const baseRow = 5;
|
||||
const baseCol = 5;
|
||||
|
||||
let currentRow = baseRow;
|
||||
|
||||
// Add company details first (if not 'all')
|
||||
if ('{{company}}' !== 'all') {
|
||||
const companyDetails = {
|
||||
name: "{{ company.company|escapejs }}",
|
||||
address: "{{ company.address|escapejs }}",
|
||||
country: "{{ company.country|escapejs }}",
|
||||
state: "{{ company.state|escapejs }}",
|
||||
city: "{{ company.city|escapejs }}",
|
||||
zip: "{{ company.zip|escapejs }}"
|
||||
};
|
||||
|
||||
function getBase64FromUrl(url) {
|
||||
return fetch(url)
|
||||
.then(response => response.blob())
|
||||
.then(blob => new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
}));
|
||||
}
|
||||
|
||||
const logoUrl = "{{ protocol }}://{{ host }}{{ company.icon.url }}";
|
||||
await getBase64FromUrl(logoUrl).then((base64) => {
|
||||
const base64Data = base64.split(',')[1];
|
||||
const imageId = workbook.addImage({
|
||||
base64: base64Data,
|
||||
extension: 'png'
|
||||
});
|
||||
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: baseCol - 1, row: currentRow - 1 },
|
||||
ext: { width: 80, height: 80 }
|
||||
});
|
||||
});
|
||||
|
||||
// Merge cells for company details text
|
||||
const companyTextCell = worksheet.getCell(currentRow, baseCol + 1);
|
||||
worksheet.mergeCells(currentRow, baseCol + 1, currentRow, baseCol + 2);
|
||||
companyTextCell.value = {
|
||||
richText: [
|
||||
{ text: `\n${companyDetails.name}\n`, font: { size: 14, bold: true, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.address}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `${companyDetails.country}, ${companyDetails.state}, ${companyDetails.city}\n`, font: { size: 11, color: { argb: 'FF333333' } } },
|
||||
{ text: `ZIP: ${companyDetails.zip}`, font: { size: 11, color: { argb: 'FF333333' } } }
|
||||
]
|
||||
};
|
||||
companyTextCell.alignment = {
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
wrapText: true
|
||||
};
|
||||
worksheet.getRow(currentRow).height = 80;
|
||||
|
||||
currentRow += 2; // Leave a blank row
|
||||
}
|
||||
|
||||
// Add timestamp
|
||||
const timestamp = new Date().toLocaleDateString('en-GB') + ' ' +
|
||||
new Date().toLocaleTimeString('en-US', {
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true
|
||||
});
|
||||
|
||||
const downloadCell = worksheet.getCell(currentRow, baseCol);
|
||||
worksheet.mergeCells(currentRow, baseCol, currentRow, baseCol + 3);
|
||||
downloadCell.value = `Generated on: ${timestamp}`;
|
||||
downloadCell.alignment = { horizontal: 'left', vertical: 'middle', wrapText: true };
|
||||
downloadCell.font = { size: 10, italic: true, color: { argb: 'FF666666' }, bold: true };
|
||||
|
||||
currentRow += 3; // Leave some rows before the table
|
||||
|
||||
// ------------------------
|
||||
// Render pivot table
|
||||
// ------------------------
|
||||
const cellMap = {};
|
||||
const allRows = Array.from(table.rows);
|
||||
const lastRowIndex = allRows.length - 1;
|
||||
|
||||
allRows.forEach((row, rowIndex) => {
|
||||
|
||||
let colIndex = baseCol;
|
||||
|
||||
Array.from(row.cells).forEach((cell) => {
|
||||
|
||||
if (
|
||||
cell.classList.contains("pvtTotal") ||
|
||||
cell.classList.contains("pvtTotalLabel") ||
|
||||
cell.classList.contains("pvtAggregator") ||
|
||||
cell.classList.contains("pvtGrandTotal")
|
||||
) return;
|
||||
|
||||
while (cellMap[`${currentRow + rowIndex}-${colIndex}`]) {
|
||||
colIndex++;
|
||||
}
|
||||
|
||||
const rowspan = parseInt(cell.getAttribute("rowspan")) || 1;
|
||||
const colspan = parseInt(cell.getAttribute("colspan")) || 1;
|
||||
const cellValue = cell.textContent.trim();
|
||||
|
||||
const excelCell = worksheet.getCell(currentRow + rowIndex, colIndex);
|
||||
excelCell.value = cellValue;
|
||||
|
||||
const isHeader = rowIndex === 0;
|
||||
const isLastRow = rowIndex === lastRowIndex;
|
||||
|
||||
excelCell.font = {
|
||||
bold: isHeader || isLastRow,
|
||||
size: isHeader ? 12 : 11,
|
||||
color: {
|
||||
argb: isHeader ? 'FFFFFFFF' :
|
||||
isLastRow ? 'FF000000' :
|
||||
'FF000000'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: {
|
||||
argb: isHeader ? 'FF545454' :
|
||||
isLastRow ? 'FFFFE599' : // light yellow
|
||||
'FFF5F5F5'
|
||||
}
|
||||
};
|
||||
|
||||
excelCell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
excelCell.alignment = { horizontal: "center", vertical: "middle" };
|
||||
|
||||
// Merge
|
||||
if (rowspan > 1 || colspan > 1) {
|
||||
worksheet.mergeCells(
|
||||
currentRow + rowIndex,
|
||||
colIndex,
|
||||
currentRow + rowIndex + rowspan - 1,
|
||||
colIndex + colspan - 1
|
||||
);
|
||||
|
||||
for (let r = 0; r < rowspan; r++) {
|
||||
for (let c = 0; c < colspan; c++) {
|
||||
cellMap[`${currentRow + rowIndex + r}-${colIndex + c}`] = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cellMap[`${currentRow + rowIndex}-${colIndex}`] = true;
|
||||
}
|
||||
|
||||
colIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
worksheet.getRow(currentRow + lastRowIndex).height = 25; // adjust height for Total
|
||||
|
||||
worksheet.getRow(currentRow).height = 30; // adjust height for Heading
|
||||
|
||||
// Auto-adjust column widths
|
||||
worksheet.columns.forEach(column => {
|
||||
let maxLength = 2;
|
||||
column.eachCell({ includeEmpty: true }, cell => {
|
||||
const value = cell.value ? cell.value.toString() : '';
|
||||
maxLength = Math.max(maxLength, value.length);
|
||||
});
|
||||
column.width = maxLength + 3;
|
||||
});
|
||||
|
||||
// Save
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
});
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
@@ -59,13 +59,13 @@
|
||||
{% endif %}
|
||||
{% if perms.recruitment.delete_recruitmentsurvey %}
|
||||
<li>
|
||||
<a href="{% url 'recruitment-survey-question-template-delete' question.id %}"
|
||||
<a href="{% url 'recruitment-survey-question-template-delete' question.id %}"
|
||||
onclick="
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopPropagation();
|
||||
confirm('{% trans "Are you sure want to delete?" %}')
|
||||
"
|
||||
class="text-danger w-full text-left px-3 py-2"
|
||||
class="text-danger w-full text-left px-3 py-2"
|
||||
>
|
||||
{% trans "Delete" %}
|
||||
</a>
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="mr-2">
|
||||
<h5 class="mb-1 font-medium text-sm">{{question}}</h5>
|
||||
<p class="text-xs text-[#565E6C] mb-1">
|
||||
{% for rec in question.recruitment_ids.all %} {{rec}},
|
||||
{% for rec in question.recruitment_ids.all %} {{rec}},
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@
|
||||
hx-get="{% url 'rec-filter-survey' %}?{{pd}}"
|
||||
hx-target="#view-container"
|
||||
min="1"
|
||||
/> / {{ questions.paginator.num_pages }}
|
||||
/> / {{ questions.paginator.num_pages }}
|
||||
</div>
|
||||
{% if questions.has_next %}
|
||||
<button
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
$(".oh-tabs__tab").on("click", function (e) {
|
||||
var dataTarget = $(this).data('target');
|
||||
|
||||
|
||||
$(".oh-tabs__content").hide();
|
||||
$(this).addClass("bg-primary-300");
|
||||
$(".oh-tabs__tab").not(this).removeClass("bg-primary-300");
|
||||
@@ -161,4 +161,4 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
@@ -160,7 +160,7 @@
|
||||
hx-get="{% url 'rec-filter-survey' %}?{{pd}}"
|
||||
hx-target="#view-container"
|
||||
min="1"
|
||||
/> / {{ grouper.list.paginator.num_pages }}
|
||||
/> / {{ grouper.list.paginator.num_pages }}
|
||||
</div>
|
||||
{% if grouper.list.has_next %}
|
||||
<button
|
||||
@@ -192,7 +192,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% if templates.has_next or templates.has_previous %}
|
||||
{% if templates.paginator.count %}
|
||||
<div
|
||||
@@ -218,7 +218,7 @@
|
||||
hx-get="{% url 'rec-filter-survey' %}?{{pd}}"
|
||||
hx-target="#view-container"
|
||||
min="1"
|
||||
/> / {{ templates.paginator.num_pages }}
|
||||
/> / {{ templates.paginator.num_pages }}
|
||||
</div>
|
||||
{% if templates.has_next %}
|
||||
<button
|
||||
@@ -241,4 +241,4 @@
|
||||
<p class="oh-empty__subtitle">{% trans "No template groups have been established yet." %} </p>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -63,13 +63,13 @@
|
||||
<h3 class="text-lg font-semibold">{% trans "Survey Templates" %}</h3>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div class="relative">
|
||||
<input
|
||||
<input
|
||||
class="text-color-600 ps-8 p-1.5 pb-2 placeholder:text-xs w-full border border-dark-50 rounded-md focus-visible:outline-0 placeholder:text-dark-100 text-sm [transition:.3s] focus:border-primary-600"
|
||||
type="text"
|
||||
name="question"
|
||||
name="question"
|
||||
aria-label="Search Input"
|
||||
placeholder="{% trans 'Search' %}"
|
||||
hx-get="{% url 'rec-filter-survey' %}"
|
||||
placeholder="{% trans 'Search' %}"
|
||||
hx-get="{% url 'rec-filter-survey' %}"
|
||||
hx-target="#view-container"
|
||||
hx-trigger="keyup changed delay:.2s"
|
||||
>
|
||||
@@ -101,12 +101,12 @@
|
||||
|
||||
<div class="relative inline-block dropdown-wrapper oh-tabs__tab" data-target="#templateTab">
|
||||
<div class="oh-dropdown" x-data="{open: false}" onclick=>
|
||||
<div class="tab-button px-2 py-1 border rounded-md text-xs flex items-center gap-2 hover:border-primary-600 transition duration-300 text-primary-600 border-primary-600"
|
||||
<div class="tab-button px-2 py-1 border rounded-md text-xs flex items-center gap-2 hover:border-primary-600 transition duration-300 text-primary-600 border-primary-600"
|
||||
data-tab="tab1"
|
||||
>
|
||||
{% trans "Template" %}
|
||||
<button class="ps-3 cursor-pointer"
|
||||
@click="open = !open"
|
||||
<button class="ps-3 cursor-pointer"
|
||||
@click="open = !open"
|
||||
@click.outside="open = false"
|
||||
onclick="event.stopPropagation()"
|
||||
>
|
||||
@@ -122,10 +122,10 @@
|
||||
<a class="oh-dropdown__item my-0"
|
||||
role="button"
|
||||
onclick="event.stopPropagation()"
|
||||
hx-get="{% url 'survey-template-create' %}"
|
||||
hx-target="#genericModalBody"
|
||||
hx-get="{% url 'survey-template-create' %}"
|
||||
hx-target="#genericModalBody"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#genericModal"
|
||||
data-target="#genericModal"
|
||||
>{% trans "Add Template" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -135,12 +135,12 @@
|
||||
|
||||
<div class="relative inline-block dropdown-wrapper oh-tabs__tab" data-target="#questionTab">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<div class="tab-button px-2 py-1 border rounded-md text-xs flex items-center gap-2 hover:border-primary-600 transition duration-300 text-primary-600 border-primary-600"
|
||||
<div class="tab-button px-2 py-1 border rounded-md text-xs flex items-center gap-2 hover:border-primary-600 transition duration-300 text-primary-600 border-primary-600"
|
||||
data-tab="tab1"
|
||||
>
|
||||
{% trans "Questions" %}
|
||||
<button class="ps-3 cursor-pointer"
|
||||
@click="open = !open"
|
||||
<button class="ps-3 cursor-pointer"
|
||||
@click="open = !open"
|
||||
@click.outside="open = false"
|
||||
onclick="event.stopPropagation()"
|
||||
>
|
||||
@@ -156,7 +156,7 @@
|
||||
<a class="oh-dropdown__item my-0"
|
||||
role="button"
|
||||
onclick="event.stopPropagation()"
|
||||
hx-get="{% url 'recruitment-survey-question-template-create' %}"
|
||||
hx-get="{% url 'recruitment-survey-question-template-create' %}"
|
||||
hx-target="#genericModalBody"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#genericModal"
|
||||
@@ -168,7 +168,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="oh-tabs__contents" id="view-container">
|
||||
{% include "survey/survey_card.html" %}
|
||||
</div>
|
||||
@@ -185,4 +185,4 @@
|
||||
<div class="oh-modal__dialog-body" id="templateModalBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<div class="overflow-hidden overflow-y-auto">
|
||||
<div class="col-12">{{form.non_field_errors}}</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 pb-5 sort-container">
|
||||
{% for question in questions %}
|
||||
{% for question in questions %}
|
||||
<div class="bg-white rounded-md shadow-card p-5" data-question-id="{{question.id}}">
|
||||
<div class="d-block w-100">
|
||||
<p class="font-semibold text-sm mb-3">
|
||||
{{ question.question }}
|
||||
{{ question.question }}
|
||||
{% if question.is_mandatory %}
|
||||
<span class="text-danger">
|
||||
* </span>
|
||||
@@ -66,14 +66,14 @@
|
||||
/> %
|
||||
</div> {% endcomment %}
|
||||
<div class="w-full">
|
||||
<input
|
||||
<input
|
||||
class="w-full h-2 bg-gray-200 rounded-lg cursor-pointer percentageSlider"
|
||||
type="range"
|
||||
id="id_{{ question.id }}"
|
||||
min="0"
|
||||
max="100"
|
||||
type="range"
|
||||
id="id_{{ question.id }}"
|
||||
min="0"
|
||||
max="100"
|
||||
value="50"
|
||||
|
||||
|
||||
/>
|
||||
<div class="flex justify-between text-sm text-gray-600 mt-2">
|
||||
<span>0%</span>
|
||||
@@ -135,31 +135,31 @@
|
||||
<div class="flex flex-wrap gap-4 pt-2">
|
||||
<!-- Yes Option -->
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="yes"
|
||||
class="red-radio"
|
||||
type="radio"
|
||||
name="{{ question.question }}"
|
||||
<input
|
||||
id="yes"
|
||||
class="red-radio"
|
||||
type="radio"
|
||||
name="{{ question.question }}"
|
||||
value="yes"
|
||||
checked
|
||||
checked
|
||||
/>
|
||||
<label
|
||||
class="cursor-pointer text-sm"
|
||||
<label
|
||||
class="cursor-pointer text-sm"
|
||||
for="yes"
|
||||
>{% trans "Yes" %}</label>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="red-radio"
|
||||
type="radio"
|
||||
id="no"
|
||||
name="{{ question.question }}"
|
||||
value="no"
|
||||
<input
|
||||
class="red-radio"
|
||||
type="radio"
|
||||
id="no"
|
||||
name="{{ question.question }}"
|
||||
value="no"
|
||||
/>
|
||||
<label
|
||||
class="cursor-pointer text-sm"
|
||||
<label
|
||||
class="cursor-pointer text-sm"
|
||||
for="no"
|
||||
>{% trans "No" %}</label>
|
||||
</div>
|
||||
@@ -465,7 +465,7 @@
|
||||
{% if question.is_mandatory %} required {% endif %}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endcomment %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -488,7 +488,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
|
||||
$(function () {
|
||||
$('.percentageSlider').on('input', function () {
|
||||
$('#sliderValue').text($(this).val() + '%');
|
||||
|
||||
Reference in New Issue
Block a user