[UPDT] HORILLA_THEME: Added the style for Report module

This commit is contained in:
Horilla
2025-07-08 15:27:16 +05:30
parent 1182421378
commit 9fc11872dd
13 changed files with 3897 additions and 54 deletions

View File

@@ -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;
}

View File

@@ -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]">

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@@ -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}},&nbsp;
{% for rec in question.recruitment_ids.all %} {{rec}},&nbsp;
{% 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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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() + '%');