diff --git a/package-lock.json b/package-lock.json index 9e0c5e5..8b9c080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "garenne", - "version": "0.8.0-beta", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "garenne", - "version": "0.8.0-beta", + "version": "1.1.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", diff --git a/src/pages/Animals/AnimalDetailPage.tsx b/src/pages/Animals/AnimalDetailPage.tsx index fe7ee0b..fcd83e2 100644 --- a/src/pages/Animals/AnimalDetailPage.tsx +++ b/src/pages/Animals/AnimalDetailPage.tsx @@ -45,11 +45,12 @@ import { useAppStore } from '../../state/store'; import { Sex, Status, Breeding } from '../../models/types'; import { calculateAgeText, formatDate } from '../../utils/dates'; import { getAnimalActiveTreatments, getAnimalWeights, getAnimalTreatments, getFemaleBreedings, getAnimalById } from '../../state/selectors'; -import { BreedingModal, WeightChart, GenealogyTree, QRCodeDisplay, PrintableRabbitSheet, MortalityModal, QuickWeightModal, QuickTreatmentModal } from '../../components/LazyComponents'; +import { BreedingModal, WeightChart, GenealogyTree, QRCodeDisplay, MortalityModal, QuickWeightModal, QuickTreatmentModal } from '../../components/LazyComponents'; import { LitterModal } from '../../components/modals/LitterModal'; import { AdvancedGenealogyTree } from '../../components/AdvancedGenealogyTree'; import { MatingRecommendations } from '../../components/MatingRecommendations'; import { PedigreePDFService } from '../../services/pedigree-pdf.service'; +import { printRabbitSheet } from '../../utils/print.utils'; interface TabPanelProps { children?: React.ReactNode; @@ -96,7 +97,6 @@ const AnimalDetailPage = () => { const [breedingModalOpen, setBreedingModalOpen] = useState(false); const [litterModalOpen, setLitterModalOpen] = useState(false); const [mortalityModalOpen, setMortalityModalOpen] = useState(false); - const [printDialogOpen, setPrintDialogOpen] = useState(false); const [selectedBreeding, setSelectedBreeding] = useState(null); const [breedingToDelete, setBreedingToDelete] = useState(null); const [breedingMenuAnchor, setBreedingMenuAnchor] = useState(null); @@ -174,16 +174,10 @@ const AnimalDetailPage = () => { }; const handlePrint = () => { - setPrintDialogOpen(true); - }; - - const handlePrintConfirm = () => { - // Don't close the dialog immediately, let the print happen first - setTimeout(() => { - window.print(); - // Close dialog after printing - setPrintDialogOpen(false); - }, 100); + // Use the new dedicated print function instead of the dialog + if (animal) { + printRabbitSheet(animal); + } }; const handleExportPedigree = async (animal: any) => { @@ -749,33 +743,6 @@ const AnimalDetailPage = () => { - {/* Print Dialog */} - setPrintDialogOpen(false)} - maxWidth="md" - fullWidth - > - - - - Aperçu de la fiche à imprimer - - - - - - - - - - - - ); }; diff --git a/src/test/print.utils.test.ts b/src/test/print.utils.test.ts new file mode 100644 index 0000000..f8fb8e8 --- /dev/null +++ b/src/test/print.utils.test.ts @@ -0,0 +1,180 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { printRabbitSheet } from '../utils/print.utils'; +import { Animal, Sex, Status } from '../models/types'; + +// Mock window.open +const mockWindow = { + document: { + write: vi.fn(), + close: vi.fn(), + }, + onload: null as (() => void) | null, + focus: vi.fn(), + print: vi.fn(), + close: vi.fn(), + onafterprint: null as (() => void) | null, +}; + +describe('Print Utils', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Mock window.open to return our mock window + vi.stubGlobal('window', { + ...window, + open: vi.fn().mockReturnValue(mockWindow), + }); + }); + + it('should open a print window and generate correct HTML content', () => { + const testAnimal: Animal = { + id: 'test-123', + name: 'Test Rabbit', + identifier: 'TR001', + sex: Sex.Female, + status: Status.Grow, + breed: 'Rex', + birthDate: '2024-01-15T00:00:00.000Z', + origin: 'PURCHASED', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + fatherId: undefined, + motherId: undefined, + cage: undefined, + notes: undefined, + tags: [], + }; + + printRabbitSheet(testAnimal); + + // Verify window.open was called + expect(window.open).toHaveBeenCalledWith('', '_blank', 'width=800,height=600'); + + // Verify document.write was called + expect(mockWindow.document.write).toHaveBeenCalledTimes(1); + + // Get the HTML content that was written + const htmlContent = mockWindow.document.write.mock.calls[0][0]; + + // Verify essential content is present + expect(htmlContent).toContain('FICHE LAPIN'); + expect(htmlContent).toContain('Test Rabbit'); + expect(htmlContent).toContain('ID: TR001'); + expect(htmlContent).toContain('Femelle ♀'); + expect(htmlContent).toContain('15/01/2024'); + expect(htmlContent).toContain('Rex'); + expect(htmlContent).toContain('Code QR'); + expect(htmlContent).toContain('Garenne'); + + // Verify CSS includes A6 page size + expect(htmlContent).toContain('@page {'); + expect(htmlContent).toContain('size: A6;'); + expect(htmlContent).toContain('margin: 0.5cm;'); + + // Verify document.close was called + expect(mockWindow.document.close).toHaveBeenCalledTimes(1); + }); + + it('should handle missing animal data gracefully', () => { + const testAnimal: Animal = { + id: 'test-456', + name: '', + identifier: '', + sex: Sex.Unknown, + status: Status.Grow, + breed: '', + birthDate: '', + origin: 'PURCHASED', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + fatherId: undefined, + motherId: undefined, + cage: undefined, + notes: undefined, + tags: [], + }; + + printRabbitSheet(testAnimal); + + const htmlContent = mockWindow.document.write.mock.calls[0][0]; + + // Should handle empty data gracefully + expect(htmlContent).toContain('Sans nom'); + expect(htmlContent).toContain('Inconnu'); // For unknown sex + expect(htmlContent).toContain('Non renseignée'); // For missing birth date + }); + + it('should setup print handlers correctly', () => { + const testAnimal: Animal = { + id: 'test-789', + name: 'Print Test', + identifier: 'PT001', + sex: Sex.Male, + status: Status.Reproducer, + breed: 'Angora', + birthDate: '2023-06-10T00:00:00.000Z', + origin: 'BORN_HERE', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + fatherId: undefined, + motherId: undefined, + cage: undefined, + notes: undefined, + tags: [], + }; + + printRabbitSheet(testAnimal); + + // Simulate onload event + if (mockWindow.onload) { + mockWindow.onload(); + } + + // Verify print-related functions were called + expect(mockWindow.focus).toHaveBeenCalledTimes(1); + expect(mockWindow.print).toHaveBeenCalledTimes(1); + + // Simulate onafterprint event + if (mockWindow.onafterprint) { + mockWindow.onafterprint(); + } + + // Verify window close was called after print + expect(mockWindow.close).toHaveBeenCalledTimes(1); + }); + + it('should handle window.open failure gracefully', () => { + // Mock window.open to return null (blocked by popup blocker) + vi.stubGlobal('window', { + ...window, + open: vi.fn().mockReturnValue(null), + }); + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const testAnimal: Animal = { + id: 'test-blocked', + name: 'Blocked Print', + identifier: 'BP001', + sex: Sex.Female, + status: Status.Grow, + breed: 'Dutch', + birthDate: '2024-03-20T00:00:00.000Z', + origin: 'PURCHASED', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + fatherId: undefined, + motherId: undefined, + cage: undefined, + notes: undefined, + tags: [], + }; + + // Should not throw an error + expect(() => printRabbitSheet(testAnimal)).not.toThrow(); + + // Should log an error message + expect(consoleErrorSpy).toHaveBeenCalledWith('Could not open print window. Please check popup blockers.'); + + consoleErrorSpy.mockRestore(); + }); +}); \ No newline at end of file diff --git a/src/utils/print.utils.ts b/src/utils/print.utils.ts new file mode 100644 index 0000000..4ff73bf --- /dev/null +++ b/src/utils/print.utils.ts @@ -0,0 +1,247 @@ +import { Animal } from '../models/types'; + +/** + * Opens a new window with the printable rabbit sheet and triggers print + */ +export const printRabbitSheet = (animal: Animal): void => { + // Create the HTML content for the print window + const printContent = createPrintableSheetHTML(animal); + + // Open a new window + const printWindow = window.open('', '_blank', 'width=800,height=600'); + + if (!printWindow) { + console.error('Could not open print window. Please check popup blockers.'); + return; + } + + // Write the content to the new window + printWindow.document.write(printContent); + printWindow.document.close(); + + // Wait for the content to load, then print + printWindow.onload = () => { + printWindow.focus(); + printWindow.print(); + // Close the window after printing (optional - user might want to keep it open) + printWindow.onafterprint = () => { + printWindow.close(); + }; + }; +}; + +/** + * Creates the complete HTML content for the printable rabbit sheet + */ +const createPrintableSheetHTML = (animal: Animal): string => { + // We'll use a simplified version of the rabbit sheet optimized for printing + return ` + + + + + + Fiche Lapin - ${animal.name || 'Sans nom'} + + + +
+
+

FICHE LAPIN

+

${animal.name || 'Sans nom'}

+ ${animal.identifier ? `
ID: ${animal.identifier}
` : ''} +
+ +
+
+
+
Sexe:
+
${getSexDisplay(animal.sex)}
+
+
+
Naissance:
+
${animal.birthDate ? formatDate(animal.birthDate) : 'Non renseignée'}
+
+
+ ${animal.breed ? ` +
+
+
Race:
+
${animal.breed}
+
+
+
+ ` : ''} +
+ +
+
Code QR
+
+
+ + +
+ + + + +`; +}; + +/** + * Helper function to format sex display + */ +const getSexDisplay = (sex: string): string => { + switch (sex) { + case 'F': return 'Femelle ♀'; + case 'M': return 'Mâle ♂'; + default: return 'Inconnu'; + } +}; + +/** + * Helper function to format dates + */ +const formatDate = (dateString: string): string => { + try { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR'); + } catch { + return dateString; + } +}; \ No newline at end of file