From 747bdad3c61de4f9f2ea302d0a70db261bc3d4bf Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 8 Jan 2025 10:13:46 +0200 Subject: [PATCH 1/3] client side xlsx export :) --- package-lock.json | 7 +++ package.json | 1 + pages/cart/publishers/index.tsx | 96 +++++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 15 deletions(-) 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..c48e139 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,69 @@ function PublishersPage({ publishers = [] }: IProps) { toast.error("Грешка при експорт на данни"); } } + + + // Add this function to your component + const exportFilteredPublishers = () => { + try { + // Format the data for Excel + const excelData = 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', + 'Imported': publisher.isImported ? 'Yes' : 'No', + 'Last Assignment': publisher.assignments.length > 0 + ? new Date(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))).toLocaleDateString() + : 'Never', + 'Number of Assignments': publisher.assignments.length, + 'Number of Availabilities': publisher.availabilities.length + })); + + // Create worksheet + const ws = utils.json_to_sheet(excelData); + + // Set column widths + const colWidths = [ + { wch: 15 }, // First Name + { wch: 15 }, // Last Name + { wch: 30 }, // Email + { wch: 10 }, // Active + { wch: 10 }, // Trained + { wch: 10 }, // Imported + { wch: 15 }, // Last Assignment + { wch: 20 }, // Number of Assignments + { wch: 20 }, // Number of Availabilities + ]; + ws['!cols'] = colWidths; + + // Create workbook and add worksheet + const wb = utils.book_new(); + utils.book_append_sheet(wb, ws, '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 + const date = new Date().toISOString().split('T')[0]; + const filename = `publishers-${date}.xlsx`; + + saveAs(blob, filename); + + toast.success("Експортът е успешен"); + } catch (error) { + console.error('Export error:', error); + toast.error("Грешка при експорт на данни"); + } + }; + return ( @@ -258,8 +311,16 @@ function PublishersPage({ publishers = [] }: IProps) { Import publishers {/* export by calling excel helper .ExportPublishersToExcel() */} -
+ {/*
+
*/} +
+ +
@@ -350,6 +411,11 @@ export const getServerSideProps = async (context) => { isActive: true, isTrained: true, isImported: true, + lastLogin: true, + phone: true, + isSubscribedToReminders: true, + isSubscribedToCoverMe: true, + familyHeadId: true, assignments: { select: { shift: { From 2d039466faa15634f5b32635e14591d8bf998ff3 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 8 Jan 2025 10:39:56 +0200 Subject: [PATCH 2/3] better export --- pages/cart/publishers/index.tsx | 94 ++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/pages/cart/publishers/index.tsx b/pages/cart/publishers/index.tsx index c48e139..e96c934 100644 --- a/pages/cart/publishers/index.tsx +++ b/pages/cart/publishers/index.tsx @@ -218,44 +218,100 @@ function PublishersPage({ publishers = [] }: IProps) { // Add this function to your component + const exportFilteredPublishers = () => { try { - // Format the data for Excel - const excelData = shownPubs.map(publisher => ({ + // Calculate total statistics + 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, + 'Phone': publisher.phone || '', 'Active': publisher.isActive ? 'Yes' : 'No', 'Trained': publisher.isTrained ? 'Yes' : 'No', - 'Imported': publisher.isImported ? 'Yes' : 'No', + 'Role': publisher.role ? 'Yes' : 'No', + 'Last Login': publisher.lastLogin ? new Date(publisher.lastLogin).toLocaleDateString() : '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 ? new Date(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))).toLocaleDateString() : 'Never', - 'Number of Assignments': publisher.assignments.length, - 'Number of Availabilities': publisher.availabilities.length })); - // Create worksheet - const ws = utils.json_to_sheet(excelData); + // Create workbook + const wb = utils.book_new(); - // Set column widths + // 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 }, // Imported + { 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 - { wch: 20 }, // Number of Assignments - { wch: 20 }, // Number of Availabilities ]; - ws['!cols'] = colWidths; + ws_data['!cols'] = colWidths; - // Create workbook and add worksheet - const wb = utils.book_new(); - utils.book_append_sheet(wb, ws, 'Publishers'); + // 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', new Date().toLocaleDateString()], + [''], + ['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' }); @@ -265,9 +321,10 @@ function PublishersPage({ publishers = [] }: IProps) { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); - // Generate filename with current date + // Generate filename with current date and filter indication const date = new Date().toISOString().split('T')[0]; - const filename = `publishers-${date}.xlsx`; + const isFiltered = filter || showZeroShiftsOnly || flterNoTraining; + const filename = `publishers-${isFiltered ? 'filtered-' : ''}${date}.xlsx`; saveAs(blob, filename); @@ -416,6 +473,7 @@ export const getServerSideProps = async (context) => { isSubscribedToReminders: true, isSubscribedToCoverMe: true, familyHeadId: true, + role: true, assignments: { select: { shift: { From 1e05cb4e5cdcbeac176ed76b272ca0157c8cc6d1 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Wed, 8 Jan 2025 10:41:42 +0200 Subject: [PATCH 3/3] dates in BG format --- pages/cart/publishers/index.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pages/cart/publishers/index.tsx b/pages/cart/publishers/index.tsx index e96c934..7f9e7ae 100644 --- a/pages/cart/publishers/index.tsx +++ b/pages/cart/publishers/index.tsx @@ -222,6 +222,14 @@ function PublishersPage({ publishers = [] }: IProps) { 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, @@ -244,15 +252,14 @@ function PublishersPage({ publishers = [] }: IProps) { 'Active': publisher.isActive ? 'Yes' : 'No', 'Trained': publisher.isTrained ? 'Yes' : 'No', 'Role': publisher.role ? 'Yes' : 'No', - 'Last Login': publisher.lastLogin ? new Date(publisher.lastLogin).toLocaleDateString() : 'Never', + '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 - ? new Date(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))).toLocaleDateString() - : 'Never', + ? formatDateBG(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))) : 'Never', })); // Create workbook @@ -293,7 +300,7 @@ function PublishersPage({ publishers = [] }: IProps) { ['Subscribed to Reminders', stats.subscribedToReminders], ['Subscribed to CoverMe', stats.subscribedToCoverMe], ['Family Heads', stats.familyHeads], - ['Export Date', new Date().toLocaleDateString()], + ['Export Date', formatDateBG(new Date())], [''], ['Applied Filters:', ''], ['Name Filter', filter || 'None'], @@ -322,7 +329,7 @@ function PublishersPage({ publishers = [] }: IProps) { }); // Generate filename with current date and filter indication - const date = new Date().toISOString().split('T')[0]; + const date = formatDateBG(new Date()).replace(/\./g, '-'); const isFiltered = filter || showZeroShiftsOnly || flterNoTraining; const filename = `publishers-${isFiltered ? 'filtered-' : ''}${date}.xlsx`;