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"` } 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 = "TelefonniSeznamWeb.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] // Parse single table (A-E columns) contacts := parseTable(f, sheetName, "A", "E", 1) return contacts, nil } func parseTable(f *excelize.File, sheetName, startCol, endCol string, tableNum int) []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 header rows (first row is header) startRow := 1 if len(rows) <= startRow { return contacts } // Column indices (A-E: name, position, phone, service_phone, extension) nameCol, positionCol, phoneCol, servicePhoneCol, extensionCol := 0, 1, 2, 3, 4 for i := startRow; i < len(rows); i++ { row := rows[i] // Skip if row is too short if len(row) <= nameCol { continue } name := strings.TrimSpace(row[nameCol]) position := "" phone := "" servicePhone := "" extension := "" if len(row) > positionCol { position = strings.TrimSpace(row[positionCol]) } if len(row) > phoneCol { phone = strings.TrimSpace(row[phoneCol]) } if len(row) > servicePhoneCol { servicePhone = strings.TrimSpace(row[servicePhoneCol]) } if len(row) > extensionCol { extension = strings.TrimSpace(row[extensionCol]) } // Clean phone numbers phone = cleanPhoneNumber(phone) servicePhone = cleanPhoneNumber(servicePhone) // Combine extension with service phone if both exist if servicePhone != "" && extension != "" { servicePhone = fmt.Sprintf("%s (%s)", servicePhone, extension) } else if extension != "" { servicePhone = extension } // If we have a name, create new contact if name != "" { contacts = append(contacts, Contact{ Name: name, Position: position, Phone: phone, ServicePhone: servicePhone, Table: tableNum, }) } } 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 ` Kontakty

📞 Firemní telefonní seznam

Poppe + Potthoff kontakty

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

` }