From 7e119ab943f71db89b274d6d4f7cffea4f431922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Dvo=C5=99=C3=A1k?= <150935816+Dvorinka@users.noreply.github.com> Date: Tue, 27 May 2025 08:56:21 +0200 Subject: [PATCH] Add files via upload --- admin-dashboard.html | 39 +- admin.html | 2 +- banner.go | 26 +- kontakt/Makefile | 44 +- kontakt/contact-scrape.go | 836 +++++++++++++++++++------------------- 5 files changed, 471 insertions(+), 476 deletions(-) diff --git a/admin-dashboard.html b/admin-dashboard.html index 2516a0e..c4440f9 100644 --- a/admin-dashboard.html +++ b/admin-dashboard.html @@ -710,6 +710,7 @@ } } + // Save banner async function saveBanner(event) { event.preventDefault(); @@ -729,39 +730,33 @@ formData.append('text', document.getElementById('bannerText').value); formData.append('link', document.getElementById('bannerLink').value); - // Create style object with default values - const style = { - backgroundColor: document.getElementById('bannerBgColor').value, - textColor: document.getElementById('bannerTextColor').value, - textAlign: document.getElementById('bannerTextAlign').value, - fontSize: document.getElementById('bannerFontSize').value || '16px', - padding: document.getElementById('bannerPadding').value || '0px', - margin: document.getElementById('bannerMargin').value || '0px', - borderRadius: document.getElementById('bannerBorderRadius').value || '0px', - isVisible: document.getElementById('bannerVisible').checked - }; - - // Convert style object to JSON string and append to form data - formData.append('style', JSON.stringify(style)); + // Add style properties to form data with the correct format + formData.append('style.backgroundColor', document.getElementById('bannerBgColor').value); + formData.append('style.textColor', document.getElementById('bannerTextColor').value); + formData.append('style.textAlign', document.getElementById('bannerTextAlign').value); + formData.append('style.fontSize', document.getElementById('bannerFontSize').value); + formData.append('style.padding', document.getElementById('bannerPadding').value); + formData.append('style.margin', document.getElementById('bannerMargin').value); + formData.append('style.borderRadius', document.getElementById('bannerBorderRadius').value); + formData.append('style.isVisible', document.getElementById('bannerVisible').checked); // Log form data for debugging - console.log('Odesílám data:'); + console.log('Odesílám data:'); for (let [key, value] of formData.entries()) { console.log(key, value); } - // Create headers object - const headers = {}; + // Create a new headers object + const headers = new Headers(); + // Don't set Content-Type header manually when using FormData + // The browser will set it automatically with the correct boundary // Add Authorization header if token exists const token = localStorage.getItem('token'); if (token) { - headers['Authorization'] = `Bearer ${token}`; + headers.append('Authorization', `Bearer ${token}`); } - // Note: Don't set Content-Type header when using FormData with files - // The browser will set it automatically with the correct boundary - const response = await fetch('/api/banner/update', { method: 'POST', headers: headers, @@ -1009,4 +1004,4 @@ document.addEventListener('DOMContentLoaded', loadBanner); - + \ No newline at end of file diff --git a/admin.html b/admin.html index 542b5a1..eeaa5e9 100644 --- a/admin.html +++ b/admin.html @@ -128,4 +128,4 @@ }); - + \ No newline at end of file diff --git a/banner.go b/banner.go index 1adc538..ba0cff5 100644 --- a/banner.go +++ b/banner.go @@ -156,20 +156,20 @@ func UpdateBannerHandler(w http.ResponseWriter, r *http.Request) { // Log form values for debugging log.Printf("Form values: %+v", r.Form) - // Parse style as JSON string - styleJSON := r.FormValue("style") - var style BannerStyle - if err := json.Unmarshal([]byte(styleJSON), &style); err != nil { - log.Printf("Error parsing style JSON: %v", err) - http.Error(w, "Error parsing style data: "+err.Error(), http.StatusBadRequest) - return - } - - // Create a new banner with parsed style + // Create a new banner with default values newBanner := BannerContent{ - Text: r.FormValue("text"), - Link: r.FormValue("link"), - Style: style, + Text: r.FormValue("text"), + Link: r.FormValue("link"), + Style: BannerStyle{ + BackgroundColor: r.FormValue("style[backgroundColor]"), + TextColor: r.FormValue("style[textColor]"), + TextAlign: r.FormValue("style[textAlign]"), + FontSize: r.FormValue("style[fontSize]"), + Padding: r.FormValue("style[padding]"), + Margin: r.FormValue("style[margin]"), + BorderRadius: r.FormValue("style[borderRadius]"), + IsVisible: r.FormValue("style[isVisible]") == "true", + }, } // Log the banner data for debugging diff --git a/kontakt/Makefile b/kontakt/Makefile index e167e30..52c7a0a 100644 --- a/kontakt/Makefile +++ b/kontakt/Makefile @@ -1,22 +1,22 @@ -# Makefile for kontakt service -.PHONY: dev run build clean - -PORT ?= 8081 -BINARY_NAME := kontakt-service -BUILD_DIR := build - -##@ Development -dev: ## Run in development mode - @echo "Starting kontakt service..." - @go run contact-scrape.go - -run: build ## Run built binary - @$(BUILD_DIR)/$(BINARY_NAME) - -build: ## Build the application - @echo "Building kontakt service..." - @go build -o $(BUILD_DIR)/$(BINARY_NAME) contact-scrape.go - -clean: ## Clean build artifacts - @rm -rf $(BUILD_DIR) - @echo "Build artifacts removed" +# Makefile for kontakt service +.PHONY: dev run build clean + +PORT ?= 8081 +BINARY_NAME := kontakt-service +BUILD_DIR := build + +##@ Development +dev: ## Run in development mode + @echo "Starting kontakt service..." + @go run contact-scrape.go + +run: build ## Run built binary + @$(BUILD_DIR)/$(BINARY_NAME) + +build: ## Build the application + @echo "Building kontakt service..." + @go build -o $(BUILD_DIR)/$(BINARY_NAME) contact-scrape.go + +clean: ## Clean build artifacts + @rm -rf $(BUILD_DIR) + @echo "Build artifacts removed" \ No newline at end of file diff --git a/kontakt/contact-scrape.go b/kontakt/contact-scrape.go index 224c3b7..37430be 100644 --- a/kontakt/contact-scrape.go +++ b/kontakt/contact-scrape.go @@ -1,418 +1,418 @@ -package main - -import ( - "crypto/md5" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "regexp" - "strings" - "time" - - "github.com/xuri/excelize/v2" -) - -type Contact struct { - Name string `json:"name"` - Position string `json:"position"` - Phone string `json:"phone,omitempty"` - ServicePhone string `json:"service_phone,omitempty"` - Internal bool `json:"internal"` - PhoneFlap string `json:"phone_flap,omitempty"` -} - -type ContactData struct { - Contacts []Contact `json:"contacts"` - InternalContacts []Contact `json:"internal_contacts"` - LastUpdated time.Time `json:"last_updated"` - FileHash string `json:"file_hash"` -} - -var ( - currentData *ContactData - dataFile = "data/contacts.json" - xlsxFile = "/mnt/telefony/TelefonniSeznamWeb.xlsx" -) - -func startAutoReload() { - ticker := time.NewTicker(1 * time.Hour) - quit := make(chan struct{}) - go func() { - for { - select { - case <-ticker.C: - log.Println("Auto-reloading contact data...") - loadData() - case <-quit: - ticker.Stop() - return - } - } - }() -} - -func main() { - // Create data directory if it doesn't exist - if err := os.MkdirAll("data", 0755); err != nil { - log.Printf("Warning: Could not create data directory: %v", err) - } - - // Start auto-reload scheduler - startAutoReload() - - // Load existing data or parse from Excel - loadData() - - // Set up HTTP handlers - http.HandleFunc("/", serveIndex) - http.HandleFunc("/contacts", serveContacts) - http.HandleFunc("/reload", reloadData) - - // Start server - port := os.Getenv("PORT") - if port == "" { - port = "80" - } - - log.Printf("Server starting on port %s", port) - log.Printf("Access the application at: http://webportal:%s", port) - log.Fatal(http.ListenAndServe(":"+port, nil)) -} - -func loadData() { - // Check if Excel file exists - if _, err := os.Stat(xlsxFile); os.IsNotExist(err) { - log.Printf("Excel file %s not found, using empty data", xlsxFile) - currentData = &ContactData{ - Contacts: []Contact{}, - InternalContacts: []Contact{}, - LastUpdated: time.Now(), - FileHash: "", - } - return - } - - // Calculate current file hash - currentHash, err := calculateFileHash(xlsxFile) - if err != nil { - log.Printf("Error calculating file hash: %v", err) - return - } - - // Check if cached data exists and is up to date - if cachedData, err := loadCachedData(); err == nil { - if cachedData.FileHash == currentHash { - log.Println("Using cached data (file unchanged)") - currentData = cachedData - return - } - } - - // Parse Excel file - log.Println("Parsing Excel file...") - contacts, err := parseExcelFile(xlsxFile) - if err != nil { - log.Printf("Error parsing Excel file: %v", err) - // Use empty data if parsing fails - currentData = &ContactData{ - Contacts: []Contact{}, - InternalContacts: []Contact{}, - LastUpdated: time.Now(), - FileHash: currentHash, - } - return - } - - currentData = processContacts(contacts) - currentData.FileHash = currentHash - - // Save to cache - if err := saveCachedData(currentData); err != nil { - log.Printf("Warning: Could not save cached data: %v", err) - } - - log.Printf("Loaded %d contacts from Excel file", len(currentData.Contacts)+len(currentData.InternalContacts)) -} - -func calculateFileHash(filename string) (string, error) { - file, err := os.Open(filename) - if err != nil { - return "", err - } - defer file.Close() - - hash := md5.New() - if _, err := io.Copy(hash, file); err != nil { - return "", err - } - - return fmt.Sprintf("%x", hash.Sum(nil)), nil -} - -func loadCachedData() (*ContactData, error) { - file, err := os.Open(dataFile) - if err != nil { - return nil, err - } - defer file.Close() - - var data ContactData - decoder := json.NewDecoder(file) - if err := decoder.Decode(&data); err != nil { - return nil, err - } - - return &data, nil -} - -func saveCachedData(data *ContactData) error { - file, err := os.Create(dataFile) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - return encoder.Encode(data) -} - -func parseExcelFile(filename string) ([]Contact, error) { - f, err := excelize.OpenFile(filename) - if err != nil { - return nil, fmt.Errorf("failed to open Excel file: %v", err) - } - defer f.Close() - - // Get the first sheet name - sheets := f.GetSheetList() - if len(sheets) == 0 { - return nil, fmt.Errorf("no sheets found in Excel file") - } - - sheetName := sheets[0] - - // Parse the single table (columns A-E) - contacts := parseTable(f, sheetName, "A", "E") - return contacts, nil -} - -func parseTable(f *excelize.File, sheetName, startCol, endCol string) []Contact { - var contacts []Contact - - // Get all rows in the sheet - rows, err := f.GetRows(sheetName) - if err != nil { - log.Printf("Error getting rows: %v", err) - return contacts - } - - // Skip first 3 and last 3 lines - startRow := 3 - endRow := len(rows) - 3 - if endRow <= startRow { - return nil - } - - // Column indices - const ( - nameCol = 0 - positionCol = 1 - phoneCol = 2 - servicePhoneCol = 3 - flapCol = 4 - ) - - for i := startRow; i < endRow; i++ { - row := rows[i] - - // Skip if row is too short - if len(row) <= nameCol { - continue - } - - // Check for "Aktualizace" - end of data - if len(row) > nameCol && strings.Contains(strings.ToLower(row[nameCol]), "aktualizace") { - break - } - - contact := Contact{ - Name: strings.TrimSpace(row[nameCol]), - Position: safeGet(row, positionCol, ""), - Phone: formatPhoneNumber(safeGet(row, phoneCol, "")), - ServicePhone: formatPhoneNumber(safeGet(row, servicePhoneCol, "")), // Full mobile number - PhoneFlap: formatPhoneFlap(safeGet(row, flapCol, "")), // Internal extension with * - } - - contacts = append(contacts, contact) - } - - return contacts -} - -func safeGet(row []string, index int, defaultValue string) string { - if index < len(row) { - return row[index] - } - return defaultValue -} - -func formatPhoneNumber(phone string) string { - if phone == "" { - return "" - } - - // Remove extra whitespace - phone = strings.TrimSpace(phone) - - // Remove common formatting characters - re := regexp.MustCompile(`[^\d+\-\s()]`) - phone = re.ReplaceAllString(phone, "") - - // If it's just a short number (internal extension), keep as is - if len(phone) <= 3 { - return phone - } - - // If it looks like a Czech number without country code, add it - if regexp.MustCompile(`^[67]\d{8}$`).MatchString(strings.ReplaceAll(phone, " ", "")) { - return "+420 " + phone - } - - return phone -} - -func formatPhoneFlap(flap string) string { - flap = strings.TrimSpace(flap) - if flap == "" { - return "" - } - if !strings.HasPrefix(flap, "*") { - return "*" + flap - } - return flap -} - -func processContacts(contacts []Contact) *ContactData { - var data ContactData - data.Contacts = []Contact{} - data.InternalContacts = []Contact{} - - for _, contact := range contacts { - // Trim whitespace and check for "Interní" - if strings.TrimSpace(contact.Position) == "Interní" { - contact.Internal = true - data.InternalContacts = append(data.InternalContacts, contact) - } else { - contact.Internal = false - data.Contacts = append(data.Contacts, contact) - } - } - data.LastUpdated = time.Now() - return &data -} - -func serveIndex(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - - // Check if index.html exists - if _, err := os.Stat("index.html"); os.IsNotExist(err) { - // Serve embedded HTML if file doesn't exist - w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprint(w, getEmbeddedHTML()) - return - } - - http.ServeFile(w, r, "index.html") -} - -func serveContacts(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - - if currentData == nil { - http.Error(w, `{"error": "No data available"}`, http.StatusInternalServerError) - return - } - - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - encoder.Encode(currentData) -} - -func reloadData(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - log.Println("Manual reload requested") - loadData() - - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"status": "reloaded", "contacts_count": %d}`, len(currentData.Contacts)+len(currentData.InternalContacts)) -} - -func getEmbeddedHTML() string { - return ` - - - - - Kontakty - - - - -
-
-

