diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 5faa363..9604f24 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -90,13 +90,12 @@ func run(buildingID, districtID, recipients, sender, password, smtpServer, subje } if icsFlag { - if icsOutputPath == "" { - icsOutputPath = fmt.Sprintf("lunch_menu_%s_to_%s.ics", start.Format("01-02-2006"), end.Format("01-02-2006")) + outputPath := fmt.Sprintf("lunch_menu_%s_to_%s.ics", start.Format("01-02-2006"), end.Format("01-02-2006")) + _, err := ics.GenerateICSFile(buildingID, districtID, startDate, endDate, outputPath, debugFlag) + if err != nil { + return fmt.Errorf("failed to generate ICS file: %v", err) } - if err := ics.GenerateICSFile(buildingID, districtID, start.Format("01-02-2006"), end.Format("01-02-2006"), icsOutputPath, debugFlag); err != nil { - return fmt.Errorf("creating ICS file: %w", err) - } - fmt.Printf("ICS file created at: %s\n", icsOutputPath) + fmt.Printf("ICS file generated successfully: %s\n", outputPath) } return nil diff --git a/cmd/web/main.go b/cmd/web/main.go new file mode 100644 index 0000000..eb09d3d --- /dev/null +++ b/cmd/web/main.go @@ -0,0 +1,225 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime/debug" + "strings" + + "github.com/asachs01/school_menu_connector/internal/menu" + "github.com/asachs01/school_menu_connector/internal/ics" + mailjet "github.com/mailjet/mailjet-apiv3-go/v4" +) + +// Create a custom logger +var ( + infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) + errorLog = log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) +) + +var senderEmail string + +func init() { + senderEmail = os.Getenv("SENDER_EMAIL") + if senderEmail == "" { + senderEmail = "noreply@schoolmenuconnector.com" + } +} + +func main() { + // Serve static files + fs := http.FileServer(http.Dir("./web")) + http.Handle("/", fs) + + // API endpoint + http.HandleFunc("/api/generate", logRequest(handleGenerate)) + + infoLog.Println("Starting server on :8080") + err := http.ListenAndServe(":8080", nil) + errorLog.Fatal(err) +} + +func logRequest(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + infoLog.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI()) + next.ServeHTTP(w, r) + } +} + +func handleGenerate(w http.ResponseWriter, r *http.Request) { + defer func() { + if r := recover(); r != nil { + errorLog.Printf("panic: %v\n%s", r, debug.Stack()) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + }() + + if r.Method != http.MethodPost { + errorLog.Printf("Method not allowed: %s", r.Method) + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var data map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + errorLog.Printf("Invalid request body: %v", err) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + buildingID, ok := data["buildingId"].(string) + if !ok { + errorLog.Print("Missing or invalid buildingId") + http.Error(w, "Missing or invalid buildingId", http.StatusBadRequest) + return + } + + districtID, ok := data["districtId"].(string) + if !ok { + errorLog.Print("Missing or invalid districtId") + http.Error(w, "Missing or invalid districtId", http.StatusBadRequest) + return + } + + startDate, ok := data["startDate"].(string) + if !ok { + errorLog.Print("Missing or invalid startDate") + http.Error(w, "Missing or invalid startDate", http.StatusBadRequest) + return + } + + endDate, ok := data["endDate"].(string) + if !ok { + errorLog.Print("Missing or invalid endDate") + http.Error(w, "Missing or invalid endDate", http.StatusBadRequest) + return + } + + action, ok := data["action"].(string) + if !ok { + errorLog.Print("Missing or invalid action") + http.Error(w, "Missing or invalid action", http.StatusBadRequest) + return + } + + menuData, err := menu.Fetch(buildingID, districtID, startDate, endDate, false) + if err != nil { + errorLog.Printf("Error fetching menu: %v", err) + http.Error(w, fmt.Sprintf("Error fetching menu: %v", err), http.StatusInternalServerError) + return + } + + switch action { + case "email": + message, err := handleEmail(data, menuData) + if err != nil { + errorLog.Printf("Error handling email: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + response := map[string]string{"message": message} + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + errorLog.Printf("Error encoding response: %v", err) + http.Error(w, "Error encoding response", http.StatusInternalServerError) + } + case "ics": + if err := handleICS(w, data, menuData); err != nil { + errorLog.Printf("Error handling ICS: %v", err) + http.Error(w, fmt.Sprintf("Error generating calendar file: %v", err), http.StatusInternalServerError) + } + // Don't write anything else here + default: + errorLog.Printf("Invalid action: %s", action) + http.Error(w, "Invalid action", http.StatusBadRequest) + } +} + +func handleEmail(data map[string]interface{}, menuData *menu.Menu) (string, error) { + recipients, ok := data["recipients"].(string) + if !ok || recipients == "" { + return "", fmt.Errorf("missing or invalid recipients") + } + + recipientList := strings.Split(recipients, ",") + + mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE")) + + var recipientsV31 mailjet.RecipientsV31 + for _, recipient := range recipientList { + recipientsV31 = append(recipientsV31, mailjet.RecipientV31{ + Email: strings.TrimSpace(recipient), + }) + } + + messagesInfo := []mailjet.InfoMessagesV31{ + { + From: &mailjet.RecipientV31{ + Email: senderEmail, + Name: "School Menu Connector", + }, + To: &recipientsV31, + Subject: "School Menu", + TextPart: menuData.GetLunchMenuString(), + HTMLPart: "
" + menuData.GetLunchMenuString() + "", + }, + } + + messages := mailjet.MessagesV31{Info: messagesInfo} + res, err := mailjetClient.SendMailV31(&messages) + if err != nil { + errorLog.Printf("Mailjet API error: %v", err) + return "", fmt.Errorf("failed to send email: %v", err) + } + + // Log the Mailjet response + infoLog.Printf("Mailjet response: %+v", res) + + return "Email sent successfully", nil +} + +func handleICS(w http.ResponseWriter, data map[string]interface{}, menuData *menu.Menu) error { + buildingID := data["buildingId"].(string) + districtID := data["districtId"].(string) + startDate := data["startDate"].(string) + endDate := data["endDate"].(string) + + infoLog.Printf("Generating ICS file for buildingID: %s, districtID: %s, startDate: %s, endDate: %s", + buildingID, districtID, startDate, endDate) + + icsContent, err := ics.GenerateICSFile(buildingID, districtID, startDate, endDate, "", false) + if err != nil { + errorLog.Printf("Failed to generate ICS file: %v", err) + return fmt.Errorf("failed to generate ICS file: %w", err) + } + + infoLog.Printf("ICS file generated successfully, content length: %d bytes", len(icsContent)) + + filename := fmt.Sprintf("lunch_menu_%s_to_%s.ics", startDate, endDate) + + infoLog.Printf("Setting headers: Content-Type: %s, Content-Disposition: %s, Content-Length: %d", + "text/calendar", fmt.Sprintf("attachment; filename=\"%s\"", filename), len(icsContent)) + + w.Header().Set("Content-Type", "text/calendar") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(icsContent))) + + n, err := w.Write(icsContent) + if err != nil { + errorLog.Printf("Error writing ICS content to response: %v", err) + return fmt.Errorf("error writing ICS content to response: %w", err) + } + infoLog.Printf("Wrote %d bytes to response", n) + + // Log the first 100 characters of the ICS content + if len(icsContent) > 100 { + infoLog.Printf("First 100 characters of ICS content: %s", string(icsContent[:100])) + } else { + infoLog.Printf("ICS content: %s", string(icsContent)) + } + + return nil +} diff --git a/go.mod b/go.mod index bc60343..9f9c797 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,8 @@ module github.com/asachs01/school_menu_connector go 1.21.5 require github.com/arran4/golang-ical v0.3.1 + +require ( + github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 // indirect + github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1 +) diff --git a/go.sum b/go.sum index e4d26be..036c009 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 h1:/gjowTurgK4iqLzVAQmjtcldyaW6tbJNA4PzZsuj2Ks= +github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0/go.mod h1:Nw3mVzRxV0CVDTlzaRcADGKt4PMNbT7gYIyEtjMrVIM= +github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1 h1:VwdxYT1lPOIBZolqNtN6GcpdOySgHhCFQNsbN5P7uh8= +github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1/go.mod h1:2SU3t6eh/uK6BSeBmdhpIUau99L4iPlIfbx4o4pAUQs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/ics/generate.go b/internal/ics/generate.go index 1fdd9bc..d3b613e 100644 --- a/internal/ics/generate.go +++ b/internal/ics/generate.go @@ -9,14 +9,14 @@ import ( "github.com/asachs01/school_menu_connector/internal/menu" ) -func GenerateICSFile(buildingID, districtID, startDateStr, endDateStr, outputPath string, debug bool) error { - start, err := time.Parse("01-02-2006", startDateStr) +func GenerateICSFile(buildingID, districtID, startDate, endDate string, outputPath string, debug bool) ([]byte, error) { + start, err := time.Parse("01-02-2006", startDate) if err != nil { - return fmt.Errorf("invalid start date: %w", err) + return nil, fmt.Errorf("invalid start date: %w", err) } - end, err := time.Parse("01-02-2006", endDateStr) + end, err := time.Parse("01-02-2006", endDate) if err != nil { - return fmt.Errorf("invalid end date: %w", err) + return nil, fmt.Errorf("invalid end date: %w", err) } if debug { @@ -61,11 +61,16 @@ func GenerateICSFile(buildingID, districtID, startDateStr, endDateStr, outputPat } } - file, err := os.Create(outputPath) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) + icsContent := cal.Serialize() + icsBytes := []byte(icsContent) + + // If outputPath is provided, write to file (for CLI use) + if outputPath != "" { + err := os.WriteFile(outputPath, icsBytes, 0644) + if err != nil { + return nil, fmt.Errorf("failed to write ICS file: %w", err) + } } - defer file.Close() - return cal.SerializeTo(file) + return icsBytes, nil } diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..0375725 --- /dev/null +++ b/web/app.js @@ -0,0 +1,115 @@ +document.addEventListener('DOMContentLoaded', function() { + const form = document.getElementById('menuForm'); + const submitButton = document.getElementById('submitButton'); + const actionSelect = document.getElementById('action'); + const emailFields = document.getElementById('emailFields'); + const dateRangeSelect = document.getElementById('dateRange'); + const datePicker = document.getElementById('datePicker'); + + // Initialize Flatpickr + let fpicker = flatpickr(datePicker, { + mode: "single", + dateFormat: "Y-m-d" + }); + + // Function to toggle email fields visibility + function toggleEmailFields() { + emailFields.style.display = actionSelect.value === 'email' ? 'block' : 'none'; + } + + // Initialize email fields visibility + toggleEmailFields(); + + // Update date picker based on date range selection + dateRangeSelect.addEventListener('change', function() { + switch(this.value) { + case 'day': + fpicker.set('mode', 'single'); + break; + case 'week': + fpicker.set('mode', 'range'); + break; + case 'month': + fpicker.set('mode', 'range'); + break; + } + fpicker.clear(); + }); + + actionSelect.addEventListener('change', toggleEmailFields); + + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + // Change button text and disable it + submitButton.textContent = 'Processing...'; + submitButton.disabled = true; + + const formData = new FormData(form); + const data = Object.fromEntries(formData); + + // Add date range to data + const selectedDates = fpicker.selectedDates; + data.startDate = formatDate(selectedDates[0]); + data.endDate = selectedDates[1] ? formatDate(selectedDates[1]) : data.startDate; + + try { + console.log('Sending request with data:', data); + const response = await fetch('/api/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + console.log('Response status:', response.status); + console.log('Response headers:', Object.fromEntries(response.headers.entries())); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error response:', errorText); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); + } + + if (data.action === 'ics') { + const contentType = response.headers.get('Content-Type'); + console.log('Content-Type:', contentType); + + const text = await response.text(); + console.log('Response as text:', text); + + const blob = new Blob([text], {type: 'text/calendar'}); + console.log('Blob size:', blob.size); + console.log('Blob type:', blob.type); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `lunch_menu_${data.startDate}_to_${data.endDate}.ics`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.getElementById('result').textContent = 'Calendar file downloaded successfully.'; + } else { + const result = await response.json(); + document.getElementById('result').textContent = result.message; + } + } catch (error) { + console.error('Error:', error); + document.getElementById('result').textContent = `An error occurred: ${error.message}`; + } finally { + // Reset button text and re-enable it + submitButton.textContent = 'Submit'; + submitButton.disabled = false; + } + }); + + function formatDate(date) { + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const year = date.getFullYear(); + return `${month}-${day}-${year}`; + } +}); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d1e475b --- /dev/null +++ b/web/index.html @@ -0,0 +1,60 @@ + + + + + +