mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-03 20:12:59 +00:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
# Install git and ca-certificates (needed for fetching dependencies)
|
||||||
|
RUN apk add --no-cache git ca-certificates
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY contact-scrape.go ./
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o contact-scrape .
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install ca-certificates for HTTPS requests
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S appgroup && \
|
||||||
|
adduser -u 1001 -S appuser -G appgroup
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binary from builder stage
|
||||||
|
COPY --from=builder /build/contact-scrape .
|
||||||
|
|
||||||
|
# Copy HTML file
|
||||||
|
COPY index.html .
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p data && chown -R appuser:appgroup /app
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./contact-scrape"]
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Contact Scrape Makefile
|
||||||
|
|
||||||
|
.PHONY: build run clean install dev test help
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
BINARY_NAME=contact-scrape
|
||||||
|
BUILD_DIR=build
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
help: ## Show this help message
|
||||||
|
@echo "Available commands:"
|
||||||
|
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
##@ Development
|
||||||
|
dev: ## Run the application in development mode
|
||||||
|
@echo "Starting development server..."
|
||||||
|
@go run .
|
||||||
|
|
||||||
|
run: build ## Build and run the application
|
||||||
|
@echo "Starting $(BINARY_NAME)..."
|
||||||
|
@./$(BUILD_DIR)/$(BINARY_NAME)
|
||||||
|
|
||||||
|
test: ## Run tests
|
||||||
|
@echo "Running tests..."
|
||||||
|
@go test -v ./...
|
||||||
|
|
||||||
|
##@ Build
|
||||||
|
build: ## Build the application
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@go build -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||||
|
@echo "Binary built: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
|
build-linux: ## Build for Linux (useful for deployment)
|
||||||
|
@echo "Building $(BINARY_NAME) for Linux..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@GOOS=linux GOARCH=amd64 go build -o $(BUILD_DIR)/$(BINARY_NAME)-linux .
|
||||||
|
@echo "Linux binary built: $(BUILD_DIR)/$(BINARY_NAME)-linux"
|
||||||
|
|
||||||
|
##@ Dependencies
|
||||||
|
deps: ## Download dependencies
|
||||||
|
@echo "Downloading dependencies..."
|
||||||
|
@go mod download
|
||||||
|
@go mod tidy
|
||||||
|
|
||||||
|
##@ Deployment
|
||||||
|
install: build ## Install the service (requires sudo)
|
||||||
|
@echo "Installing contact-scrape service..."
|
||||||
|
@sudo mkdir -p /opt/contact-scrape
|
||||||
|
@sudo cp $(BUILD_DIR)/$(BINARY_NAME) /opt/contact-scrape/
|
||||||
|
@sudo cp index.html /opt/contact-scrape/
|
||||||
|
@sudo mkdir -p /opt/contact-scrape/data
|
||||||
|
@sudo chown -R www-data:www-data /opt/contact-scrape
|
||||||
|
@sudo cp contact-scrape.service /etc/systemd/system/
|
||||||
|
@sudo systemctl daemon-reload
|
||||||
|
@sudo systemctl enable contact-scrape
|
||||||
|
@echo "Service installed. Start with: sudo systemctl start contact-scrape"
|
||||||
|
|
||||||
|
uninstall: ## Uninstall the service (requires sudo)
|
||||||
|
@echo "Uninstalling contact-scrape service..."
|
||||||
|
@sudo systemctl stop contact-scrape 2>/dev/null || true
|
||||||
|
@sudo systemctl disable contact-scrape 2>/dev/null || true
|
||||||
|
@sudo rm -f /etc/systemd/system/contact-scrape.service
|
||||||
|
@sudo systemctl daemon-reload
|
||||||
|
@sudo rm -rf /opt/contact-scrape
|
||||||
|
@echo "Service uninstalled."
|
||||||
|
|
||||||
|
status: ## Check service status
|
||||||
|
@sudo systemctl status contact-scrape
|
||||||
|
|
||||||
|
start: ## Start the service
|
||||||
|
@sudo systemctl start contact-scrape
|
||||||
|
|
||||||
|
stop: ## Stop the service
|
||||||
|
@sudo systemctl stop contact-scrape
|
||||||
|
|
||||||
|
restart: ## Restart the service
|
||||||
|
@sudo systemctl restart contact-scrape
|
||||||
|
|
||||||
|
logs: ## View service logs
|
||||||
|
@sudo journalctl -u contact-scrape -f
|
||||||
|
|
||||||
|
##@ File Management
|
||||||
|
upload-xlsx: ## Upload contacts.xlsx to server (set SERVER variable)
|
||||||
|
@if [ -z "$(SERVER)" ]; then echo "Usage: make upload-xlsx SERVER=user@hostname"; exit 1; fi
|
||||||
|
@echo "Uploading contacts.xlsx to $(SERVER)..."
|
||||||
|
@scp contacts.xlsx $(SERVER):/opt/contact-scrape/
|
||||||
|
@ssh $(SERVER) "sudo chown www-data:www-data /opt/contact-scrape/contacts.xlsx"
|
||||||
|
@ssh $(SERVER) "sudo systemctl restart contact-scrape"
|
||||||
|
@echo "File uploaded and service restarted."
|
||||||
|
|
||||||
|
##@ Monitoring
|
||||||
|
monitor: ## Monitor the service with file watching
|
||||||
|
@echo "Monitoring contacts.xlsx for changes..."
|
||||||
|
@while true; do \
|
||||||
|
inotifywait -e modify contacts.xlsx 2>/dev/null && \
|
||||||
|
echo "File changed, reloading..." && \
|
||||||
|
curl -X POST http://localhost:$(PORT)/reload; \
|
||||||
|
sleep 1; \
|
||||||
|
done
|
||||||
|
|
||||||
|
##@ Utilities
|
||||||
|
clean: ## Clean build artifacts
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@rm -rf $(BUILD_DIR)
|
||||||
|
@rm -f data/contacts.json
|
||||||
|
|
||||||
|
setup-dirs: ## Create necessary directories
|
||||||
|
@mkdir -p data
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
|
||||||
|
check-file: ## Check if contacts.xlsx exists and show info
|
||||||
|
@if [ -f "contacts.xlsx" ]; then \
|
||||||
|
echo "✓ contacts.xlsx found"; \
|
||||||
|
ls -lh contacts.xlsx; \
|
||||||
|
else \
|
||||||
|
echo "✗ contacts.xlsx not found"; \
|
||||||
|
echo "Please place your Excel file in the current directory"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
##@ Docker (Optional)
|
||||||
|
docker-build: ## Build Docker image
|
||||||
|
@echo "Building Docker image..."
|
||||||
|
@docker build -t contact-scrape .
|
||||||
|
|
||||||
|
docker-run: ## Run in Docker container
|
||||||
|
@echo "Running Docker container..."
|
||||||
|
@docker run -p $(PORT):$(PORT) -v $(PWD)/contacts.xlsx:/app/contacts.xlsx -v $(PWD)/data:/app/data contact-scrape
|
||||||
|
|
||||||
|
##@ Information
|
||||||
|
info: ## Show application information
|
||||||
|
@echo "Contact Scrape Application"
|
||||||
|
@echo "========================="
|
||||||
|
@echo "Port: $(PORT)"
|
||||||
|
@echo "Binary: $(BINARY_NAME)"
|
||||||
|
@echo "Build dir: $(BUILD_DIR)"
|
||||||
|
@echo ""
|
||||||
|
@echo "Endpoints:"
|
||||||
|
@echo " http://localhost:$(PORT)/ - Web interface"
|
||||||
|
@echo " http://localhost:$(PORT)/contacts - JSON API"
|
||||||
|
@echo " http://localhost:$(PORT)/reload - Reload data (POST)"
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 `<!DOCTYPE html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kontakty</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50 min-h-screen">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-800">Kontakty</h1>
|
||||||
|
<button onclick="reloadContacts()" id="reloadBtn"
|
||||||
|
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors">
|
||||||
|
Obnovit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<input type="text" id="searchInput" placeholder="Hledat kontakt..."
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
onkeyup="filterContacts()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="text-center py-8">
|
||||||
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||||
|
<p class="mt-2 text-gray-600">Načítání kontaktů...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="contactsList" class="hidden">
|
||||||
|
<div id="stats" class="mb-4 text-sm text-gray-600"></div>
|
||||||
|
<div id="contacts" class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error" class="hidden text-center py-8 text-red-600"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let allContacts = [];
|
||||||
|
let filteredContacts = [];
|
||||||
|
|
||||||
|
async function loadContacts() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/contacts');
|
||||||
|
const data = await response.json();
|
||||||
|
allContacts = data.contacts || [];
|
||||||
|
filteredContacts = [...allContacts];
|
||||||
|
|
||||||
|
document.getElementById('loading').classList.add('hidden');
|
||||||
|
document.getElementById('contactsList').classList.remove('hidden');
|
||||||
|
|
||||||
|
updateStats(data);
|
||||||
|
displayContacts(filteredContacts);
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('loading').classList.add('hidden');
|
||||||
|
document.getElementById('error').classList.remove('hidden');
|
||||||
|
document.getElementById('error').innerHTML = '<p>Chyba při načítání kontaktů: ' + error.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats(data) {
|
||||||
|
const lastUpdated = new Date(data.last_updated).toLocaleString('cs-CZ');
|
||||||
|
const table1Count = allContacts.filter(c => c.table === 1).length;
|
||||||
|
const table2Count = allContacts.filter(c => c.table === 2).length;
|
||||||
|
|
||||||
|
document.getElementById('stats').innerHTML =
|
||||||
|
'Celkem: ' + allContacts.length + ' kontaktů ' +
|
||||||
|
'(Tabulka 1: ' + table1Count + ', Tabulka 2: ' + table2Count + ') | ' +
|
||||||
|
'Aktualizováno: ' + lastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayContacts(contacts) {
|
||||||
|
const container = document.getElementById('contacts');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (contacts.length === 0) {
|
||||||
|
container.innerHTML = '<div class="col-span-full text-center py-8 text-gray-500">Žádné kontakty nenalezeny</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts.forEach(contact => {
|
||||||
|
const contactCard = document.createElement('div');
|
||||||
|
contactCard.className = 'bg-gray-50 p-4 rounded-lg border border-gray-200 hover:shadow-md transition-shadow';
|
||||||
|
|
||||||
|
contactCard.innerHTML =
|
||||||
|
'<div class="flex items-start justify-between mb-2">' +
|
||||||
|
'<h3 class="font-semibold text-gray-800 text-lg">' + (contact.name || 'Bez jména') + '</h3>' +
|
||||||
|
'<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">T' + contact.table + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
(contact.position ? '<p class="text-gray-600 mb-3">' + contact.position + '</p>' : '') +
|
||||||
|
'<div class="space-y-1">' +
|
||||||
|
(contact.phone ? '<div class="flex items-center text-sm"><span class="font-medium text-gray-700 w-16">Tel:</span><a href="tel:' + contact.phone + '" class="text-blue-600 hover:underline">' + contact.phone + '</a></div>' : '') +
|
||||||
|
(contact.service_phone ? '<div class="flex items-center text-sm"><span class="font-medium text-gray-700 w-16">Služ:</span><a href="tel:' + contact.service_phone + '" class="text-blue-600 hover:underline">' + contact.service_phone + '</a></div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
container.appendChild(contactCard);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterContacts() {
|
||||||
|
const query = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
filteredContacts = allContacts.filter(contact =>
|
||||||
|
(contact.name && contact.name.toLowerCase().includes(query)) ||
|
||||||
|
(contact.position && contact.position.toLowerCase().includes(query)) ||
|
||||||
|
(contact.phone && contact.phone.includes(query)) ||
|
||||||
|
(contact.service_phone && contact.service_phone.includes(query))
|
||||||
|
);
|
||||||
|
displayContacts(filteredContacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadContacts() {
|
||||||
|
const btn = document.getElementById('reloadBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Načítání...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/reload', { method: 'POST' });
|
||||||
|
await loadContacts();
|
||||||
|
} catch (error) {
|
||||||
|
alert('Chyba při obnovování: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Obnovit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load contacts on page load
|
||||||
|
loadContacts();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Contact Scrape Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
WorkingDirectory=/opt/contact-scrape
|
||||||
|
ExecStart=/opt/contact-scrape/contact-scrape
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PORT=8080
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/opt/contact-scrape/data
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=contact-scrape
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Binary file not shown.
@@ -3,36 +3,18 @@ module ppve
|
|||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/systray v1.11.0 // indirect
|
github.com/xuri/excelize/v2 v2.9.1
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
)
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
require (
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
github.com/gin-contrib/cors v1.7.5 // indirect
|
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.10.0 // indirect
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/xuri/nfp v0.0.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
golang.org/x/net v0.40.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
|
||||||
golang.org/x/net v0.38.0 // indirect
|
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
|
||||||
golang.org/x/text v0.23.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,120 +1,33 @@
|
|||||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
|
||||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
|
||||||
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
|
|
||||||
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
|
||||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
|
||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
|
|||||||
+122
-227
@@ -1,228 +1,123 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Aplikační Rozcestník</title>
|
<title>Aplikační Rozcestník</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 min-h-screen">
|
<body class="bg-gray-100 min-h-screen">
|
||||||
<header class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white shadow-lg" style="margin-bottom: 20px;">
|
<header class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white shadow-lg" style="margin-bottom: 20px;">
|
||||||
<div class="container mx-auto px-4 py-6 flex items-center">
|
<div class="container mx-auto px-4 py-6 flex items-center">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold">Poppe + Potthoff - Firemní Aplikace</h1>
|
<h1 class="text-3xl font-bold">Poppe + Potthoff - Firemní Aplikace</h1>
|
||||||
<p class="mt-2 text-blue-100">Rychlý přístup ke všem důležitým systémům</p>
|
<p class="mt-2 text-blue-100">Rychlý přístup ke všem důležitým systémům</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 py-8">
|
<main class="container mx-auto px-4 py-8">
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="mb-8 max-w-xl mx-auto">
|
<div class="mb-8 max-w-xl mx-auto">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="text" id="search" placeholder="Hledat aplikaci..." style="margin-bottom: 20px;"
|
<input type="text" id="search" placeholder="Hledat aplikaci..." style="margin-bottom: 20px;"
|
||||||
class="w-full px-4 py-3 rounded-lg shadow-sm border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
class="w-full px-4 py-3 rounded-lg shadow-sm border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
||||||
<div class="absolute right-3 top-3 text-gray-400">
|
<div class="absolute right-3 top-3 text-gray-400">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Apps Grid -->
|
<!-- Apps Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<!-- 1. Car trips app -->
|
<!-- 1. Car trips app -->
|
||||||
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-blue-600" data-name="zápis cest aut project">
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-blue-600" data-name="zápis cest aut project">
|
||||||
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-blue-100 text-blue-600 mb-4">
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-blue-100 text-blue-600 mb-4">
|
||||||
<i class="fas fa-car-side text-2xl"></i>
|
<i class="fas fa-car-side text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 mb-2">Záznam jízdy služebního vozu</h2>
|
<h2 class="text-xl font-bold text-gray-800 mb-2">Záznam jízdy služebního vozu</h2>
|
||||||
<p class="text-gray-600 mb-4">Systém pro evidenci služebních jízd.</p>
|
<p class="text-gray-600 mb-4">Systém pro evidenci služebních jízd.</p>
|
||||||
<a href="/evidence-aut" class="block text-center bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
<a href="/evidence-aut" class="block text-center bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
Otevřít aplikaci
|
Otevřít aplikaci
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. Lunches -->
|
<!-- 2. Lunches -->
|
||||||
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-green-600" data-name="obědy obedy jídlo lunch">
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-green-600" data-name="obědy obedy jídlo lunch">
|
||||||
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-green-100 text-green-600 mb-4">
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-green-100 text-green-600 mb-4">
|
||||||
<i class="fas fa-utensils text-2xl"></i>
|
<i class="fas fa-utensils text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 mb-2">Objednávka obědů</h2>
|
<h2 class="text-xl font-bold text-gray-800 mb-2">Objednávka obědů</h2>
|
||||||
<p class="text-gray-600 mb-4">Portál pro objednávku a přehled firemních obědů</p>
|
<p class="text-gray-600 mb-4">Portál pro objednávku a přehled firemních obědů</p>
|
||||||
<a href="http://ppc-app/pwkweb2" class="block text-center bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
<a href="http://ppc-app/pwkweb2" class="block text-center bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
Otevřít aplikaci
|
Otevřít aplikaci
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. OSTicket -->
|
<!-- 3. OSTicket -->
|
||||||
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-orange-600" data-name="osticket pomoc podpora support ticket">
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-orange-600" data-name="osticket pomoc podpora support ticket">
|
||||||
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-orange-100 text-orange-600 mb-4">
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-orange-100 text-orange-600 mb-4">
|
||||||
<i class="fas fa-headset text-2xl"></i>
|
<i class="fas fa-headset text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 mb-2">OSTicket</h2>
|
<h2 class="text-xl font-bold text-gray-800 mb-2">OSTicket</h2>
|
||||||
<p class="text-gray-600 mb-4">Systém technické podpory a hlášení problémů</p>
|
<p class="text-gray-600 mb-4">Systém technické podpory a hlášení problémů</p>
|
||||||
<a href="http://osticket/" class="block text-center bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
<a href="http://osticket/" class="block text-center bg-orange-600 hover:bg-orange-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
Otevřít aplikaci
|
Otevřít aplikaci
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 4. Canboard tasks -->
|
<!-- 4. Canboard tasks -->
|
||||||
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-purple-600" data-name="canboard úkoly úkolníček tasks">
|
<div class="card bg-white rounded-xl shadow p-6 border-t-4 border-purple-600" data-name="canboard úkoly úkolníček tasks">
|
||||||
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-purple-100 text-purple-600 mb-4">
|
<div class="rounded-full w-14 h-14 flex items-center justify-center bg-purple-100 text-purple-600 mb-4">
|
||||||
<i class="fas fa-tasks text-2xl"></i>
|
<i class="fas fa-tasks text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 mb-2">Kanboard úkolníček</h2>
|
<h2 class="text-xl font-bold text-gray-800 mb-2">Kanboard</h2>
|
||||||
<p class="text-gray-600 mb-4">Správa úkolů a projektů v přehledném kanban stylu</p>
|
<p class="text-gray-600 mb-4">Správa úkolů a projektů v přehledném kanban stylu</p>
|
||||||
<a href="http://kanboard/" class="block text-center bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
<a href="http://kanboard/" class="block text-center bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
|
||||||
Otevřít aplikaci
|
Otevřít aplikaci
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<!-- Separator with heading -->
|
<footer class="bg-gray-800 text-gray-400 py-6 mt-12">
|
||||||
<div class="my-10 text-center">
|
<div class="container mx-auto px-4 text-center">
|
||||||
<div class="relative">
|
<p>© 2025 Poppe + Potthoff</p>
|
||||||
<div class="absolute inset-0 flex items-center">
|
<p class="mt-2 text-sm">Created by <a href="https://tdvorak.dev" class="text-blue-400 hover:text-blue-300">TDvorak</a></p>
|
||||||
<div class="w-full border-t border-gray-300"></div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
<div class="relative flex justify-center">
|
|
||||||
<span class="bg-gray-100 px-4 text-sm text-gray-500">WINDOWS SDÍLENÉ SLOŽKY</span>
|
<script>
|
||||||
</div>
|
// Search functionality
|
||||||
</div>
|
const searchInput = document.getElementById('search');
|
||||||
</div>
|
const appCards = document.querySelectorAll('.card');
|
||||||
|
|
||||||
<!-- Windows Folders Grid - smaller and more subtle cards -->
|
searchInput.addEventListener('input', function() {
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
const searchTerm = this.value.toLowerCase();
|
||||||
<!-- 1. Telefonní seznam -->
|
|
||||||
<div class="card bg-white rounded-lg shadow-sm p-4 border border-gray-200" data-name="telefonní seznam telefony kontakty">
|
appCards.forEach(card => {
|
||||||
<div class="flex items-center">
|
const cardName = card.getAttribute('data-name').toLowerCase();
|
||||||
<div class="rounded-full w-10 h-10 flex items-center justify-center bg-gray-100 text-gray-600 mr-3">
|
const cardTitle = card.querySelector('h2').textContent.toLowerCase();
|
||||||
<i class="fas fa-phone-alt"></i>
|
const cardDesc = card.querySelector('p').textContent.toLowerCase();
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
if (cardName.includes(searchTerm) || cardTitle.includes(searchTerm) || cardDesc.includes(searchTerm)) {
|
||||||
<h3 class="text-md font-medium text-gray-800">Telefonní seznam</h3>
|
card.style.display = 'block';
|
||||||
<p class="text-xs text-gray-500 mb-2">Aktuální telefonní seznam společnosti</p>
|
} else {
|
||||||
</div>
|
card.style.display = 'none';
|
||||||
</div>
|
}
|
||||||
<button onclick="openWindowsFolder('M:\\X. IT\\0. PUBLIC\\Aktuální telefonní seznam')" class="block w-full text-center bg-gray-200 hover:bg-gray-300 text-gray-700 text-sm py-1 px-3 rounded transition-colors mt-2">
|
});
|
||||||
<i class="fas fa-folder-open mr-1"></i> Otevřít složku
|
});
|
||||||
</button>
|
</script>
|
||||||
</div>
|
</body>
|
||||||
|
|
||||||
<!-- 2. Řízená dokumentace -->
|
|
||||||
<div class="card bg-white rounded-lg shadow-sm p-4 border border-gray-200" data-name="řízená dokumentace dokumenty">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="rounded-full w-10 h-10 flex items-center justify-center bg-gray-100 text-gray-600 mr-3">
|
|
||||||
<i class="fas fa-file-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-md font-medium text-gray-800">Řízená dokumentace</h3>
|
|
||||||
<p class="text-xs text-gray-500 mb-2">Přístup k firemní řízené dokumentaci</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onclick="openWindowsFolder('M:\\06. ŘÍZENÁ DOKUMENTACE')" class="block w-full text-center bg-gray-200 hover:bg-gray-300 text-gray-700 text-sm py-1 px-3 rounded transition-colors mt-2">
|
|
||||||
<i class="fas fa-folder-open mr-1"></i> Otevřít složku
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. Management úkolů -->
|
|
||||||
<div class="card bg-white rounded-lg shadow-sm p-4 border border-gray-200" data-name="management úkolů úkoly projekty">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="rounded-full w-10 h-10 flex items-center justify-center bg-gray-100 text-gray-600 mr-3">
|
|
||||||
<i class="fas fa-clipboard-list"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-md font-medium text-gray-800">Management úkolů</h3>
|
|
||||||
<p class="text-xs text-gray-500 mb-2">Systém pro správu a sledování firemních úkolů</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onclick="openWindowsFolder('C:\\Users\\conta\\Downloads\\PROG+HTML')" class="block w-full text-center bg-gray-200 hover:bg-gray-300 text-gray-700 text-sm py-1 px-3 rounded transition-colors mt-2">
|
|
||||||
<i class="fas fa-folder-open mr-1"></i> Otevřít složku
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="bg-gray-800 text-gray-400 py-6 mt-12">
|
|
||||||
<div class="container mx-auto px-4 text-center">
|
|
||||||
<p>© 2025 Poppe + Potthoff</p>
|
|
||||||
<p class="mt-2 text-sm">Created by <a href="https://tdvorak.dev" class="text-blue-400 hover:text-blue-300">TDvorak</a></p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Search functionality
|
|
||||||
const searchInput = document.getElementById('search');
|
|
||||||
const appCards = document.querySelectorAll('.card');
|
|
||||||
|
|
||||||
searchInput.addEventListener('input', function() {
|
|
||||||
const searchTerm = this.value.toLowerCase();
|
|
||||||
|
|
||||||
appCards.forEach(card => {
|
|
||||||
const cardName = card.getAttribute('data-name').toLowerCase();
|
|
||||||
const cardTitle = card.querySelector('h2') || card.querySelector('h3');
|
|
||||||
const cardDesc = card.querySelector('p');
|
|
||||||
const titleText = cardTitle ? cardTitle.textContent.toLowerCase() : '';
|
|
||||||
const descText = cardDesc ? cardDesc.textContent.toLowerCase() : '';
|
|
||||||
|
|
||||||
if (cardName.includes(searchTerm) || titleText.includes(searchTerm) || descText.includes(searchTerm)) {
|
|
||||||
card.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
card.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to open Windows folders via Go server
|
|
||||||
function openWindowsFolder(path) {
|
|
||||||
// Show loading indicator or disable button
|
|
||||||
const allButtons = document.querySelectorAll('button');
|
|
||||||
allButtons.forEach(button => {
|
|
||||||
if (button.innerHTML.includes('Otevřít složku')) {
|
|
||||||
button.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make API call to our Go endpoint
|
|
||||||
fetch('/open-folder', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ path: path })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
console.error('Chyba při otevírání složky:', data.error);
|
|
||||||
alert('Chyba při otevírání složky: ' + data.error);
|
|
||||||
} else {
|
|
||||||
console.log('Složka úspěšně otevřena');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Chyba při komunikaci se serverem:', error);
|
|
||||||
alert('Chyba při komunikaci se serverem. Zkuste to prosím později.');
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// Re-enable buttons
|
|
||||||
allButtons.forEach(button => {
|
|
||||||
button.disabled = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -51,11 +50,9 @@ func main() {
|
|||||||
http.ServeFile(w, r, "evidence-aut.html")
|
http.ServeFile(w, r, "evidence-aut.html")
|
||||||
}))
|
}))
|
||||||
|
|
||||||
http.HandleFunc("/open-folder", enableCORS(handleOpenFolder))
|
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "8080"
|
port = "80"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Server běží na portu %s", port)
|
log.Printf("Server běží na portu %s", port)
|
||||||
@@ -159,66 +156,6 @@ func handleSubmit(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(`{"message":"Záznam byl úspěšně uložen a email odeslán"}`))
|
w.Write([]byte(`{"message":"Záznam byl úspěšně uložen a email odeslán"}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleOpenFolder opens a Windows Explorer window to the specified folder path
|
|
||||||
func handleOpenFolder(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// Only allow POST requests
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
if r.Method == http.MethodOptions {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "Only POST method is allowed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the request body
|
|
||||||
type FolderRequest struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var req FolderRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Chyba při čtení těla požadavku: %v", err)
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to parse request body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the folder path
|
|
||||||
if req.Path == "" {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "Path is required"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize the path (basic security - you might want to add more validation)
|
|
||||||
// Ensure it's a valid Windows network path
|
|
||||||
if !strings.HasPrefix(req.Path, "C:\\") {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "Only network paths on M: drive are allowed"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the command to open Windows Explorer
|
|
||||||
cmd := exec.Command("explorer.exe", req.Path)
|
|
||||||
|
|
||||||
// Run the command
|
|
||||||
err = cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Chyba při spouštění explorer.exe: %v", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Failed to open folder: %v", err)})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a success response
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "Folder opened successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendEmail(entry TripEntry, parsedDateStart, parsedDateEnd time.Time, czechMonths []string) error {
|
func sendEmail(entry TripEntry, parsedDateStart, parsedDateEnd time.Time, czechMonths []string) error {
|
||||||
smtpHost := "mail.pp-kunovice.cz"
|
smtpHost := "mail.pp-kunovice.cz"
|
||||||
smtpPort := 465
|
smtpPort := 465
|
||||||
|
|||||||
Reference in New Issue
Block a user