diff --git a/apps/admin/locale/en.json b/apps/admin/locale/en.json index 4a7e9f8c7..4e8d225ee 100644 --- a/apps/admin/locale/en.json +++ b/apps/admin/locale/en.json @@ -896,6 +896,34 @@ "sfid-label": "Salesforce Event ID", "sfid-placeholder": "Enter your Salesforce Event ID", "sfid-help": "The unique identifier of your event in Salesforce" + }, + "sendgrid": { + "template-id-label": "SendGrid Template ID", + "template-id-placeholder": "d-xxxxxxxxxxxxxxxxxxxxxxxx", + "template-id-help": "Dynamic template ID from SendGrid", + "from-address-label": "From Email Address", + "from-address-placeholder": "noreply@example.com", + "from-address-help": "Verified sender email address", + "test-email-address-label": "Test Email Address", + "test-email-address-placeholder": "admin@example.com", + "test-email-address-help": "Where test emails will be sent", + "send-test-email-button": "Send Test Email", + "email-contacts-title": "Email Contacts", + "csv-description": "Upload a CSV file with columns: Team Number, Region, Recipient Name, Email Address", + "csv-requirements-title": "CSV File Requirements", + "csv-requirement-header": "File must include a header row", + "csv-requirement-columns": "Columns: Team Number, Region, Recipient Name, Recipient Email", + "csv-requirement-encoding": "Saved as UTF-8 encoded CSV", + "csv-example": "Example Format", + "upload-csv-button": "Upload CSV", + "validation-template-id-required": "Template ID is required", + "validation-from-address-required": "Valid email required", + "validation-test-email-required": "Valid email required", + "csv-error-invalid-format": "Please upload a CSV file", + "csv-error-upload-failed": "Failed to upload contacts", + "csv-error-send-test-failed": "Failed to send test email", + "csv-success-contacts-uploaded": "Successfully uploaded {count} email contacts", + "csv-success-test-email-sent": "Test email sent successfully to {email}" } } }, @@ -903,6 +931,10 @@ "first-israel-dashboard": { "name": "FIRST Israel Dashboard", "description": "Upload team information and export results from your FIRST Israel Dashboard account" + }, + "sendgrid": { + "name": "SendGrid", + "description": "Send event-related emails to teams using SendGrid email templates" } } }, diff --git a/apps/admin/locale/he.json b/apps/admin/locale/he.json index 6ed8e8ec0..974c79aae 100644 --- a/apps/admin/locale/he.json +++ b/apps/admin/locale/he.json @@ -896,6 +896,34 @@ "sfid-label": "מזהה אירוע של Salesforce", "sfid-placeholder": "הזן את מזהה ה-Salesforce Event ID שלך", "sfid-help": "המזהה הייחודי של האירוע שלך ב-Salesforce" + }, + "sendgrid": { + "template-id-label": "SendGrid Template ID", + "template-id-placeholder": "d-xxxxxxxxxxxxxxxxxxxxxxxx", + "template-id-help": "מזהה תבנית דינמית מ-SendGrid", + "from-address-label": "כתובת דוא״ל מ", + "from-address-placeholder": "noreply@example.com", + "from-address-help": "כתובת דוא״ל של משלח מאומת", + "test-email-address-label": "כתובת דוא״ל לבדיקה", + "test-email-address-placeholder": "admin@example.com", + "test-email-address-help": "לאן ישלחו דוא״ל בדיקה", + "send-test-email-button": "שלח דוא״ל בדיקה", + "email-contacts-title": "אנשי קשר בדוא״ל", + "csv-description": "העלה קובץ CSV עם עמודות: Team Number, Region, Recipient Name, Email Address", + "csv-requirements-title": "דרישות קובץ CSV", + "csv-requirement-header": "הקובץ חייב לכלול שורת כותרת", + "csv-requirement-columns": "עמודות: Team Number, Region, Recipient Name, Recipient Email", + "csv-requirement-encoding": "שמור כקובץ CSV בקידוד UTF-8", + "csv-example": "פורמט דוגמה", + "upload-csv-button": "העלה CSV", + "validation-template-id-required": "מזהה תבנית נדרש", + "validation-from-address-required": "דוא״ל תקין נדרש", + "validation-test-email-required": "דוא״ל תקין נדרש", + "csv-error-invalid-format": "אנא העלה קובץ CSV", + "csv-error-upload-failed": "העלאת אנשי קשר נכשלה", + "csv-error-send-test-failed": "שליחת דוא״ל בדיקה נכשלה", + "csv-success-contacts-uploaded": "העלאת {count} אנשי קשר בדוא״ל בהצלחה", + "csv-success-test-email-sent": "דוא״ל בדיקה נשלח בהצלחה אל {email}" } } }, @@ -903,6 +931,10 @@ "first-israel-dashboard": { "name": "לוח בקרה FIRST Israel", "description": "העלה מידע קבוצות וייצא תוצאות מחשבון לוח הבקרה של FIRST Israel שלך" + }, + "sendgrid": { + "name": "SendGrid", + "description": "שלח דוא״ל הקשור לאירועים לקבוצות באמצעות תבניות דוא״ל של SendGrid" } } }, diff --git a/apps/admin/locale/pl.json b/apps/admin/locale/pl.json index e12e904a5..0625f9e11 100644 --- a/apps/admin/locale/pl.json +++ b/apps/admin/locale/pl.json @@ -896,6 +896,34 @@ "sfid-label": "ID wydarzenia w Salesforce", "sfid-placeholder": "Wprowadź ID wydarzenia w Salesforce", "sfid-help": "Unikalny identyfikator Twojego wydarzenia w Salesforce" + }, + "sendgrid": { + "template-id-label": "SendGrid Template ID", + "template-id-placeholder": "d-xxxxxxxxxxxxxxxxxxxxxxxx", + "template-id-help": "Dynamiczny identyfikator szablonu z SendGrid", + "from-address-label": "Adres e-mail nadawcy", + "from-address-placeholder": "noreply@example.com", + "from-address-help": "Zweryfikowany adres e-mail nadawcy", + "test-email-address-label": "Testowy adres e-mail", + "test-email-address-placeholder": "admin@example.com", + "test-email-address-help": "Gdzie będą wysyłane testowe wiadomości e-mail", + "send-test-email-button": "Wyślij testową wiadomość e-mail", + "email-contacts-title": "Kontakty e-mail", + "csv-description": "Prześlij plik CSV z kolumnami: Team Number, Region, Recipient Name, Email Address", + "csv-requirements-title": "Wymagania dotyczące pliku CSV", + "csv-requirement-header": "Plik musi zawierać wiersz nagłówka", + "csv-requirement-columns": "Kolumny: Team Number, Region, Recipient Name, Recipient Email", + "csv-requirement-encoding": "Zapisane jako plik CSV w kodowaniu UTF-8", + "csv-example": "Format przykładowy", + "upload-csv-button": "Prześlij CSV", + "validation-template-id-required": "Wymagany identyfikator szablonu", + "validation-from-address-required": "Wymagany prawidłowy e-mail", + "validation-test-email-required": "Wymagany prawidłowy e-mail", + "csv-error-invalid-format": "Proszę przesłać plik CSV", + "csv-error-upload-failed": "Nie udało się przesłać kontaktów", + "csv-error-send-test-failed": "Nie udało się wysłać testową wiadomość e-mail", + "csv-success-contacts-uploaded": "Pomyślnie przesłano {count} kontaktów e-mail", + "csv-success-test-email-sent": "Testowa wiadomość e-mail została wysłana pomyślnie na adres {email}" } } }, @@ -903,6 +931,10 @@ "first-israel-dashboard": { "name": "FIRST Israel Dashboard", "description": "Prześlij informacje o drużynach i eksportuj wyniki ze swojego konta FIRST Israel Dashboard" + }, + "sendgrid": { + "name": "SendGrid", + "description": "Wyślij e-maile związane z wydarzeniem do drużyn za pomocą szablonów e-mail SendGrid" } } }, diff --git a/apps/admin/public/assets/integration-icons/sendgrid.svg b/apps/admin/public/assets/integration-icons/sendgrid.svg new file mode 100644 index 000000000..29a5c8ea0 --- /dev/null +++ b/apps/admin/public/assets/integration-icons/sendgrid.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid-settings.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid-settings.tsx new file mode 100644 index 000000000..cb86df829 --- /dev/null +++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid-settings.tsx @@ -0,0 +1,307 @@ +'use client'; + +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useTranslations } from 'next-intl'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import { Info as InfoIcon, Description as DescriptionIcon } from '@mui/icons-material'; +import { + Stack, + TextField, + Button, + Alert, + CircularProgress, + Box, + Typography, + Divider, + Paper, + List, + ListItem, + ListItemIcon, + ListItemText +} from '@mui/material'; +import { SendGridSettingsSchema } from '@lems/shared/integrations'; +import { IntegrationSettingsComponentProps } from './settings-factory'; + +interface SendGridFormValues { + templateId: string; + fromAddress: string; + testEmailAddress: string; +} + +export const SendGridSettings: React.FC = ({ + settings, + onSave, + isLoading = false, + showErrors = false +}) => { + const t = useTranslations('pages.events.integrations.detail-panel.settings.sendgrid'); + + const [formValues, setFormValues] = useState({ + templateId: '', + fromAddress: '', + testEmailAddress: '' + }); + const [errors, setErrors] = useState>({}); + const [csvError, setCsvError] = useState(''); + const [csvSuccess, setCsvSuccess] = useState(''); + const [isTestingEmail, setIsTestingEmail] = useState(false); + const fileInputRef = useRef(null); + const prevSettingsRef = useRef(null); + const hasInitializedRef = useRef(false); + + // Initialize form state from settings + useEffect(() => { + const settingsStr = JSON.stringify(settings); + if (!hasInitializedRef.current || prevSettingsRef.current !== settingsStr) { + setFormValues({ + templateId: (settings.templateId as string) || '', + fromAddress: (settings.fromAddress as string) || '', + testEmailAddress: (settings.testEmailAddress as string) || '' + }); + setErrors({}); + prevSettingsRef.current = settingsStr; + hasInitializedRef.current = true; + } + }, [settings]); + + // Validate and save when showErrors is true + useEffect(() => { + if (showErrors) { + try { + const validated = SendGridSettingsSchema.parse(formValues); + setErrors({}); + onSave(validated); + } catch (error) { + if (error instanceof Error) { + const message = error.message; + // Parse Zod error message to set individual field errors + if (message.includes('templateId')) + setErrors(e => ({ ...e, templateId: t('validation-template-id-required') })); + if (message.includes('fromAddress')) + setErrors(e => ({ ...e, fromAddress: t('validation-from-address-required') })); + if (message.includes('testEmailAddress')) + setErrors(e => ({ ...e, testEmailAddress: t('validation-test-email-required') })); + } + } + } + }, [showErrors, formValues, onSave, t]); + + const handleFieldChange = useCallback( + (field: keyof SendGridFormValues, value: string) => { + setFormValues(prev => ({ ...prev, [field]: value })); + if (errors[field]) setErrors(prev => ({ ...prev, [field]: undefined })); + }, + [errors] + ); + + const handleCSVUpload = async (file: File) => { + setCsvError(''); + setCsvSuccess(''); + + if (!file.name.endsWith('.csv')) { + setCsvError(t('csv-error-invalid-format')); + return; + } + + try { + const csvContent = await file.text(); + const eventId = window.location.pathname.split('/')[3]; + + const response = await fetch(`/api/integrations/sendgrid/${eventId}/upload-contacts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ csvContent }) + }); + + if (!response.ok) { + const error = await response.json(); + setCsvError(error.error || t('csv-error-upload-failed')); + return; + } + + const result = await response.json(); + setCsvSuccess(t('csv-success-contacts-uploaded', { count: result.count })); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } catch (error) { + setCsvError(error instanceof Error ? error.message : t('csv-error-upload-failed')); + } + }; + + const handleTestEmail = async () => { + setIsTestingEmail(true); + try { + const eventId = window.location.pathname.split('/')[3]; + const response = await fetch(`/api/integrations/sendgrid/${eventId}/send-test`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formValues) + }); + + if (!response.ok) { + const error = await response.json(); + setCsvError(error.error || t('csv-error-send-test-failed')); + return; + } + + setCsvSuccess(t('csv-success-test-email-sent', { email: formValues.testEmailAddress })); + } catch (error) { + setCsvError(error instanceof Error ? error.message : t('csv-error-send-test-failed')); + } finally { + setIsTestingEmail(false); + } + }; + + return ( + + + handleFieldChange('templateId', e.target.value)} + disabled={isLoading} + error={showErrors && !!errors.templateId} + helperText={showErrors && errors.templateId ? errors.templateId : t('template-id-help')} + size="small" + /> + + + + handleFieldChange('fromAddress', e.target.value)} + disabled={isLoading} + error={showErrors && !!errors.fromAddress} + helperText={ + showErrors && errors.fromAddress ? errors.fromAddress : t('from-address-help') + } + size="small" + /> + + + + handleFieldChange('testEmailAddress', e.target.value)} + disabled={isLoading} + error={showErrors && !!errors.testEmailAddress} + helperText={ + showErrors && errors.testEmailAddress + ? errors.testEmailAddress + : t('test-email-address-help') + } + size="small" + /> + + + + + + + + + + + + + + {t('email-contacts-title')} + + + {t('csv-description')} + + + + + + + + {t('csv-requirements-title')} + + + + + + + + + + + + + + + + + + + + + + + + {t('csv-example')} + + + + {`Team Number,Region,Recipient Name,Recipient Email +1234,North,John Doe,john@example.com +5678,South,Jane Smith,jane@example.com +9999,Central,Bob Johnson,bob@example.com`} + + + + + + + { + const file = e.target.files?.[0]; + if (file) handleCSVUpload(file); + }} + /> + + + {csvError && {csvError}} + {csvSuccess && {csvSuccess}} + + ); +}; diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx index dd5fdbb94..5785d3166 100644 --- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx +++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx @@ -2,6 +2,7 @@ import { IntegrationType } from '@lems/shared/integrations'; import { FirstIsraelDashboardSettings } from './first-israel-dashboard-settings'; +import { SendGridSettings } from './sendgrid-settings'; /** * Props passed to integration settings components @@ -26,7 +27,8 @@ export type IntegrationSettingsComponent = React.FC = { - 'first-israel-dashboard': FirstIsraelDashboardSettings + 'first-israel-dashboard': FirstIsraelDashboardSettings, + 'sendgrid': SendGridSettings }; /** diff --git a/apps/backend/.template.env b/apps/backend/.template.env index 46f9ab1cb..16faf8434 100644 --- a/apps/backend/.template.env +++ b/apps/backend/.template.env @@ -35,4 +35,7 @@ SCHEDULER_JWT_SECRET="" DIGITALOCEAN_ENDPOINT="" DIGITALOCEAN_SPACE="" DIGITALOCEAN_KEY="" -DIGITALOCEAN_SECRET="" \ No newline at end of file +DIGITALOCEAN_SECRET="" + +# SendGrid Email Integration +SENDGRID_API_KEY="" \ No newline at end of file diff --git a/apps/backend/src/routers/admin/events/settings/index.ts b/apps/backend/src/routers/admin/events/settings/index.ts index 5426b0320..04a8c3492 100644 --- a/apps/backend/src/routers/admin/events/settings/index.ts +++ b/apps/backend/src/routers/admin/events/settings/index.ts @@ -1,6 +1,8 @@ import express from 'express'; +import { IntegrationTypes } from '@lems/shared/integrations'; import db from '../../../../lib/database'; import { AdminEventRequest } from '../../../../types/express'; +import { publishEventResults } from '../../../integrations/sendgrid/publish'; import { makeAdminSettingsResponse, makeUpdateableEventSettings } from './util'; const router = express.Router({ mergeParams: true }); @@ -28,16 +30,41 @@ router.post('/complete', async (req: AdminEventRequest, res) => { }); router.post('/publish', async (req: AdminEventRequest, res) => { - const settings = await db.events.byId(req.eventId).getSettings(); - if (!settings.completed) { - res.json() - } + try { + const settings = await db.events.byId(req.eventId).getSettings(); + if (!settings.completed) { + res.status(400).json({ error: 'Event must be completed before publishing' }); + return; + } - const updatedSettings = await db.events.byId(req.eventId).updateSettings({ published: true }); - if (!updatedSettings) { - throw new Error('Failed to publish event'); + const updatedSettings = await db.events.byId(req.eventId).updateSettings({ published: true }); + if (!updatedSettings) { + throw new Error('Failed to publish event'); + } + + // Send emails if SendGrid integration is enabled + const integrations = await db.integrations.byEventId(req.eventId).getAll(); + const sendgridIntegration = integrations.find( + i => i.integration_type === IntegrationTypes.SENDGRID && i.enabled + ); + + if (sendgridIntegration) { + try { + await publishEventResults({ + eventId: req.eventId, + settings: sendgridIntegration.settings + }); + } catch (error) { + console.error('Error sending SendGrid emails:', error); + // Don't fail the publish - log the error but continue + } + } + + res.json({ success: true }); + } catch (error) { + console.error('Error publishing event:', error); + res.status(500).json({ error: 'Failed to publish event' }); } - res.json({ success: true }); }); export default router; diff --git a/apps/backend/src/routers/integrations/index.ts b/apps/backend/src/routers/integrations/index.ts index 1577b07b0..9484ed650 100644 --- a/apps/backend/src/routers/integrations/index.ts +++ b/apps/backend/src/routers/integrations/index.ts @@ -1,8 +1,10 @@ import express from 'express'; import firstIsraelDashboardRouter from './first-israel-dashboard'; +import sendgridRouter from './sendgrid'; const router = express.Router({ mergeParams: true }); router.use('/first-israel-dashboard', firstIsraelDashboardRouter); +router.use('/sendgrid', sendgridRouter); export default router; diff --git a/apps/backend/src/routers/integrations/sendgrid/index.ts b/apps/backend/src/routers/integrations/sendgrid/index.ts new file mode 100644 index 000000000..ed7ef86cc --- /dev/null +++ b/apps/backend/src/routers/integrations/sendgrid/index.ts @@ -0,0 +1,154 @@ +import express from 'express'; +import { parse } from 'csv-parse/sync'; +import { AdminEventRequest } from '../../../types/express'; +import { requirePermission } from '../../../routers/admin/middleware/require-permission'; +import db from '../../../lib/database'; +import { sendEmailWithSendGrid } from './sendgrid-lib'; +import { generatePlaceholderPDF } from './placeholder-generator'; +import { publishEventResults } from './publish'; +import { CSVRecord } from './types'; + +const router = express.Router({ mergeParams: true }); + +interface PublishRequest extends AdminEventRequest { + body: { + settings: Record; + }; +} + +router.post( + '/:eventId/upload-contacts', + requirePermission('MANAGE_EVENT_DETAILS'), + async (req: AdminEventRequest, res) => { + try { + const { csvContent } = req.body; + if (!csvContent) { + res.status(400).json({ error: 'No CSV content provided' }); + return; + } + + const csvText = + typeof csvContent === 'string' ? csvContent : Buffer.from(csvContent).toString('utf-8'); + + const records = parse(csvText, { + columns: ['team_number', 'region', 'recipient_name', 'recipient_email'], + skip_empty_lines: true, + from_line: 2 // Skip header row + }) as CSVRecord[]; + + if (!Array.isArray(records) || records.length === 0) { + res.status(400).json({ error: 'CSV file is empty or invalid' }); + return; + } + + // Validate email format + const validRecords = records.filter((record: CSVRecord) => { + const email = record.recipient_email?.toString().trim(); + return email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + }); + + if (validRecords.length === 0) { + res.status(400).json({ error: 'No valid email addresses found in CSV' }); + return; + } + + // Get current integration and update with base64-encoded CSV data + const eventId = req.params.eventId; + const integration = await db.integrations.byType(eventId, 'sendgrid').get(); + if (!integration) { + res.status(404).json({ error: 'SendGrid integration not found' }); + return; + } + + // Encode CSV content to base64 + const emailContactsData = Buffer.from(csvContent).toString('base64'); + + // Update integration settings with encoded data + const updatedSettings = { + ...integration.settings, + emailContactsData + }; + + await db.integrations.byId(integration.pk.toString()).update({ settings: updatedSettings }); + + res.json({ count: validRecords.length }); + } catch (error) { + console.error('Error uploading contacts:', error); + res.status(500).json({ error: 'Failed to process CSV file' }); + } + } +); + +router.post( + '/:eventId/send-test', + requirePermission('MANAGE_EVENT_DETAILS'), + async (req: AdminEventRequest, res) => { + try { + const { templateId, fromAddress, testEmailAddress } = req.body; + + if (!templateId || !fromAddress || !testEmailAddress) { + res.status(400).json({ error: 'Missing required settings' }); + return; + } + + const apiKey = process.env.SENDGRID_API_KEY; + if (!apiKey) { + res.status(500).json({ error: 'SendGrid API key not configured' }); + return; + } + + // Generate placeholder PDF + const pdfBuffer = await generatePlaceholderPDF(); + const pdfBase64 = pdfBuffer.toString('base64'); + + await sendEmailWithSendGrid({ + apiKey, + from: fromAddress, + to: testEmailAddress, + templateId, + dynamicTemplateData: { + eventName: 'Test Event', + teamNumber: 0, + recipientName: 'Test User' + }, + attachments: [ + { + filename: 'scoresheet.pdf', + content: pdfBase64, + type: 'application/pdf' + }, + { + filename: 'rubric.pdf', + content: pdfBase64, + type: 'application/pdf' + } + ] + }); + + res.json({ success: true }); + } catch (error) { + console.error('Error sending test email:', error); + res + .status(500) + .json({ error: error instanceof Error ? error.message : 'Failed to send test email' }); + } + } +); + +// Publish event results via email +router.post('/:eventId/publish', async (req: PublishRequest, res) => { + try { + const result = await publishEventResults({ + eventId: req.params.eventId, + settings: req.body.settings + }); + res.json(result); + } catch (error) { + console.error('Error publishing event results:', error); + res.status(500).json({ + error: error instanceof Error ? error.message : 'Failed to publish event results' + }); + } +}); + +export default router; diff --git a/apps/backend/src/routers/integrations/sendgrid/placeholder-generator.ts b/apps/backend/src/routers/integrations/sendgrid/placeholder-generator.ts new file mode 100644 index 000000000..f6e0b7501 --- /dev/null +++ b/apps/backend/src/routers/integrations/sendgrid/placeholder-generator.ts @@ -0,0 +1,52 @@ +import puppeteer from 'puppeteer'; + +/** + * TODO: Replace this placeholder with actual scoresheet/rubric PDF generation + * This should query the database for event results and generate PDFs from templates + */ +export async function generatePlaceholderPDF(): Promise { + let browser; + try { + browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + const page = await browser.newPage(); + + // Generate empty A4 page with basic styling + await page.setContent(` + + + + + + +
+ + + `); + + const pdf = await page.pdf({ + format: 'A4', + margin: { top: 0, right: 0, bottom: 0, left: 0 } + }); + + return pdf || Buffer.alloc(0); + } finally { + if (browser) { + await browser.close(); + } + } +} diff --git a/apps/backend/src/routers/integrations/sendgrid/publish.ts b/apps/backend/src/routers/integrations/sendgrid/publish.ts new file mode 100644 index 000000000..7e317ec35 --- /dev/null +++ b/apps/backend/src/routers/integrations/sendgrid/publish.ts @@ -0,0 +1,95 @@ +import { parse } from 'csv-parse/sync'; +import db from '../../../lib/database'; +import { sendEmailWithSendGrid } from './sendgrid-lib'; +import { generatePlaceholderPDF } from './placeholder-generator'; +import { CSVRecord } from './types'; + +export interface SendGridPublishOptions { + eventId: string; + settings: Record; +} + +export async function publishEventResults(options: SendGridPublishOptions) { + const { eventId, settings } = options; + + if (!settings) { + throw new Error('Missing integration settings'); + } + + const apiKey = process.env.SENDGRID_API_KEY; + if (!apiKey) { + throw new Error('SendGrid API key not configured'); + } + + const { templateId, fromAddress, emailContactsData } = settings; + if (!templateId || !fromAddress) { + throw new Error('SendGrid integration missing required settings'); + } + + if (!emailContactsData) { + throw new Error('No email contacts configured for event'); + } + + // Decode base64 CSV data + const csvContent = Buffer.from(emailContactsData as string, 'base64').toString('utf-8'); + const contacts = parse(csvContent, { + columns: ['team_number', 'region', 'recipient_name', 'recipient_email'], + skip_empty_lines: true, + from_line: 2 // Skip header row + }); + + if (!Array.isArray(contacts) || contacts.length === 0) { + throw new Error('No email contacts in CSV data'); + } + + const event = await db.events.byId(eventId).get(); + const pdfBuffer = await generatePlaceholderPDF(); + const pdfBase64 = pdfBuffer.toString('base64'); + + const failedEmails: string[] = []; + + // Send emails to each contact + for (const contact of contacts) { + try { + const typedContact = contact as CSVRecord; + const teamNumber = parseInt(typedContact.team_number, 10); + await sendEmailWithSendGrid({ + apiKey, + from: fromAddress as string, + to: typedContact.recipient_email?.toString().trim() || '', + toName: typedContact.recipient_name?.toString().trim(), + templateId: templateId as string, + dynamicTemplateData: { + eventName: event?.name || 'Event', + teamNumber, + recipientName: typedContact.recipient_name, + region: typedContact.region + }, + attachments: [ + { + filename: `team-${teamNumber}-scoresheet.pdf`, + content: pdfBase64, + type: 'application/pdf' + }, + { + filename: `team-${teamNumber}-rubric.pdf`, + content: pdfBase64, + type: 'application/pdf' + } + ] + }); + } catch (error) { + const typedContact = contact as CSVRecord; + const email = typedContact.recipient_email; + failedEmails.push(email); + console.error(`Failed to send email to ${email}:`, error); + } + } + + return { + success: true, + total: contacts.length, + failed: failedEmails.length, + failedEmails: failedEmails.length > 0 ? failedEmails : undefined + }; +} diff --git a/apps/backend/src/routers/integrations/sendgrid/sendgrid-lib.ts b/apps/backend/src/routers/integrations/sendgrid/sendgrid-lib.ts new file mode 100644 index 000000000..f6bf66638 --- /dev/null +++ b/apps/backend/src/routers/integrations/sendgrid/sendgrid-lib.ts @@ -0,0 +1,46 @@ +import sgMail from '@sendgrid/mail'; + +interface EmailAttachment { + filename: string; + content: string; // base64 encoded + type: string; +} + +interface SendEmailParams { + apiKey: string; + from: string; + to: string; + toName?: string; + templateId: string; + dynamicTemplateData: Record; + attachments?: EmailAttachment[]; +} + +export async function sendEmailWithSendGrid(params: SendEmailParams): Promise { + sgMail.setApiKey(params.apiKey); + + const mail = { + to: { + email: params.to, + name: params.toName + }, + from: params.from, + templateId: params.templateId, + dynamicTemplateData: params.dynamicTemplateData, + attachments: params.attachments?.map(att => ({ + filename: att.filename, + content: att.content, + type: att.type, + disposition: 'attachment' as const + })) + }; + + try { + await sgMail.send(mail); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to send email to ${params.to}: ${error.message}`); + } + throw error; + } +} diff --git a/apps/backend/src/routers/integrations/sendgrid/types.ts b/apps/backend/src/routers/integrations/sendgrid/types.ts new file mode 100644 index 000000000..4259aa340 --- /dev/null +++ b/apps/backend/src/routers/integrations/sendgrid/types.ts @@ -0,0 +1,6 @@ +export interface CSVRecord { + team_number: string; + region: string; + recipient_name: string; + recipient_email: string; +} diff --git a/compose.yml b/compose.yml index a68adacdb..adf762110 100644 --- a/compose.yml +++ b/compose.yml @@ -42,6 +42,9 @@ services: - DIGITALOCEAN_KEY=${DIGITALOCEAN_KEY} - DIGITALOCEAN_SECRET=${DIGITALOCEAN_SECRET} + # Integrations + - SENDGRID_API_KEY=${SENDGRID_API_KEY} + admin: image: ${REGISTRY}/lems:admin-${IMAGE_TAG} ports: diff --git a/libs/shared/src/lib/integrations.ts b/libs/shared/src/lib/integrations.ts index ba9e2007d..73906d01d 100644 --- a/libs/shared/src/lib/integrations.ts +++ b/libs/shared/src/lib/integrations.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; export const IntegrationTypes = { - FIRST_ISRAEL_DASHBOARD: 'first-israel-dashboard' + FIRST_ISRAEL_DASHBOARD: 'first-israel-dashboard', + SENDGRID: 'sendgrid' } as const; export type IntegrationType = (typeof IntegrationTypes)[keyof typeof IntegrationTypes]; @@ -12,12 +13,33 @@ export const FirstIsraelDashboardSettingsSchema = z.object({ export type FirstIsraelDashboardSettings = z.infer; -export const IntegrationSettingsSchema = z.union([FirstIsraelDashboardSettingsSchema]); +export const SendGridSettingsSchema = z.object({ + templateId: z.string().nullable().default(null).describe('SendGrid Dynamic Template ID'), + fromAddress: z + .email('Must be a valid email') + .nullable() + .default(null) + .describe('Sender email address'), + testEmailAddress: z + .email('Must be a valid email') + .nullable() + .default(null) + .describe('Test recipient email address'), + emailContactsData: z.string().optional().describe('Base64-encoded CSV contact data') +}); + +export type SendGridSettings = z.infer; + +export const IntegrationSettingsSchema = z.union([ + FirstIsraelDashboardSettingsSchema, + SendGridSettingsSchema +]); export type IntegrationSettings = z.infer; const INTEGRATION_LOGOS: Record = { - [IntegrationTypes.FIRST_ISRAEL_DASHBOARD]: 'integration-icons/first-israel-dashboard.svg' + [IntegrationTypes.FIRST_ISRAEL_DASHBOARD]: 'integration-icons/first-israel-dashboard.svg', + [IntegrationTypes.SENDGRID]: 'integration-icons/sendgrid.svg' } as const; export interface IntegrationConfig { @@ -31,6 +53,11 @@ const INTEGRATIONS_REGISTRY: Record = { type: IntegrationTypes.FIRST_ISRAEL_DASHBOARD, settingsSchema: FirstIsraelDashboardSettingsSchema, logoAsset: INTEGRATION_LOGOS[IntegrationTypes.FIRST_ISRAEL_DASHBOARD] + }, + [IntegrationTypes.SENDGRID]: { + type: IntegrationTypes.SENDGRID, + settingsSchema: SendGridSettingsSchema, + logoAsset: INTEGRATION_LOGOS[IntegrationTypes.SENDGRID] } }; diff --git a/package-lock.json b/package-lock.json index 2c9c5c0e5..9cc1c5d83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@mui/stylis-plugin-rtl": "^7.3.6", "@mui/x-data-grid": "^8.25.0", "@mui/x-date-pickers": "^8.24.0", + "@sendgrid/mail": "^8.1.6", "@uiw/react-color": "^2.9.2", "@uiw/react-signature": "^1.3.3", "axios": "^1.13.2", @@ -149,6 +150,7 @@ "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.0.tgz", "integrity": "sha512-N/nZXGNBMoHnshNaHXxHZoC42BcIjRqD4XgpmNBPhueoWIbp17VIJe/sGysFNQo1w7DVD78K6gVsNMO87nfgRQ==", "license": "MIT", + "peer": true, "workspaces": [ "dist", "codegen", @@ -248,6 +250,7 @@ "resolved": "https://registry.npmjs.org/@apollo/server/-/server-5.2.0.tgz", "integrity": "sha512-OEAl5bwVitkvVkmZlgWksSnQ10FUr6q2qJMdkexs83lsvOGmd/y81X5LoETmKZux8UiQsy/A/xzP00b8hTHH/w==", "license": "MIT", + "peer": true, "dependencies": { "@apollo/cache-control-types": "^1.0.3", "@apollo/server-gateway-interface": "^2.0.0", @@ -1866,6 +1869,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -3755,6 +3759,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -3920,6 +3925,7 @@ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", @@ -3960,6 +3966,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -3997,6 +4004,7 @@ "resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz", "integrity": "sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/utils": "^1.2.1", "html-tokenize": "^2.0.0", @@ -4023,6 +4031,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -4897,6 +4906,7 @@ "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.37.3.tgz", "integrity": "sha512-rNJjwsYGhcZRdZ2FnyU6ss06xQaZ4UordyvOhp7+b/bEqQiEBpMOLJjuUr48Z6T7zEbZBnzCJpIJyXNqlcfQeA==", "license": "MIT", + "peer": true, "dependencies": { "@graphiql/toolkit": "^0.11.3", "@radix-ui/react-dialog": "^1.1", @@ -6663,6 +6673,7 @@ "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" @@ -6806,6 +6817,7 @@ "integrity": "sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime": "0.21.6", "@module-federation/webpack-bundler-runtime": "0.21.6" @@ -6969,6 +6981,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -7134,6 +7147,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz", "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.6", @@ -11428,6 +11442,7 @@ "integrity": "sha512-uDxPQsPh/+2DnOISuKnUiXZ9M0y2G1BOsI0IesxPJGp42ME2QW7axbJfUqD3bwp4bi3RN2zqh56NgxU/XETQvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.0", @@ -11524,6 +11539,44 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, + "node_modules/@sendgrid/client": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz", + "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==", + "license": "MIT", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.12.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz", + "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==", + "license": "MIT", + "dependencies": { + "@sendgrid/client": "^8.1.5", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -12483,6 +12536,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -12609,6 +12663,7 @@ "integrity": "sha512-VQ0hJ5jX31TVv/fhZx4xJRzd8pwn6VvzYd2tGOHHr2TfXGCBixZoqdPDXTiEoJLCTS2MmvBf6zyQZZ0M8aGQCQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@swc-node/core": "^1.14.1", "@swc-node/sourcemap-support": "^0.6.1", @@ -12740,6 +12795,7 @@ "integrity": "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -12962,6 +13018,7 @@ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -13180,7 +13237,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -13295,8 +13351,7 @@ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/eslint": { "version": "9.6.1", @@ -13515,6 +13570,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -13836,6 +13892,7 @@ "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -14808,7 +14865,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", @@ -14828,7 +14884,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", @@ -14857,7 +14912,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -14869,7 +14923,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tinyrainbow": "^3.0.3" }, @@ -14884,7 +14937,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" @@ -14900,7 +14952,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", @@ -14917,7 +14968,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "funding": { "url": "https://opencollective.com/vitest" } @@ -14929,7 +14979,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" @@ -15340,6 +15389,7 @@ "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -15365,6 +15415,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -15441,6 +15492,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15820,7 +15872,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=12" } @@ -16168,6 +16219,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -16248,6 +16300,7 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -16609,6 +16662,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -16919,7 +16973,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -18099,7 +18152,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/csv-parse": { "version": "6.1.0", @@ -18303,6 +18357,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -18331,7 +18386,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debounce-promise": { "version": "3.1.2", @@ -18443,7 +18499,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18682,7 +18737,8 @@ "version": "0.0.1534754", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz", "integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/diff": { "version": "4.0.2", @@ -19216,8 +19272,7 @@ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -19393,6 +19448,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -19493,6 +19549,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -19594,6 +19651,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -20191,7 +20249,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=12.0.0" } @@ -20201,6 +20258,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -20604,6 +20662,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -20971,6 +21030,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -21841,6 +21901,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -21897,6 +21958,7 @@ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.6.tgz", "integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -22543,6 +22605,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -24567,6 +24630,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -25008,6 +25072,7 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" } @@ -25958,7 +26023,8 @@ "version": "0.52.2", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/monaco-graphql": { "version": "1.7.3", @@ -26281,6 +26347,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.1.1", "@swc/helpers": "0.5.15", @@ -26626,6 +26693,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -26939,8 +27007,7 @@ "https://opencollective.com/debug" ], "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/on-exit-leak-free": { "version": "2.1.2", @@ -27472,8 +27539,7 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/pend": { "version": "1.2.0", @@ -27492,6 +27558,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", "integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.10.0", "pg-pool": "^3.11.0", @@ -27888,6 +27955,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -28667,6 +28735,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -29072,6 +29141,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -29099,6 +29169,7 @@ "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.1.0-rc.1.tgz", "integrity": "sha512-wCt6g+cRh8g32QT18/9blfQHywGjYu+4FlEc3CW1mx3pPxYzZZl1y+VtqxRgnKKBCFLIGUYxog4j4rs5YS86hw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" } @@ -29108,6 +29179,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -29151,7 +29223,8 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -29417,7 +29490,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -29725,6 +29799,7 @@ "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -30741,6 +30816,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -30862,6 +30938,7 @@ "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -30883,6 +30960,7 @@ "integrity": "sha512-wH3CbOThHYGX0bUyqFf7laLKyhVWIFc2lHynitkqMIUCtX2ixH9mQh0bN7+hkUu5BFt/SXvEMjFbkEbBMpQiSQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", @@ -31934,8 +32012,7 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC", - "optional": true, - "peer": true + "optional": true }, "node_modules/signal-exit": { "version": "3.0.7", @@ -32002,6 +32079,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" @@ -32283,8 +32361,7 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/standard-as-callback": { "version": "2.1.0", @@ -32307,8 +32384,7 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", @@ -32802,7 +32878,8 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/supports-color": { "version": "7.2.0", @@ -33009,6 +33086,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -33282,8 +33360,7 @@ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/tinycolor2": { "version": "1.6.0", @@ -33298,7 +33375,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -33340,7 +33416,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=14.0.0" } @@ -33544,6 +33619,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -33627,7 +33703,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsscmp": { "version": "1.0.6", @@ -33645,6 +33722,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -33805,6 +33883,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -34342,595 +34421,6 @@ "d3-timer": "^3.0.1" } }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/vitest": { "version": "4.0.16", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", @@ -34938,7 +34428,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -35018,7 +34507,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=12" }, @@ -35108,6 +34596,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -35264,6 +34753,7 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -36024,7 +35514,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" @@ -36192,6 +35681,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -36271,6 +35761,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -36370,6 +35861,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index b306543bc..394fbe174 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@mui/stylis-plugin-rtl": "^7.3.6", "@mui/x-data-grid": "^8.25.0", "@mui/x-date-pickers": "^8.24.0", + "@sendgrid/mail": "^8.1.6", "@uiw/react-color": "^2.9.2", "@uiw/react-signature": "^1.3.3", "axios": "^1.13.2",