Merge branch 'production'

This commit is contained in:
Dobromir Popov
2025-01-08 10:42:37 +02:00
3 changed files with 154 additions and 15 deletions

7
package-lock.json generated
View File

@ -37,6 +37,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-jwt": "^8.4.1", "express-jwt": "^8.4.1",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"file-saver": "^2.0.5",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"gapi": "^0.0.3", "gapi": "^0.0.3",
"gapi-script": "^1.2.0", "gapi-script": "^1.2.0",
@ -7906,6 +7907,12 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/file-stream-rotator": { "node_modules/file-stream-rotator": {
"version": "0.6.1", "version": "0.6.1",
"license": "MIT", "license": "MIT",

View File

@ -55,6 +55,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-jwt": "^8.4.1", "express-jwt": "^8.4.1",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"file-saver": "^2.0.5",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"gapi": "^0.0.3", "gapi": "^0.0.3",
"gapi-script": "^1.2.0", "gapi-script": "^1.2.0",

View File

@ -23,6 +23,10 @@ import ConfirmationModal from '../../../components/ConfirmationModal';
import { relative } from "path"; import { relative } from "path";
import { set } from "lodash"; import { set } from "lodash";
import { utils, write } from 'xlsx';
import { saveAs } from 'file-saver';
interface IProps { interface IProps {
initialItems: Publisher[]; initialItems: Publisher[];
} }
@ -38,20 +42,6 @@ function PublishersPage({ publishers = [] }: IProps) {
indeterminate: true, indeterminate: true,
}); });
// const cbRefFilterTraining = useRef<HTMLInputElement>(null);
// const getCheckboxState = (currentState: boolean | null) => {
// if (currentState === true) return 'unchecked';
// if (currentState === false) return 'checked';
// return 'indeterminate';
// };
// const cbRefFilterTraining = useRef<HTMLInputElement>(null);
// const [cbFilterTrainingState, setcbFilterTrainingState] = useState<boolean | null>(null);
// useEffect(() => {
// if (cbRefFilterTraining.current) {
// cbRefFilterTraining.current.indeterminate = cbFilterTrainingState === null;
// }
// }, [cbFilterTrainingState]);
const [flterNoTraining, setFilterNoTraining] = useState(false); const [flterNoTraining, setFilterNoTraining] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
@ -225,6 +215,133 @@ function PublishersPage({ publishers = [] }: IProps) {
toast.error("Грешка при експорт на данни"); toast.error("Грешка при експорт на данни");
} }
} }
// Add this function to your component
const exportFilteredPublishers = () => {
try {
// Calculate total statistics
const formatDateBG = (date) => {
if (!date) return '';
return new Date(date).toLocaleDateString('bg-BG', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const stats = {
totalPublishers: shownPubs.length,
activePublishers: shownPubs.filter(p => p.isActive).length,
trainedPublishers: shownPubs.filter(p => p.isTrained).length,
totalAssignments: shownPubs.reduce((sum, p) => sum + p.assignments.length, 0),
totalAvailabilities: shownPubs.reduce((sum, p) => sum + p.availabilities.length, 0),
subscribedToReminders: shownPubs.filter(p => p.isSubscribedToReminders).length,
subscribedToCoverMe: shownPubs.filter(p => p.isSubscribedToCoverMe).length,
familyHeads: shownPubs.filter(p => p.familyHeadId).length,
avgAssignments: (shownPubs.reduce((sum, p) => sum + p.assignments.length, 0) / shownPubs.length).toFixed(2),
avgAvailabilities: (shownPubs.reduce((sum, p) => sum + p.availabilities.length, 0) / shownPubs.length).toFixed(2)
};
// Format the publishers data for Excel
const publishersData = shownPubs.map(publisher => ({
'First Name': publisher.firstName,
'Last Name': publisher.lastName,
'Email': publisher.email,
'Phone': publisher.phone || '',
'Active': publisher.isActive ? 'Yes' : 'No',
'Trained': publisher.isTrained ? 'Yes' : 'No',
'Role': publisher.role ? 'Yes' : 'No',
'Last Login': publisher.lastLogin ? formatDateBG(publisher.lastLogin) : 'Never',
'Subscribed to Reminders': publisher.isSubscribedToReminders ? 'Yes' : 'No',
'Subscribed to CoverMe': publisher.isSubscribedToCoverMe ? 'Yes' : 'No',
'Family Head ID': publisher.familyHeadId || '',
'Assignments Count': publisher.assignments.length,
'Availabilities Count': publisher.availabilities.length,
'Last Assignment': publisher.assignments.length > 0
? formatDateBG(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))) : 'Never',
}));
// Create workbook
const wb = utils.book_new();
// Create publishers worksheet
const ws_data = utils.json_to_sheet(publishersData);
// Set column widths for publishers sheet
const colWidths = [
{ wch: 15 }, // First Name
{ wch: 15 }, // Last Name
{ wch: 30 }, // Email
{ wch: 15 }, // Phone
{ wch: 10 }, // Active
{ wch: 10 }, // Trained
{ wch: 10 }, // Role
{ wch: 15 }, // Last Login
{ wch: 20 }, // Subscribed to Reminders
{ wch: 20 }, // Subscribed to CoverMe
{ wch: 15 }, // Family Head ID
{ wch: 15 }, // Assignments Count
{ wch: 15 }, // Availabilities Count
{ wch: 15 }, // Last Assignment
];
ws_data['!cols'] = colWidths;
// Create summary worksheet
const summary_data = [
['Summary Statistics', ''],
['Total Publishers', stats.totalPublishers],
['Active Publishers', stats.activePublishers],
['Trained Publishers', stats.trainedPublishers],
['Total Assignments', stats.totalAssignments],
['Average Assignments per Publisher', stats.avgAssignments],
['Total Availabilities', stats.totalAvailabilities],
['Average Availabilities per Publisher', stats.avgAvailabilities],
['Subscribed to Reminders', stats.subscribedToReminders],
['Subscribed to CoverMe', stats.subscribedToCoverMe],
['Family Heads', stats.familyHeads],
['Export Date', formatDateBG(new Date())],
[''],
['Applied Filters:', ''],
['Name Filter', filter || 'None'],
['Zero Shifts Only', showZeroShiftsOnly ? 'Yes' : 'No'],
['No Training', flterNoTraining ? 'Yes' : 'No'],
];
const ws_summary = utils.aoa_to_sheet(summary_data);
// Set column widths for summary sheet
ws_summary['!cols'] = [
{ wch: 30 }, // Label
{ wch: 15 }, // Value
];
// Add sheets to workbook
utils.book_append_sheet(wb, ws_summary, 'Summary');
utils.book_append_sheet(wb, ws_data, 'Publishers');
// Generate buffer
const excelBuffer = write(wb, { bookType: 'xlsx', type: 'array' });
// Create blob and save file
const blob = new Blob([excelBuffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
// Generate filename with current date and filter indication
const date = formatDateBG(new Date()).replace(/\./g, '-');
const isFiltered = filter || showZeroShiftsOnly || flterNoTraining;
const filename = `publishers-${isFiltered ? 'filtered-' : ''}${date}.xlsx`;
saveAs(blob, filename);
toast.success("Експортът е успешен");
} catch (error) {
console.error('Export error:', error);
toast.error("Грешка при експорт на данни");
}
};
return ( return (
<Layout> <Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}> <ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
@ -258,8 +375,16 @@ function PublishersPage({ publishers = [] }: IProps) {
<a href="/cart/publishers/import" className="btn">Import publishers</a> <a href="/cart/publishers/import" className="btn">Import publishers</a>
</div> </div>
{/* export by calling excel helper .ExportPublishersToExcel() */} {/* export by calling excel helper .ExportPublishersToExcel() */}
<div className="flex justify-center m-4"> {/* <div className="flex justify-center m-4">
<button className="button m-2 btn btn-primary" onClick={exportPublishers}>Export to Excel</button> <button className="button m-2 btn btn-primary" onClick={exportPublishers}>Export to Excel</button>
</div> */}
<div className="flex justify-center m-4">
<button className="button m-2 btn btn-primary" onClick={exportPublishers}>
Export All XLSX
</button>
<button className="button m-2 btn btn-primary" onClick={exportFilteredPublishers}>
Export Filtered XLSX
</button>
</div> </div>
</div> </div>
@ -350,6 +475,12 @@ export const getServerSideProps = async (context) => {
isActive: true, isActive: true,
isTrained: true, isTrained: true,
isImported: true, isImported: true,
lastLogin: true,
phone: true,
isSubscribedToReminders: true,
isSubscribedToCoverMe: true,
familyHeadId: true,
role: true,
assignments: { assignments: {
select: { select: {
shift: { shift: {