📞 Firemní telefonní seznam

-

Poppe + Potthoff kontakty

-
-
- -
-
-
- -
- -
-
- - -
-
- -
-
-

Načítání kontaktů...

-
- - -
-
- - - -` -} +package main + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "regexp" + "strings" + "time" + + "github.com/xuri/excelize/v2" +) + +type Contact struct { + Name string `json:"name"` + Position string `json:"position"` + Phone string `json:"phone,omitempty"` + ServicePhone string `json:"service_phone,omitempty"` + Internal bool `json:"internal"` + PhoneFlap string `json:"phone_flap,omitempty"` +} + +type ContactData struct { + Contacts []Contact `json:"contacts"` + InternalContacts []Contact `json:"internal_contacts"` + LastUpdated time.Time `json:"last_updated"` + FileHash string `json:"file_hash"` +} + +var ( + currentData *ContactData + dataFile = "data/contacts.json" + xlsxFile = "/mnt/telefony/TelefonniSeznamWeb.xlsx" +) + +func startAutoReload() { + ticker := time.NewTicker(1 * time.Hour) + quit := make(chan struct{}) + go func() { + for { + select { + case <-ticker.C: + log.Println("Auto-reloading contact data...") + loadData() + case <-quit: + ticker.Stop() + return + } + } + }() +} + +func main() { + // Create data directory if it doesn't exist + if err := os.MkdirAll("data", 0755); err != nil { + log.Printf("Warning: Could not create data directory: %v", err) + } + + // Start auto-reload scheduler + startAutoReload() + + // Load existing data or parse from Excel + loadData() + + // Set up HTTP handlers + http.HandleFunc("/", serveIndex) + http.HandleFunc("/contacts", serveContacts) + http.HandleFunc("/reload", reloadData) + + // Start server + port := os.Getenv("PORT") + if port == "" { + port = "80" + } + + log.Printf("Server starting on port %s", port) + log.Printf("Access the application at: http://webportal:%s", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} + +func loadData() { + // Check if Excel file exists + if _, err := os.Stat(xlsxFile); os.IsNotExist(err) { + log.Printf("Excel file %s not found, using empty data", xlsxFile) + currentData = &ContactData{ + Contacts: []Contact{}, + InternalContacts: []Contact{}, + LastUpdated: time.Now(), + FileHash: "", + } + return + } + + // Calculate current file hash + currentHash, err := calculateFileHash(xlsxFile) + if err != nil { + log.Printf("Error calculating file hash: %v", err) + return + } + + // Check if cached data exists and is up to date + if cachedData, err := loadCachedData(); err == nil { + if cachedData.FileHash == currentHash { + log.Println("Using cached data (file unchanged)") + currentData = cachedData + return + } + } + + // Parse Excel file + log.Println("Parsing Excel file...") + contacts, err := parseExcelFile(xlsxFile) + if err != nil { + log.Printf("Error parsing Excel file: %v", err) + // Use empty data if parsing fails + currentData = &ContactData{ + Contacts: []Contact{}, + InternalContacts: []Contact{}, + LastUpdated: time.Now(), + FileHash: currentHash, + } + return + } + + currentData = processContacts(contacts) + currentData.FileHash = currentHash + + // Save to cache + if err := saveCachedData(currentData); err != nil { + log.Printf("Warning: Could not save cached data: %v", err) + } + + log.Printf("Loaded %d contacts from Excel file", len(currentData.Contacts)+len(currentData.InternalContacts)) +} + +func calculateFileHash(filename string) (string, error) { + file, err := os.Open(filename) + if err != nil { + return "", err + } + defer file.Close() + + hash := md5.New() + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} + +func loadCachedData() (*ContactData, error) { + file, err := os.Open(dataFile) + if err != nil { + return nil, err + } + defer file.Close() + + var data ContactData + decoder := json.NewDecoder(file) + if err := decoder.Decode(&data); err != nil { + return nil, err + } + + return &data, nil +} + +func saveCachedData(data *ContactData) error { + file, err := os.Create(dataFile) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(data) +} + +func parseExcelFile(filename string) ([]Contact, error) { + f, err := excelize.OpenFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to open Excel file: %v", err) + } + defer f.Close() + + // Get the first sheet name + sheets := f.GetSheetList() + if len(sheets) == 0 { + return nil, fmt.Errorf("no sheets found in Excel file") + } + + sheetName := sheets[0] + + // Parse the single table (columns A-E) + contacts := parseTable(f, sheetName, "A", "E") + return contacts, nil +} + +func parseTable(f *excelize.File, sheetName, startCol, endCol string) []Contact { + var contacts []Contact + + // Get all rows in the sheet + rows, err := f.GetRows(sheetName) + if err != nil { + log.Printf("Error getting rows: %v", err) + return contacts + } + + // Skip first 3 and last 3 lines + startRow := 3 + endRow := len(rows) - 3 + if endRow <= startRow { + return nil + } + + // Column indices + const ( + nameCol = 0 + positionCol = 1 + phoneCol = 2 + servicePhoneCol = 3 + flapCol = 4 + ) + + for i := startRow; i < endRow; i++ { + row := rows[i] + + // Skip if row is too short + if len(row) <= nameCol { + continue + } + + // Check for "Aktualizace" - end of data + if len(row) > nameCol && strings.Contains(strings.ToLower(row[nameCol]), "aktualizace") { + break + } + + contact := Contact{ + Name: strings.TrimSpace(row[nameCol]), + Position: safeGet(row, positionCol, ""), + Phone: formatPhoneNumber(safeGet(row, phoneCol, "")), + ServicePhone: formatPhoneNumber(safeGet(row, servicePhoneCol, "")), // Full mobile number + PhoneFlap: formatPhoneFlap(safeGet(row, flapCol, "")), // Internal extension with * + } + + contacts = append(contacts, contact) + } + + return contacts +} + +func safeGet(row []string, index int, defaultValue string) string { + if index < len(row) { + return row[index] + } + return defaultValue +} + +func formatPhoneNumber(phone string) string { + if phone == "" { + return "" + } + + // Remove extra whitespace + phone = strings.TrimSpace(phone) + + // Remove common formatting characters + re := regexp.MustCompile(`[^\d+\-\s()]`) + phone = re.ReplaceAllString(phone, "") + + // If it's just a short number (internal extension), keep as is + if len(phone) <= 3 { + return phone + } + + // If it looks like a Czech number without country code, add it + if regexp.MustCompile(`^[67]\d{8}$`).MatchString(strings.ReplaceAll(phone, " ", "")) { + return "+420 " + phone + } + + return phone +} + +func formatPhoneFlap(flap string) string { + flap = strings.TrimSpace(flap) + if flap == "" { + return "" + } + if !strings.HasPrefix(flap, "*") { + return "*" + flap + } + return flap +} + +func processContacts(contacts []Contact) *ContactData { + var data ContactData + data.Contacts = []Contact{} + data.InternalContacts = []Contact{} + + for _, contact := range contacts { + // Trim whitespace and check for "Interní" + if strings.TrimSpace(contact.Position) == "Interní" { + contact.Internal = true + data.InternalContacts = append(data.InternalContacts, contact) + } else { + contact.Internal = false + data.Contacts = append(data.Contacts, contact) + } + } + data.LastUpdated = time.Now() + return &data +} + +func serveIndex(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + // Check if index.html exists + if _, err := os.Stat("index.html"); os.IsNotExist(err) { + // Serve embedded HTML if file doesn't exist + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, getEmbeddedHTML()) + return + } + + http.ServeFile(w, r, "index.html") +} + +func serveContacts(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if currentData == nil { + http.Error(w, `{"error": "No data available"}`, http.StatusInternalServerError) + return + } + + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + encoder.Encode(currentData) +} + +func reloadData(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + log.Println("Manual reload requested") + loadData() + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"status": "reloaded", "contacts_count": %d}`, len(currentData.Contacts)+len(currentData.InternalContacts)) +} + +func getEmbeddedHTML() string { + return ` + + + + + Kontakty + + + + +
+
+

📞 Firemní telefonní seznam

+

Poppe + Potthoff kontakty

+
+
+ +
+
+
+ +
+ +
+
+ + +
+
+ +
+
+

Načítání kontaktů...

+
+ + +
+
+ + + +` +}