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"` Table int `json:"table"` // 1 for first table, 2 for second table } type ContactData struct { Contacts []Contact `json:"contacts"` LastUpdated time.Time `json:"last_updated"` FileHash string `json:"file_hash"` } var ( currentData *ContactData dataFile = "data/contacts.json" xlsxFile = "contacts.xlsx" ) func startAutoReload() { ticker := time.NewTicker(3 * 24 * 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://localhost:%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{}, 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{}, LastUpdated: time.Now(), FileHash: currentHash, } return } currentData = &ContactData{ Contacts: contacts, LastUpdated: time.Now(), 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(contacts)) } 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] var contacts []Contact // Parse first table (A-D columns) contacts = append(contacts, parseTable(f, sheetName, "A", "D", 1)...) // Parse second table (F-H columns) contacts = append(contacts, parseTable(f, sheetName, "F", "H", 2)...) return contacts, nil } func parseTable(f *excelize.File, sheetName, startCol, endCol string, tableNum int) []Contact { var contacts []Contact var currentContact *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 header rows (first 3 rows based on your description) startRow := 3 if len(rows) <= startRow { return contacts } // Column indices var nameCol, positionCol, phoneCol, servicePhoneCol int if tableNum == 1 { nameCol, positionCol, phoneCol, servicePhoneCol = 0, 1, 2, 3 // A, B, C, D } else { nameCol, positionCol, phoneCol = 5, 6, 7 // F, G, H } for i := startRow; i < len(rows); 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 } // Check for special formatting rows (like "*02(xx)") if len(row) > positionCol && strings.Contains(row[positionCol], "*") { continue } name := strings.TrimSpace(row[nameCol]) position := "" phone := "" servicePhone := "" if len(row) > positionCol { position = strings.TrimSpace(row[positionCol]) } if len(row) > phoneCol { phone = strings.TrimSpace(row[phoneCol]) } if tableNum == 1 && len(row) > servicePhoneCol { servicePhone = strings.TrimSpace(row[servicePhoneCol]) } // Clean phone numbers phone = cleanPhoneNumber(phone) servicePhone = cleanPhoneNumber(servicePhone) // If we have a name, start a new contact if name != "" && !strings.Contains(name, "(") { currentContact = &Contact{ Name: name, Position: position, Phone: phone, ServicePhone: servicePhone, Table: tableNum, } contacts = append(contacts, *currentContact) } else if currentContact != nil { // This is additional data for the current contact newContact := *currentContact if position != "" { newContact.Position = position } if phone != "" { newContact.Phone = phone } if servicePhone != "" { newContact.ServicePhone = servicePhone } contacts = append(contacts, newContact) } } return contacts } func cleanPhoneNumber(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 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)) } func getEmbeddedHTML() string { return `
Poppe + Potthoff kontakty
Načítání kontaktů...