diff --git a/package-lock.json b/package-lock.json index 421c62d..f34a8af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "express": "^4.18.2", "express-jwt": "^8.4.1", "fastest-levenshtein": "^1.0.16", + "file-saver": "^2.0.5", "fs": "^0.0.1-security", "gapi": "^0.0.3", "gapi-script": "^1.2.0", @@ -7906,6 +7907,12 @@ "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": { "version": "0.6.1", "license": "MIT", diff --git a/package.json b/package.json index c86025c..191d986 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "express": "^4.18.2", "express-jwt": "^8.4.1", "fastest-levenshtein": "^1.0.16", + "file-saver": "^2.0.5", "fs": "^0.0.1-security", "gapi": "^0.0.3", "gapi-script": "^1.2.0", diff --git a/pages/cart/publishers/index.tsx b/pages/cart/publishers/index.tsx index 10e5bc4..7f9e7ae 100644 --- a/pages/cart/publishers/index.tsx +++ b/pages/cart/publishers/index.tsx @@ -23,6 +23,10 @@ import ConfirmationModal from '../../../components/ConfirmationModal'; import { relative } from "path"; import { set } from "lodash"; +import { utils, write } from 'xlsx'; +import { saveAs } from 'file-saver'; + + interface IProps { initialItems: Publisher[]; } @@ -38,20 +42,6 @@ function PublishersPage({ publishers = [] }: IProps) { indeterminate: true, }); - // const cbRefFilterTraining = useRef(null); - - // const getCheckboxState = (currentState: boolean | null) => { - // if (currentState === true) return 'unchecked'; - // if (currentState === false) return 'checked'; - // return 'indeterminate'; - // }; - // const cbRefFilterTraining = useRef(null); - // const [cbFilterTrainingState, setcbFilterTrainingState] = useState(null); - // useEffect(() => { - // if (cbRefFilterTraining.current) { - // cbRefFilterTraining.current.indeterminate = cbFilterTrainingState === null; - // } - // }, [cbFilterTrainingState]); const [flterNoTraining, setFilterNoTraining] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -225,6 +215,133 @@ function PublishersPage({ publishers = [] }: IProps) { 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 ( @@ -258,8 +375,16 @@ function PublishersPage({ publishers = [] }: IProps) { Import publishers {/* export by calling excel helper .ExportPublishersToExcel() */} -
+ {/*
+
*/} +
+ +
@@ -350,6 +475,12 @@ export const getServerSideProps = async (context) => { isActive: true, isTrained: true, isImported: true, + lastLogin: true, + phone: true, + isSubscribedToReminders: true, + isSubscribedToCoverMe: true, + familyHeadId: true, + role: true, assignments: { select: { shift: {