540 lines
16 KiB
React
540 lines
16 KiB
React
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
|
import { useDispatch } from 'react-redux';
|
||
|
|
import { Messages } from '../../../Components/Notifications/Messages';
|
||
|
|
import FormHeader from '../../PageComponents/FormHeader';
|
||
|
|
import QRCode from 'react-qr-code';
|
||
|
|
import PublicQrCodeShare from '../PublicQrCodeShare/PublicQrCodeShare';
|
||
|
|
import { encryptObject, getSession } from '../../../Services/Others';
|
||
|
|
import { PublicQrCodepost } from '../../../Features/ConfigMasterPage/ConfigMasterPage';
|
||
|
|
import Buttons from '../../../Components/Forms/Buttons';
|
||
|
|
import { changeBreadCrumb } from '../../../Features/AppPage/CenterPage';
|
||
|
|
import { Modal } from 'antd';
|
||
|
|
import { jsPDF } from 'jspdf';
|
||
|
|
import {
|
||
|
|
FaFacebookF,
|
||
|
|
FaWhatsapp,
|
||
|
|
FaTwitter,
|
||
|
|
FaLinkedinIn,
|
||
|
|
FaEnvelope,
|
||
|
|
} from 'react-icons/fa';
|
||
|
|
import QRCodeLib from 'qrcode';
|
||
|
|
import { LuDownload } from 'react-icons/lu';
|
||
|
|
import { BsFileEarmarkPdf } from 'react-icons/bs';
|
||
|
|
|
||
|
|
const subDirectory = import.meta.env.ENV_MAIN_BASE_URL || '/';
|
||
|
|
|
||
|
|
const MenuQRCode = () => {
|
||
|
|
const dispatch = useDispatch();
|
||
|
|
|
||
|
|
// Session
|
||
|
|
const CompId = getSession('CompId');
|
||
|
|
const BranchId = getSession('BranchId');
|
||
|
|
const AppId = getSession('AppId');
|
||
|
|
const UserId = getSession('UserId');
|
||
|
|
|
||
|
|
// States
|
||
|
|
const [qrCodeUrl, setQrCodeUrl] = useState('');
|
||
|
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
||
|
|
const [messageType, setMessageType] = useState(null);
|
||
|
|
const [messageData, setMessageData] = useState(null);
|
||
|
|
|
||
|
|
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
||
|
|
const [pdfPreviewUrl, setPdfPreviewUrl] = useState('');
|
||
|
|
const [socialIconsDataUrls, setSocialIconsDataUrls] = useState([]);
|
||
|
|
|
||
|
|
const cardRef = useRef(null);
|
||
|
|
const socialIconsRef = useRef(null);
|
||
|
|
|
||
|
|
// breadcrumb
|
||
|
|
useEffect(() => {
|
||
|
|
const items = [
|
||
|
|
{
|
||
|
|
name: 'Home',
|
||
|
|
link: `${subDirectory}app-page/home`,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
dispatch(changeBreadCrumb({ items }));
|
||
|
|
HandleSubmit();
|
||
|
|
}, [dispatch]);
|
||
|
|
|
||
|
|
// Generate QR Code as Data URL using qrcode library
|
||
|
|
useEffect(() => {
|
||
|
|
if (qrCodeUrl) {
|
||
|
|
QRCodeLib.toDataURL(qrCodeUrl, {
|
||
|
|
width: 200,
|
||
|
|
margin: 1,
|
||
|
|
color: {
|
||
|
|
dark: '#000000',
|
||
|
|
light: '#FFFFFF',
|
||
|
|
},
|
||
|
|
})
|
||
|
|
.then((url) => {
|
||
|
|
setQrCodeDataUrl(url);
|
||
|
|
})
|
||
|
|
.catch((err) => {
|
||
|
|
console.error('QR generation error:', err);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, [qrCodeUrl]);
|
||
|
|
|
||
|
|
// Convert social icons to images
|
||
|
|
useEffect(() => {
|
||
|
|
if (qrCodeUrl && socialIconsRef.current) {
|
||
|
|
const convertIconsToImages = async () => {
|
||
|
|
try {
|
||
|
|
const icons = socialIconsRef.current.querySelectorAll(
|
||
|
|
'.social-icon-container'
|
||
|
|
);
|
||
|
|
const dataUrls = [];
|
||
|
|
|
||
|
|
for (let icon of icons) {
|
||
|
|
const svg = icon.querySelector('svg');
|
||
|
|
if (svg) {
|
||
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
||
|
|
const canvas = document.createElement('canvas');
|
||
|
|
const ctx = canvas.getContext('2d');
|
||
|
|
const img = new Image();
|
||
|
|
|
||
|
|
canvas.width = 16;
|
||
|
|
canvas.height = 16;
|
||
|
|
|
||
|
|
await new Promise((resolve) => {
|
||
|
|
img.onload = () => {
|
||
|
|
ctx.fillStyle = '#0f1724';
|
||
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
|
ctx.globalCompositeOperation = 'destination-in';
|
||
|
|
ctx.drawImage(img, 0, 0, 16, 16);
|
||
|
|
const dataUrl = canvas.toDataURL('image/png');
|
||
|
|
dataUrls.push(dataUrl);
|
||
|
|
resolve();
|
||
|
|
};
|
||
|
|
img.src =
|
||
|
|
'data:image/svg+xml;base64,' +
|
||
|
|
btoa(unescape(encodeURIComponent(svgData)));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setSocialIconsDataUrls(dataUrls);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Icon conversion error:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
setTimeout(convertIconsToImages, 100);
|
||
|
|
}
|
||
|
|
}, [qrCodeUrl]);
|
||
|
|
|
||
|
|
const HandleSubmit = async () => {
|
||
|
|
try {
|
||
|
|
let data = { BranchId, CompId, AppId, UserId };
|
||
|
|
const encrypted = encryptObject(data);
|
||
|
|
const postData = { Url: encrypted, CreatedBy: UserId };
|
||
|
|
|
||
|
|
const res = await dispatch(PublicQrCodepost(postData)).unwrap();
|
||
|
|
|
||
|
|
if (res?.data?.statusCode === 1) {
|
||
|
|
const QrUrl = `${subDirectory}MenuQRCode?code=${res?.data?.ReferenceNo}`;
|
||
|
|
setQrCodeUrl(QrUrl);
|
||
|
|
setMessageType('success');
|
||
|
|
setMessageData('QR Code generated.');
|
||
|
|
} else {
|
||
|
|
setMessageType('error');
|
||
|
|
setMessageData(res?.data?.message || 'Failed to generate QR.');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('HandleSubmit Error:', error);
|
||
|
|
setMessageType('error');
|
||
|
|
setMessageData('An unexpected error occurred.');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const onComplete = useCallback(() => {
|
||
|
|
setMessageData(null);
|
||
|
|
setMessageType(null);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// const clearData = () => {
|
||
|
|
// setQrCodeUrl("");
|
||
|
|
// setQrCodeDataUrl("");
|
||
|
|
// setTableId(null);
|
||
|
|
// if (pdfPreviewUrl) {
|
||
|
|
// URL.revokeObjectURL(pdfPreviewUrl);
|
||
|
|
// setPdfPreviewUrl("");
|
||
|
|
// }
|
||
|
|
// };
|
||
|
|
|
||
|
|
const generatePdfFromCard = async (openPreview = true) => {
|
||
|
|
try {
|
||
|
|
if (!qrCodeDataUrl || socialIconsDataUrls.length < 4) {
|
||
|
|
setMessageType('error');
|
||
|
|
setMessageData('QR Code and icons not ready. Please wait a moment.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const pdf = new jsPDF({
|
||
|
|
orientation: 'portrait',
|
||
|
|
unit: 'pt',
|
||
|
|
format: 'a4',
|
||
|
|
});
|
||
|
|
|
||
|
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
||
|
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
||
|
|
|
||
|
|
// Card dimensions
|
||
|
|
const cardWidth = 350;
|
||
|
|
const cardX = (pageWidth - cardWidth) / 2;
|
||
|
|
let yPos = 80;
|
||
|
|
|
||
|
|
// Background card
|
||
|
|
pdf.setFillColor(255, 255, 255);
|
||
|
|
pdf.roundedRect(cardX, yPos, cardWidth, 500, 8, 8, 'F');
|
||
|
|
|
||
|
|
// Shadow effect
|
||
|
|
pdf.setDrawColor(0, 0, 0);
|
||
|
|
pdf.setLineWidth(0.5);
|
||
|
|
pdf.setGState(pdf.GState({ opacity: 0.1 }));
|
||
|
|
pdf.roundedRect(cardX, yPos, cardWidth, 500, 8, 8, 'S');
|
||
|
|
pdf.setGState(pdf.GState({ opacity: 1 }));
|
||
|
|
|
||
|
|
yPos += 40;
|
||
|
|
|
||
|
|
// Title
|
||
|
|
pdf.setFontSize(16);
|
||
|
|
pdf.setFont('helvetica', 'bold');
|
||
|
|
pdf.setTextColor(15, 23, 36);
|
||
|
|
pdf.text('Scan the QR Code', pageWidth / 2, yPos, { align: 'center' });
|
||
|
|
|
||
|
|
yPos += 40;
|
||
|
|
|
||
|
|
// QR Code background frame
|
||
|
|
const qrSize = 200;
|
||
|
|
const qrX = (pageWidth - qrSize) / 2;
|
||
|
|
|
||
|
|
// Outer decorative frame
|
||
|
|
pdf.setFillColor(255, 255, 255);
|
||
|
|
pdf.roundedRect(
|
||
|
|
qrX - 20,
|
||
|
|
yPos - 20,
|
||
|
|
qrSize + 40,
|
||
|
|
qrSize + 40,
|
||
|
|
12,
|
||
|
|
12,
|
||
|
|
'F'
|
||
|
|
);
|
||
|
|
|
||
|
|
// Shadow for QR frame
|
||
|
|
pdf.setDrawColor(0, 0, 0);
|
||
|
|
pdf.setGState(pdf.GState({ opacity: 0.06 }));
|
||
|
|
pdf.roundedRect(
|
||
|
|
qrX - 20,
|
||
|
|
yPos - 20,
|
||
|
|
qrSize + 40,
|
||
|
|
qrSize + 40,
|
||
|
|
12,
|
||
|
|
12,
|
||
|
|
'S'
|
||
|
|
);
|
||
|
|
pdf.setGState(pdf.GState({ opacity: 1 }));
|
||
|
|
|
||
|
|
// Inner white background for QR
|
||
|
|
pdf.setFillColor(255, 255, 255);
|
||
|
|
pdf.roundedRect(qrX - 8, yPos - 8, qrSize + 16, qrSize + 16, 8, 8, 'F');
|
||
|
|
|
||
|
|
// Add QR Code image
|
||
|
|
pdf.addImage(qrCodeDataUrl, 'PNG', qrX, yPos, qrSize, qrSize);
|
||
|
|
|
||
|
|
yPos += qrSize + 40;
|
||
|
|
|
||
|
|
// MENU button
|
||
|
|
const buttonWidth = 120;
|
||
|
|
const buttonHeight = 45;
|
||
|
|
const buttonX = (pageWidth - buttonWidth) / 2;
|
||
|
|
|
||
|
|
pdf.setFillColor(15, 107, 117);
|
||
|
|
pdf.roundedRect(buttonX, yPos, buttonWidth, buttonHeight, 8, 8, 'F');
|
||
|
|
|
||
|
|
pdf.setFontSize(22);
|
||
|
|
pdf.setFont('helvetica', 'bold');
|
||
|
|
pdf.setTextColor(255, 255, 255);
|
||
|
|
pdf.text('MENU', pageWidth / 2, yPos + 30, { align: 'center' });
|
||
|
|
|
||
|
|
yPos += buttonHeight + 30;
|
||
|
|
|
||
|
|
// Social icons with actual images
|
||
|
|
const iconSize = 36;
|
||
|
|
const iconSpacing = 18;
|
||
|
|
const totalIconsWidth = iconSize * 4 + iconSpacing * 3;
|
||
|
|
let iconX = (pageWidth - totalIconsWidth) / 2;
|
||
|
|
|
||
|
|
pdf.setFillColor(255, 255, 255);
|
||
|
|
pdf.setDrawColor(230, 238, 240);
|
||
|
|
pdf.setLineWidth(1);
|
||
|
|
|
||
|
|
socialIconsDataUrls.forEach((iconDataUrl) => {
|
||
|
|
// Draw circle background
|
||
|
|
pdf.circle(iconX + iconSize / 2, yPos, iconSize / 2, 'FD');
|
||
|
|
|
||
|
|
// Draw icon image centered in circle
|
||
|
|
const iconImageSize = 16;
|
||
|
|
const iconImageX = iconX + (iconSize - iconImageSize) / 2;
|
||
|
|
const iconImageY = yPos - iconImageSize / 2;
|
||
|
|
pdf.addImage(
|
||
|
|
iconDataUrl,
|
||
|
|
'PNG',
|
||
|
|
iconImageX,
|
||
|
|
iconImageY,
|
||
|
|
iconImageSize,
|
||
|
|
iconImageSize
|
||
|
|
);
|
||
|
|
|
||
|
|
iconX += iconSize + iconSpacing;
|
||
|
|
});
|
||
|
|
|
||
|
|
yPos += 50;
|
||
|
|
|
||
|
|
// URL text at bottom
|
||
|
|
pdf.setFontSize(10);
|
||
|
|
pdf.setTextColor(156, 163, 175);
|
||
|
|
const urlText =
|
||
|
|
qrCodeUrl.length > 50 ? qrCodeUrl.substring(0, 47) + '...' : qrCodeUrl;
|
||
|
|
pdf.text(urlText, pageWidth / 2, yPos, { align: 'center' });
|
||
|
|
|
||
|
|
// Create blob for preview
|
||
|
|
const pdfBlob = pdf.output('blob');
|
||
|
|
const blobUrl = URL.createObjectURL(pdfBlob);
|
||
|
|
|
||
|
|
if (pdfPreviewUrl) {
|
||
|
|
URL.revokeObjectURL(pdfPreviewUrl);
|
||
|
|
}
|
||
|
|
|
||
|
|
setPdfPreviewUrl(blobUrl);
|
||
|
|
|
||
|
|
if (openPreview) {
|
||
|
|
setIsPreviewModalOpen(true);
|
||
|
|
} else {
|
||
|
|
pdf.save('menu-qr.pdf');
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('PDF generation failed', err);
|
||
|
|
setMessageType('error');
|
||
|
|
setMessageData('Failed to generate PDF.');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const downloadPreviewPdf = () => {
|
||
|
|
if (!pdfPreviewUrl) return;
|
||
|
|
fetch(pdfPreviewUrl)
|
||
|
|
.then((r) => r.blob())
|
||
|
|
.then((blob) => {
|
||
|
|
const url = URL.createObjectURL(blob);
|
||
|
|
const a = document.createElement('a');
|
||
|
|
a.href = url;
|
||
|
|
a.download = 'menu-qr.pdf';
|
||
|
|
document.body.appendChild(a);
|
||
|
|
a.click();
|
||
|
|
a.remove();
|
||
|
|
URL.revokeObjectURL(url);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="userPageTable">
|
||
|
|
<div className="userPageContent">
|
||
|
|
<Messages
|
||
|
|
messageType={messageType}
|
||
|
|
messageData={messageData}
|
||
|
|
onComplete={onComplete}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div className="formAddNew">
|
||
|
|
<FormHeader title={'Menu QR Code Generator'} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="reportTables">
|
||
|
|
{qrCodeUrl && (
|
||
|
|
<div
|
||
|
|
ref={socialIconsRef}
|
||
|
|
style={{ position: 'absolute', left: '-9999px' }}
|
||
|
|
>
|
||
|
|
<div className="social-icon-container">
|
||
|
|
<FaFacebookF size={16} color="#0f1724" />
|
||
|
|
</div>
|
||
|
|
<div className="social-icon-container">
|
||
|
|
<FaWhatsapp size={16} color="#0f1724" />
|
||
|
|
</div>
|
||
|
|
<div className="social-icon-container">
|
||
|
|
<FaEnvelope size={16} color="#0f1724" />
|
||
|
|
</div>
|
||
|
|
<div className="social-icon-container">
|
||
|
|
<FaLinkedinIn size={16} color="#0f1724" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Card Display */}
|
||
|
|
{qrCodeUrl && qrCodeDataUrl && (
|
||
|
|
<div
|
||
|
|
ref={cardRef}
|
||
|
|
id="qr-export-card"
|
||
|
|
style={{
|
||
|
|
width: 420,
|
||
|
|
margin: '0 auto',
|
||
|
|
padding: 24,
|
||
|
|
background: '#fff',
|
||
|
|
borderRadius: 12,
|
||
|
|
boxShadow: '0 6px 18px rgba(0,0,0,0.08)',
|
||
|
|
textAlign: 'center',
|
||
|
|
color: '#0f1724',
|
||
|
|
border: '1px solid grey',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<h3
|
||
|
|
style={{
|
||
|
|
fontSize: 16,
|
||
|
|
marginBottom: 12,
|
||
|
|
fontWeight: 500,
|
||
|
|
fontFamily: 'Poppins',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Scan the QR Code
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
{/* QR with decorative rounded frame */}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'inline-block',
|
||
|
|
padding: 18,
|
||
|
|
background: '#fff',
|
||
|
|
borderRadius: 18,
|
||
|
|
boxShadow: '0 2px 10px rgba(0,0,0,0.06)',
|
||
|
|
position: 'relative',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: 'absolute',
|
||
|
|
inset: 6,
|
||
|
|
borderRadius: 18,
|
||
|
|
border: '6px solid transparent',
|
||
|
|
boxShadow: '0 0 0 6px #ffffff inset',
|
||
|
|
pointerEvents: 'none',
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<div
|
||
|
|
style={{ background: '#fff', padding: 6, borderRadius: 12 }}
|
||
|
|
>
|
||
|
|
<img
|
||
|
|
src={qrCodeDataUrl}
|
||
|
|
alt="QR Code"
|
||
|
|
width={200}
|
||
|
|
height={200}
|
||
|
|
style={{ display: 'block', imageRendering: 'crisp-edges' }}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p
|
||
|
|
style={{
|
||
|
|
marginTop: 18,
|
||
|
|
color: '#67696d',
|
||
|
|
fontSize: 12,
|
||
|
|
wordBreak: 'break-all',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{qrCodeUrl}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{qrCodeUrl && <PublicQrCodeShare qrCodeUrl={qrCodeUrl} />}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
gap: 12,
|
||
|
|
justifyContent: 'center',
|
||
|
|
marginTop: 8,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* {qrCodeUrl && (
|
||
|
|
<Buttons buttonText="Generate QR" className="primary_Button" handleSubmit={HandleSubmit}>
|
||
|
|
Generate
|
||
|
|
</Buttons>
|
||
|
|
)} */}
|
||
|
|
|
||
|
|
{qrCodeUrl && (
|
||
|
|
<>
|
||
|
|
<Buttons
|
||
|
|
buttonText="Preview PDF"
|
||
|
|
className="primary_Button"
|
||
|
|
handleSubmit={() => generatePdfFromCard(true)}
|
||
|
|
disabled={!qrCodeDataUrl}
|
||
|
|
icon={<BsFileEarmarkPdf size={20} />}
|
||
|
|
>
|
||
|
|
Preview PDF
|
||
|
|
</Buttons>
|
||
|
|
|
||
|
|
<Buttons
|
||
|
|
buttonText="Download PDF"
|
||
|
|
className="primary_Button"
|
||
|
|
handleSubmit={() => generatePdfFromCard(false)}
|
||
|
|
disabled={!qrCodeDataUrl}
|
||
|
|
icon={<LuDownload size={20} />}
|
||
|
|
>
|
||
|
|
Download PDF
|
||
|
|
</Buttons>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Modal PDF Preview */}
|
||
|
|
<Modal
|
||
|
|
title="PDF Preview"
|
||
|
|
open={isPreviewModalOpen}
|
||
|
|
onCancel={() => {
|
||
|
|
setIsPreviewModalOpen(false);
|
||
|
|
}}
|
||
|
|
footer={[
|
||
|
|
<Buttons
|
||
|
|
key="download"
|
||
|
|
buttonText="Download"
|
||
|
|
className="primary_Button"
|
||
|
|
handleSubmit={downloadPreviewPdf}
|
||
|
|
>
|
||
|
|
Download
|
||
|
|
</Buttons>,
|
||
|
|
<Buttons
|
||
|
|
key="close"
|
||
|
|
buttonText="Close"
|
||
|
|
className="secondary_Button"
|
||
|
|
handleSubmit={() => setIsPreviewModalOpen(false)}
|
||
|
|
>
|
||
|
|
Close
|
||
|
|
</Buttons>,
|
||
|
|
]}
|
||
|
|
width={900}
|
||
|
|
style={{ top: 20 }}
|
||
|
|
>
|
||
|
|
{pdfPreviewUrl ? (
|
||
|
|
<div style={{ textAlign: 'center' }}>
|
||
|
|
<iframe
|
||
|
|
src={pdfPreviewUrl}
|
||
|
|
width="100%"
|
||
|
|
height="600px"
|
||
|
|
style={{ border: 'none', borderRadius: 4 }}
|
||
|
|
title="PDF Preview"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div style={{ textAlign: 'center', padding: 40 }}>
|
||
|
|
Preparing preview...
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
export default MenuQRCode;
|