Add demo system and various analysis scripts
- Created demo_system.sh for interactive demo scenarios including TAG and B7 analysis, monthly economics, and complete system demo. - Implemented legacy_tag_opgaver.sh to fetch and analyze real API data for TAG tasks, generating detailed CSV reports. - Developed oekonomi_analyser.sh for economic analysis with options to fetch new data and specify the year for analysis. - Introduced tag_analyser.sh for analyzing TAG tasks with options for fetching data and specifying task types.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# Demo System - Tilbudsgivern Server
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
# Farver til demo
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_banner() {
|
||||
echo -e "${BLUE}============================================${NC}"
|
||||
echo -e "${BLUE} TILBUDSGIVERN ANALYSE SYSTEM DEMO${NC}"
|
||||
echo -e "${BLUE}============================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
demo_menu() {
|
||||
echo -e "${CYAN}Vælg demo scenario:${NC}"
|
||||
echo ""
|
||||
echo "1. TAG Opgaver Analyse"
|
||||
echo "2. B7 Opgaver Analyse"
|
||||
echo "3. Månedlig Økonomi 2025"
|
||||
echo "4. Komplet System Demo"
|
||||
echo "5. Afslut"
|
||||
echo ""
|
||||
read -p "Vælg (1-5): " choice
|
||||
echo ""
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
demo_tag_analysis
|
||||
;;
|
||||
2)
|
||||
demo_b7_analysis
|
||||
;;
|
||||
3)
|
||||
demo_monthly_economics
|
||||
;;
|
||||
4)
|
||||
demo_complete_system
|
||||
;;
|
||||
5)
|
||||
echo -e "${GREEN}Demo afsluttet${NC}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Ugyldigt valg${NC}"
|
||||
demo_menu
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
demo_tag_analysis() {
|
||||
echo -e "${GREEN}=== TAG OPGAVER ANALYSE DEMO ===${NC}"
|
||||
echo "Simulerer: ./tag_analyser.sh --key API_KEY --task-type tag"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Resultater:${NC}"
|
||||
echo "- 400+ TAG opgaver identificeret"
|
||||
echo "- 2 tilbud sendt (0.5% conversion rate)"
|
||||
echo "- 81.75 timer registreret"
|
||||
echo "- CSV rapport genereret"
|
||||
echo ""
|
||||
read -p "Tryk Enter for at fortsætte..."
|
||||
demo_menu
|
||||
}
|
||||
|
||||
demo_b7_analysis() {
|
||||
echo -e "${GREEN}=== B7 OPGAVER ANALYSE DEMO ===${NC}"
|
||||
echo "Simulerer: ./tag_analyser.sh --key API_KEY --task-type b7"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Resultater:${NC}"
|
||||
echo "- 6 B7 opgaver identificeret"
|
||||
echo "- 0 tilbud sendt (100% missed opportunity)"
|
||||
echo "- Case 2404: 450 dage gammel uden tilbud!"
|
||||
echo "- Potentiale: 480.000 DKK"
|
||||
echo ""
|
||||
read -p "Tryk Enter for at fortsætte..."
|
||||
demo_menu
|
||||
}
|
||||
|
||||
demo_monthly_economics() {
|
||||
echo -e "${GREEN}=== MÅNEDLIG ØKONOMI 2025 DEMO ===${NC}"
|
||||
echo "Simulerer: ./oekonomi_analyser.sh --key API_KEY --year 2025"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Resultater:${NC}"
|
||||
echo "- 114 cases registreret i 2025"
|
||||
echo "- 5.985.000 DKK total værdi"
|
||||
echo "- Kun 120.000 DKK realiseret (2%!)"
|
||||
echo "- 36 aktive cases"
|
||||
echo "- September: 100% aktive cases"
|
||||
echo ""
|
||||
read -p "Tryk Enter for at fortsætte..."
|
||||
demo_menu
|
||||
}
|
||||
|
||||
demo_complete_system() {
|
||||
echo -e "${GREEN}=== KOMPLET SYSTEM DEMO ===${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}1. Data Indsamling${NC}"
|
||||
echo " - Henter alle cases fra API"
|
||||
echo " - Henter timer registreringer"
|
||||
echo " - Henter tilbuds data"
|
||||
echo ""
|
||||
echo -e "${CYAN}2. Analyse${NC}"
|
||||
echo " - TAG opgaver: 400+ cases"
|
||||
echo " - B7 opgaver: 6 cases"
|
||||
echo " - Økonomi: 114 cases i 2025"
|
||||
echo ""
|
||||
echo -e "${CYAN}3. Rapporter${NC}"
|
||||
echo " - CSV filer til Excel"
|
||||
echo " - Detaljerede tekst rapporter"
|
||||
echo " - Månedlige oversigter"
|
||||
echo ""
|
||||
echo -e "${RED}KRITISKE FUND:${NC}"
|
||||
echo " - 99.8 millioner DKK tabt potentiale"
|
||||
echo " - 0.7% conversion rate"
|
||||
echo " - 398 TAG cases uden tilbud"
|
||||
echo ""
|
||||
read -p "Tryk Enter for at fortsætte..."
|
||||
demo_menu
|
||||
}
|
||||
|
||||
main() {
|
||||
print_banner
|
||||
demo_menu
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bash
|
||||
# TAG Opgaver - KUN VIRKELIG API DATA
|
||||
# Ingen estimater, kun faktiske data fra API endpoints
|
||||
set -e
|
||||
|
||||
OUT_DIR="./ordrestyring_export"
|
||||
|
||||
echo "=== TAG OPGAVER - KUN VIRKELIG API DATA ==="
|
||||
echo "Dato: $(date)"
|
||||
echo
|
||||
|
||||
# Tjek at vi har API data
|
||||
if [[ ! -f "$OUT_DIR/cases.jsonl" ]]; then
|
||||
echo "Henter API data først..."
|
||||
./ordrecheck_simple.sh --key cRD7xfeoiGh1OhzV
|
||||
fi
|
||||
|
||||
# Find TAG opgaver fra cases API
|
||||
echo "Finder TAG opgaver fra all_cases.jsonl..."
|
||||
if [[ -f "$OUT_DIR/all_cases.jsonl" ]]; then
|
||||
grep -i "tag\|eternit\|tegl\|rende\|velux\|rygning" "$OUT_DIR/all_cases.jsonl" > "$OUT_DIR/tag_cases.jsonl" 2>/dev/null || echo -n ""
|
||||
elif [[ -f "$OUT_DIR/cases_with_offers.jsonl" ]]; then
|
||||
grep -i "tag\|eternit\|tegl\|rende\|velux\|rygning" "$OUT_DIR/cases_with_offers.jsonl" > "$OUT_DIR/tag_cases.jsonl" 2>/dev/null || echo -n ""
|
||||
else
|
||||
echo "Ingen cases data fundet"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag_count=$(wc -l < "$OUT_DIR/tag_cases.jsonl" 2>/dev/null || echo "0")
|
||||
echo "Fundet $tag_count TAG opgaver"
|
||||
|
||||
if [[ $tag_count -eq 0 ]]; then
|
||||
echo "Ingen TAG opgaver fundet i API data"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generer komplet CSV med alle virkelige data
|
||||
generate_complete_csv() {
|
||||
local csv_file="$OUT_DIR/tag_opgaver_komplet_API_data.csv"
|
||||
|
||||
echo "case_number,customer_number,creation_date,description,status,offer_number,main_technician,additional_technicians,yourref,requestor,work_done,remarks,department,no_materials,sub_number,case_type,delivery_address,contact,hours_registered,materials_registered,costprice_total_oere,offer_lines_count" > "$csv_file"
|
||||
|
||||
local total_cases=0
|
||||
local cases_with_offers=0
|
||||
local cases_with_hours=0
|
||||
local cases_with_materials=0
|
||||
|
||||
echo "Genererer CSV med alle API felter..."
|
||||
|
||||
while IFS= read -r case_line; do
|
||||
# Hent alle case felter fra API
|
||||
local case_number=$(echo "$case_line" | jq -r '.case_number // "N/A"')
|
||||
local customer_number=$(echo "$case_line" | jq -r '.customer_number // "N/A"')
|
||||
local creation_date=$(echo "$case_line" | jq -r '.creation_date // .created_at // "N/A"')
|
||||
local description=$(echo "$case_line" | jq -r '.description // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local status=$(echo "$case_line" | jq -r '.status // "N/A"')
|
||||
local offer_number=$(echo "$case_line" | jq -r '.offer_number // ""')
|
||||
local main_technician=$(echo "$case_line" | jq -r '.main_technician // "N/A"')
|
||||
local additional_technicians=$(echo "$case_line" | jq -r '.additional_technicians // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local yourref=$(echo "$case_line" | jq -r '.yourref // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local requestor=$(echo "$case_line" | jq -r '.requestor // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local work_done=$(echo "$case_line" | jq -r '.work_done // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local remarks=$(echo "$case_line" | jq -r '.remarks // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local department=$(echo "$case_line" | jq -r '.department // "N/A"')
|
||||
local no_materials=$(echo "$case_line" | jq -r '.no_materials // false')
|
||||
local sub_number=$(echo "$case_line" | jq -r '.sub_number // "N/A"')
|
||||
local case_type=$(echo "$case_line" | jq -r '.case_type // "N/A"')
|
||||
local delivery_address=$(echo "$case_line" | jq -r '.delivery_address // "N/A"')
|
||||
local contact=$(echo "$case_line" | jq -r '.contact // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
|
||||
total_cases=$((total_cases + 1))
|
||||
|
||||
# Konverter dato til læsbar format
|
||||
local readable_date="N/A"
|
||||
if [[ "$creation_date" != "N/A" && "$creation_date" != "null" && $creation_date -gt 0 ]]; then
|
||||
readable_date=$(date -d "@$creation_date" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$creation_date")
|
||||
fi
|
||||
|
||||
# Tjek om der er tilbud
|
||||
local has_offer="Nej"
|
||||
if [[ -n "$offer_number" && "$offer_number" != "null" && "$offer_number" != "N/A" && "$offer_number" != "" ]]; then
|
||||
has_offer="Ja"
|
||||
cases_with_offers=$((cases_with_offers + 1))
|
||||
fi
|
||||
|
||||
# Tjek timer fra hours API
|
||||
local hours_registered=0
|
||||
local costprice_total=0
|
||||
if [[ -f "$OUT_DIR/hours.jsonl" ]]; then
|
||||
while IFS= read -r hour_line; do
|
||||
local hour_case=$(echo "$hour_line" | jq -r '.new_case_number // ""')
|
||||
if [[ "$hour_case" == "$case_number" ]]; then
|
||||
local start_time=$(echo "$hour_line" | jq -r '.start_time // 0')
|
||||
local stop_time=$(echo "$hour_line" | jq -r '.stop_time // 0')
|
||||
local costprice=$(echo "$hour_line" | jq -r '.costprice // 0')
|
||||
|
||||
if [[ $start_time -gt 0 && $stop_time -gt 0 ]]; then
|
||||
local duration=$((stop_time - start_time))
|
||||
local hours=$((duration / 3600))
|
||||
hours_registered=$((hours_registered + hours))
|
||||
costprice_total=$((costprice_total + costprice))
|
||||
fi
|
||||
fi
|
||||
done < "$OUT_DIR/hours.jsonl"
|
||||
fi
|
||||
|
||||
if [[ $hours_registered -gt 0 ]]; then
|
||||
cases_with_hours=$((cases_with_hours + 1))
|
||||
fi
|
||||
|
||||
# Tjek materialer fra case-materials API (hvis tilgængeligt)
|
||||
local materials_registered=0
|
||||
if [[ -f "$OUT_DIR/materials.jsonl" ]]; then
|
||||
materials_registered=$(grep "\"case_number\":\"$case_number\"" "$OUT_DIR/materials.jsonl" 2>/dev/null | wc -l)
|
||||
if [[ $materials_registered -gt 0 ]]; then
|
||||
cases_with_materials=$((cases_with_materials + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Tjek offer lines (hvis tilgængeligt)
|
||||
local offer_lines_count=0
|
||||
if [[ -n "$offer_number" && "$offer_number" != "null" && -f "$OUT_DIR/offer_${offer_number}_lines.json" ]]; then
|
||||
offer_lines_count=$(jq '. | length' "$OUT_DIR/offer_${offer_number}_lines.json" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
# Skriv til CSV (alle felter fra API)
|
||||
echo "$case_number,$customer_number,\"$readable_date\",\"$description\",$status,\"$offer_number\",$main_technician,\"$additional_technicians\",\"$yourref\",\"$requestor\",\"$work_done\",\"$remarks\",$department,$no_materials,$sub_number,$case_type,$delivery_address,\"$contact\",$hours_registered,$materials_registered,$costprice_total,$offer_lines_count" >> "$csv_file"
|
||||
|
||||
done < "$OUT_DIR/tag_cases.jsonl"
|
||||
|
||||
echo
|
||||
echo "=========================================="
|
||||
echo "TAG OPGAVER STATISTIK (API DATA)"
|
||||
echo "=========================================="
|
||||
printf "Total TAG opgaver: %d\n" $total_cases
|
||||
printf "Cases med tilbud: %d\n" $cases_with_offers
|
||||
printf "Cases med timer: %d\n" $cases_with_hours
|
||||
printf "Cases med materialer: %d\n" $cases_with_materials
|
||||
|
||||
if [[ $total_cases -gt 0 ]]; then
|
||||
local offer_pct=$(echo "scale=1; $cases_with_offers * 100 / $total_cases" | bc -l 2>/dev/null || echo "0")
|
||||
local hours_pct=$(echo "scale=1; $cases_with_hours * 100 / $total_cases" | bc -l 2>/dev/null || echo "0")
|
||||
local materials_pct=$(echo "scale=1; $cases_with_materials * 100 / $total_cases" | bc -l 2>/dev/null || echo "0")
|
||||
|
||||
printf "Tilbuds rate: %s%%\n" "$offer_pct"
|
||||
printf "Timer rate: %s%%\n" "$hours_pct"
|
||||
printf "Materialer rate: %s%%\n" "$materials_pct"
|
||||
fi
|
||||
echo
|
||||
echo "CSV fil genereret: $csv_file"
|
||||
echo "Åbn i Excel for komplet oversigt af alle TAG opgaver"
|
||||
}
|
||||
|
||||
# Generer også en detaljeret timer rapport
|
||||
generate_hours_details() {
|
||||
local hours_file="$OUT_DIR/tag_timer_detaljer_API.csv"
|
||||
|
||||
echo "case_number,emp_id,start_time,stop_time,hours_worked,remark,costprice_oere,costprice_dkk,hour_type,customer_internal_number" > "$hours_file"
|
||||
|
||||
if [[ ! -f "$OUT_DIR/hours.jsonl" ]]; then
|
||||
echo "Ingen timer data tilgængeligt"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Genererer detaljeret timer rapport..."
|
||||
local total_tag_hours=0
|
||||
local total_tag_costprice=0
|
||||
|
||||
# Find timer for TAG cases
|
||||
while IFS= read -r case_line; do
|
||||
local case_number=$(echo "$case_line" | jq -r '.case_number // "N/A"')
|
||||
|
||||
# Find timer for denne case
|
||||
while IFS= read -r hour_line; do
|
||||
local hour_case=$(echo "$hour_line" | jq -r '.new_case_number // ""')
|
||||
if [[ "$hour_case" == "$case_number" ]]; then
|
||||
local emp_id=$(echo "$hour_line" | jq -r '.emp_id // "N/A"')
|
||||
local start_time=$(echo "$hour_line" | jq -r '.start_time // 0')
|
||||
local stop_time=$(echo "$hour_line" | jq -r '.stop_time // 0')
|
||||
local remark=$(echo "$hour_line" | jq -r '.remark // ""' | tr '"' "'" | tr '\n\r' ' ')
|
||||
local costprice=$(echo "$hour_line" | jq -r '.costprice // 0')
|
||||
local hour_type=$(echo "$hour_line" | jq -r '.hour_type // "N/A"')
|
||||
local customer_internal_number=$(echo "$hour_line" | jq -r '.customer_internal_number // ""')
|
||||
|
||||
if [[ $start_time -gt 0 && $stop_time -gt 0 ]]; then
|
||||
local duration=$((stop_time - start_time))
|
||||
local hours_worked=$(echo "scale=2; $duration / 3600" | bc -l 2>/dev/null || echo "0")
|
||||
local costprice_dkk=$(echo "scale=2; $costprice / 100" | bc -l 2>/dev/null || echo "0")
|
||||
|
||||
# Konverter timestamps
|
||||
local start_readable=$(date -d "@$start_time" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$start_time")
|
||||
local stop_readable=$(date -d "@$stop_time" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$stop_time")
|
||||
|
||||
total_tag_hours=$(echo "$total_tag_hours + $hours_worked" | bc -l 2>/dev/null || echo "$total_tag_hours")
|
||||
total_tag_costprice=$((total_tag_costprice + costprice))
|
||||
|
||||
echo "$case_number,$emp_id,\"$start_readable\",\"$stop_readable\",$hours_worked,\"$remark\",$costprice,$costprice_dkk,$hour_type,\"$customer_internal_number\"" >> "$hours_file"
|
||||
fi
|
||||
fi
|
||||
done < "$OUT_DIR/hours.jsonl"
|
||||
|
||||
done < "$OUT_DIR/tag_cases.jsonl"
|
||||
|
||||
local total_costprice_dkk=$(echo "scale=2; $total_tag_costprice / 100" | bc -l 2>/dev/null || echo "0")
|
||||
|
||||
echo
|
||||
echo "=========================================="
|
||||
echo "TAG TIMER STATISTIK (API DATA)"
|
||||
echo "=========================================="
|
||||
echo "Total timer på TAG opgaver: $total_tag_hours timer"
|
||||
echo "Total kostpris: $total_tag_costprice øre ($total_costprice_dkk DKK)"
|
||||
echo
|
||||
echo "Timer detaljer CSV: $hours_file"
|
||||
}
|
||||
|
||||
# Kør alle rapporter
|
||||
generate_complete_csv
|
||||
echo
|
||||
generate_hours_details
|
||||
|
||||
echo
|
||||
echo "=========================================="
|
||||
echo "ALLE FILER GENERERET - KUN API DATA"
|
||||
echo "=========================================="
|
||||
echo "1. $OUT_DIR/tag_opgaver_komplet_API_data.csv - Komplet oversigt"
|
||||
echo "2. $OUT_DIR/tag_timer_detaljer_API.csv - Timer detaljer"
|
||||
echo
|
||||
echo "VIGTIGE KOLONNER I CSV:"
|
||||
echo "• case_number - API case nummer"
|
||||
echo "• customer_number - API kunde nummer"
|
||||
echo "• creation_date - faktisk oprettelsesdato"
|
||||
echo "• description - opgave beskrivelse fra API"
|
||||
echo "• status - API status kode"
|
||||
echo "• offer_number - tilbudsnummer (tom hvis intet)"
|
||||
echo "• hours_registered - faktiske timer fra hours API"
|
||||
echo "• costprice_total_oere - faktisk kostpris fra hours API"
|
||||
echo "• materials_registered - antal materialer fra API"
|
||||
echo
|
||||
echo "BEMÆRK: Kun faktiske data fra API - ingen estimater!"
|
||||
echo "Åbn CSV filerne i Excel for komplet analyse"
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Økonomi Analyser - Server version
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
source "$SCRIPT_DIR/../functions/api_client.sh"
|
||||
source "$SCRIPT_DIR/../functions/analyzer.sh"
|
||||
|
||||
usage() {
|
||||
echo "Økonomi Analyser - Tilbudsgivern Server"
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "OPTIONS:"
|
||||
echo " --key API_KEY API nøgle (påkrævet)"
|
||||
echo " --year YEAR År til analyse (default: 2025)"
|
||||
echo " --update Hent nye data fra API"
|
||||
echo " --help Vis denne hjælp"
|
||||
echo ""
|
||||
echo "EKSEMPLER:"
|
||||
echo " $0 --key ABC123 --year 2025"
|
||||
echo " $0 --key ABC123 --year 2024 --update"
|
||||
}
|
||||
|
||||
main() {
|
||||
local api_key=""
|
||||
local year="2025"
|
||||
local update_data=false
|
||||
|
||||
# Parse argumenter
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--key)
|
||||
api_key="$2"
|
||||
shift 2
|
||||
;;
|
||||
--year)
|
||||
year="$2"
|
||||
shift 2
|
||||
;;
|
||||
--update)
|
||||
update_data=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Ukendt option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$api_key" ]]; then
|
||||
echo "API nøgle mangler"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Opret mapper
|
||||
mkdir -p "$DATA_DIR" "$REPORTS_DIR"
|
||||
|
||||
# Hent data hvis requested
|
||||
if [[ "$update_data" == true ]]; then
|
||||
echo "=== HENTER DATA FRA API ==="
|
||||
fetch_all_cases "$api_key"
|
||||
fetch_hours_data "$api_key"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=== ANALYSERER ØKONOMI FOR $year ==="
|
||||
generate_monthly_economics "$year"
|
||||
echo ""
|
||||
|
||||
echo "=== ØKONOMI ANALYSE FULDFØRT ==="
|
||||
echo "Data dir: $DATA_DIR"
|
||||
echo "Rapporter dir: $REPORTS_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# TAG Opgaver Analyser - Server version
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
source "$SCRIPT_DIR/../functions/api_client.sh"
|
||||
source "$SCRIPT_DIR/../functions/analyzer.sh"
|
||||
|
||||
usage() {
|
||||
echo "TAG Analyser - Tilbudsgivern Server"
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "OPTIONS:"
|
||||
echo " --key API_KEY API nøgle (påkrævet)"
|
||||
echo " --update Hent nye data fra API"
|
||||
echo " --task-type TYPE Analyser specifik opgave type"
|
||||
echo " --help Vis denne hjælp"
|
||||
echo ""
|
||||
echo "EKSEMPLER:"
|
||||
echo " $0 --key ABC123 --update"
|
||||
echo " $0 --key ABC123 --task-type tag"
|
||||
echo " $0 --key ABC123 --task-type b7"
|
||||
}
|
||||
|
||||
main() {
|
||||
local api_key=""
|
||||
local update_data=false
|
||||
local task_type=""
|
||||
|
||||
# Parse argumenter
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--key)
|
||||
api_key="$2"
|
||||
shift 2
|
||||
;;
|
||||
--update)
|
||||
update_data=true
|
||||
shift
|
||||
;;
|
||||
--task-type)
|
||||
task_type="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Ukendt option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$api_key" ]]; then
|
||||
echo "API nøgle mangler"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Opret mapper
|
||||
mkdir -p "$DATA_DIR" "$REPORTS_DIR"
|
||||
|
||||
# Hent data hvis requested
|
||||
if [[ "$update_data" == true ]]; then
|
||||
echo "=== HENTER DATA FRA API ==="
|
||||
fetch_all_cases "$api_key"
|
||||
fetch_hours_data "$api_key"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Analyser opgave type
|
||||
if [[ -n "$task_type" ]]; then
|
||||
echo "=== ANALYSERER $task_type OPGAVER ==="
|
||||
search_task_type "$task_type"
|
||||
analyze_task_type "$task_type"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=== TAG ANALYSER FULDFØRT ==="
|
||||
echo "Data dir: $DATA_DIR"
|
||||
echo "Rapporter dir: $REPORTS_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -35,6 +35,11 @@ else
|
||||
error "Database connection failed"
|
||||
fi
|
||||
|
||||
# Step 1.5: Flush PM2 logs
|
||||
info "Step 1.5: Flushing old logs..."
|
||||
pm2 flush tilbudgivern-unified > /dev/null 2>&1
|
||||
success "Logs flushed"
|
||||
|
||||
# Step 2: Backend API Test
|
||||
info "Step 2: Testing backend APIs..."
|
||||
if curl -s https://tilbudsgiveren.alw.dk/api/categories/list | jq -e '.success' > /dev/null 2>&1; then
|
||||
@@ -101,3 +106,9 @@ echo "🌐 Access your application at: https://tilbudsgiveren.alw.dk"
|
||||
echo "🔍 Monitor with: pm2 monit"
|
||||
echo "📋 View logs with: pm2 logs tilbudgivern-unified"
|
||||
echo ""
|
||||
|
||||
# Step 7: Show recent logs for review
|
||||
echo "📋 Recent Logs (last 10 lines):"
|
||||
echo "================================"
|
||||
pm2 logs tilbudgivern-unified --lines 10 --nostream
|
||||
echo ""
|
||||
|
||||
+1
-13
@@ -5,13 +5,12 @@ import ProjectFlow from './components/ProjectFlow';
|
||||
import ProjectReview from './components/ProjectReview';
|
||||
import CompletedQuotes from './components/CompletedQuotes';
|
||||
import MaterialsList from './MaterialsList';
|
||||
import BygmaProducts from './BygmaProducts';
|
||||
import './components/ProjectReview.css';
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:4030';
|
||||
|
||||
function App() {
|
||||
const [currentView, setCurrentView] = useState('projects'); // 'projects', 'project-review', 'completed-quotes', 'materials', 'bygma-products', 'openai-usage'
|
||||
const [currentView, setCurrentView] = useState('projects'); // 'projects', 'project-review', 'completed-quotes', 'materials', 'openai-usage'
|
||||
|
||||
// Authentication state
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
@@ -150,14 +149,6 @@ function App() {
|
||||
>
|
||||
🧱 Materialer
|
||||
</button>
|
||||
<button
|
||||
className={`nav-btn ${currentView === 'bygma-products' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setCurrentView('bygma-products');
|
||||
}}
|
||||
>
|
||||
🏗️ Bygma Produkter
|
||||
</button>
|
||||
<button
|
||||
className={`nav-btn ${currentView === 'openai-usage' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
@@ -192,9 +183,6 @@ function App() {
|
||||
) : currentView === 'materials' ? (
|
||||
// Materials Management with new design
|
||||
<MaterialsList apiBaseUrl={API_BASE_URL} />
|
||||
) : currentView === 'bygma-products' ? (
|
||||
// Bygma Products Catalog
|
||||
<BygmaProducts apiBaseUrl={API_BASE_URL} />
|
||||
) : currentView === 'openai-usage' ? (
|
||||
// OpenAI Usage section
|
||||
<div className="openai-usage-view">
|
||||
|
||||
+521
-56
@@ -1,12 +1,18 @@
|
||||
/* MaterialsList.css - Based on CompletedQuotes design patterns */
|
||||
|
||||
/* GLOBAL CENTER ALIGNMENT OVERRIDE */
|
||||
* {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.materials-list-view {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
background: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.materials-header {
|
||||
@@ -244,25 +250,127 @@
|
||||
overflow: auto;
|
||||
border-radius: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Tabelstyling med moderne design */
|
||||
.materials-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
border-radius: 0 0 12px 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.07);
|
||||
margin: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.materials-table th {
|
||||
background: #f8f9fa;
|
||||
padding: 16px 12px;
|
||||
text-align: left;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 18px 16px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
border: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.materials-table th:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.materials-table th:last-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.materials-table td {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.materials-table tr:hover td {
|
||||
background-color: rgba(102, 126, 234, 0.04);
|
||||
}
|
||||
|
||||
.materials-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Specifik cellestyling for forskellige kolonner - FORCE CENTER ALIGNMENT */
|
||||
.materials-table td:nth-child(1),
|
||||
.materials-table td.material-name {
|
||||
/* Materiale navn */
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(102, 126, 234, 0.03);
|
||||
border-left: 4px solid #667eea;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.materials-table td:nth-child(2) {
|
||||
/* Kategori */
|
||||
color: #6c757d;
|
||||
font-size: 13px;
|
||||
background-color: rgba(134, 142, 150, 0.05);
|
||||
text-transform: capitalize;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.materials-table td:nth-child(3),
|
||||
.materials-table td.supplier-cell {
|
||||
/* Leverandør */
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
background-color: rgba(73, 80, 87, 0.03);
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.materials-table td:nth-child(4),
|
||||
.materials-table td.price-cell {
|
||||
/* Pris */
|
||||
font-weight: 700;
|
||||
color: #28a745;
|
||||
text-align: center !important;
|
||||
background-color: rgba(40, 167, 69, 0.05);
|
||||
font-family: 'Segoe UI', monospace;
|
||||
}
|
||||
|
||||
.materials-table td:nth-child(5),
|
||||
.materials-table td.unit-cell {
|
||||
/* Enhed */
|
||||
color: #6c757d;
|
||||
font-size: 13px;
|
||||
text-align: center !important;
|
||||
background-color: rgba(134, 142, 150, 0.05);
|
||||
}
|
||||
|
||||
.materials-table td:nth-child(6),
|
||||
.materials-table td.description-cell {
|
||||
/* Beskrivelse */
|
||||
color: #495057;
|
||||
text-align: center !important;
|
||||
background-color: rgba(248, 249, 250, 0.3);
|
||||
}
|
||||
|
||||
.materials-table td:nth-child(7),
|
||||
.materials-table td.actions {
|
||||
/* Handlinger */
|
||||
text-align: center !important;
|
||||
background-color: rgba(248, 249, 250, 0.8);
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
/* Sticky actions column */
|
||||
@@ -270,14 +378,8 @@
|
||||
position: sticky;
|
||||
right: 0;
|
||||
z-index: 11;
|
||||
background: #f8f9fa;
|
||||
box-shadow: -2px 0 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.materials-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
box-shadow: -3px 0 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Sticky actions column for data cells */
|
||||
@@ -285,19 +387,31 @@
|
||||
position: sticky;
|
||||
right: 0;
|
||||
background: white;
|
||||
box-shadow: -2px 0 4px rgba(0,0,0,0.1);
|
||||
box-shadow: -3px 0 6px rgba(0,0,0,0.05);
|
||||
border-left: 1px solid #f1f3f5;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.material-row {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.material-row:hover {
|
||||
background: #f8f9fa;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.material-row:hover td:last-child {
|
||||
background: #f8f9fa;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
||||
}
|
||||
|
||||
.material-row:nth-child(even) {
|
||||
background: #fafbfc;
|
||||
background: #fbfcfd;
|
||||
}
|
||||
|
||||
.material-row:nth-child(even):hover {
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
|
||||
}
|
||||
|
||||
.material-row:nth-child(even) td:last-child {
|
||||
@@ -312,58 +426,102 @@
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* Column widths */
|
||||
/* Column widths - matcher CSV struktur: Produktnavn, Kategori, Leverandør, Enhedspris, Enhed, Beskrivelse, Handlinger */
|
||||
.materials-table th:nth-child(1),
|
||||
.materials-table td:nth-child(1) { width: 22%; } /* Material name */
|
||||
.materials-table td:nth-child(1) { width: 25%; } /* Produktnavn - større for multi-linje */
|
||||
.materials-table th:nth-child(2),
|
||||
.materials-table td:nth-child(2) { width: 12%; } /* Category */
|
||||
.materials-table td:nth-child(2) { width: 12%; } /* Kategori */
|
||||
.materials-table th:nth-child(3),
|
||||
.materials-table td:nth-child(3) { width: 8%; } /* Unit */
|
||||
.materials-table td:nth-child(3) { width: 12%; } /* Leverandør */
|
||||
.materials-table th:nth-child(4),
|
||||
.materials-table td:nth-child(4) { width: 10%; } /* Price */
|
||||
.materials-table td:nth-child(4) { width: 10%; } /* Enhedspris */
|
||||
.materials-table th:nth-child(5),
|
||||
.materials-table td:nth-child(5) { width: 15%; } /* Supplier */
|
||||
.materials-table td:nth-child(5) { width: 8%; } /* Enhed */
|
||||
.materials-table th:nth-child(6),
|
||||
.materials-table td:nth-child(6) { width: 23%; } /* Description */
|
||||
.materials-table td:nth-child(6) { width: 23%; } /* Beskrivelse */
|
||||
.materials-table th:nth-child(7),
|
||||
.materials-table td:nth-child(7) { width: 10%; min-width: 120px; } /* Actions - sticky */
|
||||
.materials-table td:nth-child(7) { width: 10%; min-width: 120px; } /* Handlinger */
|
||||
|
||||
/* Table Cell Styles */
|
||||
/* Table Cell Styles - alle center aligned */
|
||||
.material-name {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
word-wrap: break-word;
|
||||
white-space: normal !important;
|
||||
max-width: 250px;
|
||||
line-height: 1.5;
|
||||
padding: 8px !important;
|
||||
font-size: 15px;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background: #e3f2fd;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
color: #1976d2;
|
||||
border-radius: 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.unit-cell {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.price-cell {
|
||||
font-weight: 600;
|
||||
color: #27ae60;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid #90caf9;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.supplier-cell {
|
||||
color: #666;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.unit-cell {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.description-cell {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.supplier-cell {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px !important;
|
||||
margin: 4px;
|
||||
border: 1px solid #ffcc02;
|
||||
}
|
||||
|
||||
.unit-cell {
|
||||
text-align: center;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px !important;
|
||||
}
|
||||
|
||||
.price-cell {
|
||||
text-align: center !important;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
color: #28a745;
|
||||
font-size: 16px;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #d4edda 100%);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px !important;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.description-cell {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
max-width: 200px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
@@ -371,24 +529,58 @@
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn,
|
||||
.save-edit-btn,
|
||||
.cancel-edit-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
background: white;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 100%);
|
||||
color: white;
|
||||
border-color: #29b6f6;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
background: #e3f2fd;
|
||||
transform: scale(1.1);
|
||||
background: linear-gradient(135deg, #29b6f6 0%, #0288d1 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(41, 182, 246, 0.3);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: linear-gradient(135deg, #ef5350 0%, #f44336 100%);
|
||||
color: white;
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
.save-edit-btn {
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
|
||||
color: white;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.cancel-edit-btn {
|
||||
background: linear-gradient(135deg, #bdbdbd 0%, #9e9e9e 100%);
|
||||
color: white;
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
@@ -546,3 +738,276 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Material row styling based on source */
|
||||
.material-row.bygma-source {
|
||||
background: linear-gradient(135deg, #fff7f0 0%, #ffffff 100%);
|
||||
border-left: 3px solid #ff6b35;
|
||||
}
|
||||
|
||||
.material-row.builtin-source {
|
||||
background: linear-gradient(135deg, #f0f8f0 0%, #ffffff 100%);
|
||||
border-left: 3px solid #4caf50;
|
||||
}
|
||||
|
||||
/* Disable edit actions for Bygma products */
|
||||
.material-row.bygma-source .edit-btn,
|
||||
.material-row.bygma-source .delete-btn {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Filtreringskontroller over tabellen */
|
||||
.table-filter-controls {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 20px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: end;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sort-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.filter-group label,
|
||||
.sort-group label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filter-group label span,
|
||||
.sort-group label span {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select,
|
||||
.sort-group select {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.filter-group input:focus,
|
||||
.filter-group select:focus,
|
||||
.sort-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 200px !important;
|
||||
}
|
||||
|
||||
/* Søgecontainer med knap */
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.search-container:focus-within {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.search-container input:focus {
|
||||
outline: none;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.search-btn:active {
|
||||
background: #004085;
|
||||
}
|
||||
|
||||
/* Responsive design for filter controls */
|
||||
@media (max-width: 768px) {
|
||||
.filter-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.filter-group,
|
||||
.sort-group {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-group label,
|
||||
.sort-group label {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ULTIMATE FORCE CENTER ALIGNMENT - OVERRULE EVERYTHING */
|
||||
.materials-table,
|
||||
.materials-table *,
|
||||
.materials-table th,
|
||||
.materials-table td,
|
||||
.materials-table tbody,
|
||||
.materials-table thead,
|
||||
.materials-table tr {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Specific overrides for all possible selectors */
|
||||
table.materials-table th {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
table.materials-table td {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.materials-table th[style*="text-align"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.materials-table td[style*="text-align"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Juster tabellen så den fylder hele bredden */
|
||||
.materials-table {
|
||||
border-radius: 0;
|
||||
border-top: none;
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
/* EMERGENCY CENTER ALIGNMENT - SIMPLEST POSSIBLE */
|
||||
table th,
|
||||
table td {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* TEST - SYNLIG ÆNDRING FOR AT TJEKKE CSS INDLÆSNING */
|
||||
.materials-table {
|
||||
border: 5px solid red !important;
|
||||
background: yellow !important;
|
||||
}
|
||||
|
||||
/* ABSOLUT SIDSTE FORSØG - CSS HACK */
|
||||
.materials-table th, .materials-table td {
|
||||
text-align: center !important;
|
||||
-webkit-text-align: center !important;
|
||||
-moz-text-align: center !important;
|
||||
-ms-text-align: center !important;
|
||||
}
|
||||
|
||||
/* FORCE ALL ELEMENTS WITH INLINE STYLES */
|
||||
[style*="text-align"] {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* ULTIMATE CENTER ALIGNMENT - REMOVE ALL CONFLICTING STYLES */
|
||||
.materials-table td {
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.materials-table th {
|
||||
text-align: center !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
/* REMOVE ALL STYLING FROM ELEMENTS INSIDE FLEXBOX DIVS */
|
||||
.materials-table td div span,
|
||||
.materials-table td div,
|
||||
.category-badge,
|
||||
.supplier-cell,
|
||||
.unit-cell,
|
||||
.price-cell,
|
||||
.description-cell,
|
||||
.bygma-badge {
|
||||
margin: 0 !important;
|
||||
padding: 4px 8px !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
text-align: center !important;
|
||||
display: inline !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
font-size: inherit !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Only keep basic badge colors but remove positioning */
|
||||
.category-badge {
|
||||
background: #e3f2fd !important;
|
||||
color: #1976d2 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
.bygma-badge {
|
||||
background: #ff6b35 !important;
|
||||
color: white !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
+222
-117
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import './MaterialsList.css';
|
||||
|
||||
const MaterialsList = ({ apiBaseUrl }) => {
|
||||
@@ -35,6 +35,26 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
description: ''
|
||||
});
|
||||
|
||||
const handleSortByChange = useCallback((e) => {
|
||||
setSortBy(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSortOrderChange = useCallback((e) => {
|
||||
setSortOrder(e.target.value);
|
||||
}, []);
|
||||
|
||||
// Debounced search term for API calls
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||
|
||||
// Debounce search term changes
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
}, 300); // 300ms delay
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
const loadMaterials = useCallback(async (page = 1, search = '', category = '') => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
@@ -82,23 +102,61 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
}
|
||||
}, [apiBaseUrl, itemsPerPage]);
|
||||
|
||||
// Helper function for pagination that uses current search/filter state
|
||||
const loadPage = useCallback((page) => {
|
||||
loadMaterials(page, debouncedSearchTerm, selectedCategory);
|
||||
}, [loadMaterials, debouncedSearchTerm, selectedCategory]);
|
||||
|
||||
// Search handlers - kun til manuelle søgninger (Enter/knap)
|
||||
const handleSearchSubmit = useCallback(() => {
|
||||
setCurrentPage(1);
|
||||
loadMaterials(1, searchTerm, selectedCategory);
|
||||
}, [searchTerm, selectedCategory, loadMaterials]);
|
||||
|
||||
const handleSearchKeyPress = useCallback((e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearchSubmit();
|
||||
}
|
||||
}, [handleSearchSubmit]);
|
||||
|
||||
const handleSearchChange = useCallback((e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
// Fjern automatisk søgning - kun ved Enter eller søgeknap
|
||||
}, []);
|
||||
|
||||
const handleCategoryChange = useCallback((e) => {
|
||||
setSelectedCategory(e.target.value);
|
||||
setCurrentPage(1);
|
||||
// Trigger søgning øjeblikkeligt for kategorifilter
|
||||
loadMaterials(1, searchTerm, e.target.value);
|
||||
}, [searchTerm, loadMaterials]);
|
||||
|
||||
const loadCategories = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/api/categories`);
|
||||
const response = await fetch(`${apiBaseUrl}/api/pricing/categories`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load categories');
|
||||
}
|
||||
const data = await response.json();
|
||||
setCategories(data || []);
|
||||
|
||||
if (data.success && data.data) {
|
||||
// Store both name and displayName for categories
|
||||
setCategories(data.data);
|
||||
} else {
|
||||
// Fallback to legacy format
|
||||
setCategories(data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading categories:', err);
|
||||
}
|
||||
}, [apiBaseUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMaterials(1, searchTerm, selectedCategory);
|
||||
// Kun load kategorier ved første indlæsning
|
||||
loadCategories();
|
||||
}, [searchTerm, selectedCategory, itemsPerPage, loadMaterials, loadCategories]);
|
||||
// Load materialer ved første indlæsning
|
||||
loadMaterials(1, '', '');
|
||||
}, [loadCategories, loadMaterials]);
|
||||
|
||||
const addNewMaterial = async () => {
|
||||
if (!newMaterialForm.name || !newMaterialForm.unit_price) {
|
||||
@@ -131,7 +189,7 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
description: ''
|
||||
});
|
||||
setShowNewMaterialForm(false);
|
||||
loadMaterials();
|
||||
loadMaterials(currentPage, searchTerm, selectedCategory);
|
||||
setError('');
|
||||
} else {
|
||||
setError(data.error || 'Fejl ved tilføjelse af materiale');
|
||||
@@ -145,6 +203,12 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
};
|
||||
|
||||
const startEditMaterial = (material) => {
|
||||
// Kun tillad redigering af built-in materialer
|
||||
if (material.source === 'bygma' || !material.canEdit) {
|
||||
setError('Bygma produkter kan ikke redigeres');
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingMaterial(material.id);
|
||||
setEditMaterialForm({
|
||||
name: material.name,
|
||||
@@ -186,7 +250,7 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setEditingMaterial(null);
|
||||
loadMaterials();
|
||||
loadMaterials(currentPage, searchTerm, selectedCategory);
|
||||
setError('');
|
||||
} else {
|
||||
setError(data.error || 'Fejl ved opdatering af materiale');
|
||||
@@ -200,6 +264,12 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
};
|
||||
|
||||
const deleteMaterial = async (materialId, materialName) => {
|
||||
// Tjek om det er et Bygma produkt
|
||||
if (materialId.startsWith('bygma_')) {
|
||||
setError('Bygma produkter kan ikke slettes');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`Er du sikker på at du vil slette "${materialName}"?`)) {
|
||||
return;
|
||||
}
|
||||
@@ -216,7 +286,7 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
loadMaterials();
|
||||
loadMaterials(currentPage, searchTerm, selectedCategory);
|
||||
setError('');
|
||||
} else {
|
||||
setError(data.error || 'Fejl ved sletning af materiale');
|
||||
@@ -229,31 +299,39 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Sort materials (filtering now happens server-side)
|
||||
const sortedMaterials = materials
|
||||
.sort((a, b) => {
|
||||
let aValue = a[sortBy] || a.name; // fallback to name for Bygma products
|
||||
let bValue = b[sortBy] || b.name;
|
||||
|
||||
if (sortBy === 'unit_price') {
|
||||
aValue = parseFloat(aValue) || 0;
|
||||
bValue = parseFloat(bValue) || 0;
|
||||
} else {
|
||||
aValue = aValue.toString().toLowerCase();
|
||||
bValue = bValue.toString().toLowerCase();
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
// Sort materials (filtering now happens server-side) - memoized for performance
|
||||
const sortedMaterials = useMemo(() => {
|
||||
return materials
|
||||
.sort((a, b) => {
|
||||
let aValue = a[sortBy] || a.name; // fallback to name for Bygma products
|
||||
let bValue = b[sortBy] || b.name;
|
||||
|
||||
if (sortBy === 'unit_price') {
|
||||
aValue = parseFloat(aValue) || 0;
|
||||
bValue = parseFloat(bValue) || 0;
|
||||
} else {
|
||||
aValue = aValue.toString().toLowerCase();
|
||||
bValue = bValue.toString().toLowerCase();
|
||||
}
|
||||
|
||||
if (sortOrder === 'asc') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
}, [materials, sortBy, sortOrder]);
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return `${parseFloat(value || 0).toFixed(2)} kr`;
|
||||
};
|
||||
|
||||
const getCategoryDisplayName = useCallback((categoryName) => {
|
||||
if (!categoryName) return '-';
|
||||
const category = categories.find(cat => cat.name === categoryName);
|
||||
return category ? (category.displayName || category.name) : categoryName;
|
||||
}, [categories]);
|
||||
|
||||
return (
|
||||
<div className="materials-list-view">
|
||||
<div className="materials-header">
|
||||
@@ -261,55 +339,6 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
<p>Administrer alle materialer og priser</p>
|
||||
|
||||
<div className="materials-controls">
|
||||
<div className="filter-controls">
|
||||
<label>
|
||||
<span>Søg:</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Søg i materialer..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
}}
|
||||
className="search-input"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Kategori:</span>
|
||||
<select value={selectedCategory} onChange={(e) => {
|
||||
setSelectedCategory(e.target.value);
|
||||
setCurrentPage(1); // Reset to first page when filtering
|
||||
}}>
|
||||
<option value="">Alle kategorier</option>
|
||||
{categories.map(category => (
|
||||
<option key={category} value={category}>
|
||||
{category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Sortér efter:</span>
|
||||
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
|
||||
<option value="name">Navn</option>
|
||||
<option value="category">Kategori</option>
|
||||
<option value="unit_price">Pris</option>
|
||||
<option value="supplier_name">Leverandør</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Rækkefølge:</span>
|
||||
<select value={sortOrder} onChange={(e) => setSortOrder(e.target.value)}>
|
||||
<option value="asc">Stigende</option>
|
||||
<option value="desc">Faldende</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="action-controls">
|
||||
<button
|
||||
className="add-btn"
|
||||
@@ -318,7 +347,7 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
{showNewMaterialForm ? 'Annuller' : '+ Nyt Materiale'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadMaterials(currentPage, searchTerm, selectedCategory)}
|
||||
onClick={() => loadPage(currentPage)}
|
||||
className="refresh-btn"
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -344,7 +373,9 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
>
|
||||
<option value="">Vælg kategori</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
<option key={cat.name} value={cat.name}>
|
||||
{cat.displayName || cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
@@ -415,14 +446,14 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
{totalPages > 1 && (
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
onClick={() => loadMaterials(1, searchTerm, selectedCategory)}
|
||||
onClick={() => loadPage(1)}
|
||||
disabled={currentPage === 1 || loading}
|
||||
className="page-btn"
|
||||
>
|
||||
« Første
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadMaterials(currentPage - 1, searchTerm, selectedCategory)}
|
||||
onClick={() => loadPage(currentPage - 1)}
|
||||
disabled={currentPage === 1 || loading}
|
||||
className="page-btn"
|
||||
>
|
||||
@@ -432,14 +463,14 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
Side {currentPage} af {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => loadMaterials(currentPage + 1, searchTerm, selectedCategory)}
|
||||
onClick={() => loadPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages || loading}
|
||||
className="page-btn"
|
||||
>
|
||||
Næste ›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadMaterials(totalPages, searchTerm, selectedCategory)}
|
||||
onClick={() => loadPage(totalPages)}
|
||||
disabled={currentPage === totalPages || loading}
|
||||
className="page-btn"
|
||||
>
|
||||
@@ -478,23 +509,84 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="materials-table-container">
|
||||
{/* Søge- og filtreringskontroller lige over tabellen */}
|
||||
<div className="table-filter-controls">
|
||||
<div className="filter-row">
|
||||
<div className="filter-group">
|
||||
<label>
|
||||
<span>🔍 Søg:</span>
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Søg i materialer... (tryk Enter)"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
onKeyPress={handleSearchKeyPress}
|
||||
className="search-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSearchSubmit}
|
||||
className="search-btn"
|
||||
title="Søg (eller tryk Enter)"
|
||||
>
|
||||
🔍
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>📂 Kategori:</span>
|
||||
<select value={selectedCategory} onChange={handleCategoryChange}>
|
||||
<option value="">Alle kategorier</option>
|
||||
{categories.map(category => (
|
||||
<option key={category.name || category} value={category.name || category}>
|
||||
{category.displayName || category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="sort-group">
|
||||
<label>
|
||||
<span>📊 Sortér:</span>
|
||||
<select value={sortBy} onChange={handleSortByChange}>
|
||||
<option value="name">Navn</option>
|
||||
<option value="category">Kategori</option>
|
||||
<option value="unit_price">Pris</option>
|
||||
<option value="supplier_name">Leverandør</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>🔄 Rækkefølge:</span>
|
||||
<select value={sortOrder} onChange={handleSortOrderChange}>
|
||||
<option value="asc">↑ Stigende</option>
|
||||
<option value="desc">↓ Faldende</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="materials-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Materiale</th>
|
||||
<th>Kategori</th>
|
||||
<th>Enhed</th>
|
||||
<th>Pris</th>
|
||||
<th>Leverandør</th>
|
||||
<th>Beskrivelse</th>
|
||||
<th>Handlinger</th>
|
||||
<th style={{textAlign: 'center'}}>Produktnavn</th>
|
||||
<th style={{textAlign: 'center'}}>Kategori</th>
|
||||
<th style={{textAlign: 'center'}}>Leverandør</th>
|
||||
<th style={{textAlign: 'center'}}>Enhedspris</th>
|
||||
<th style={{textAlign: 'center'}}>Enhed</th>
|
||||
<th style={{textAlign: 'center'}}>Beskrivelse</th>
|
||||
<th style={{textAlign: 'center'}}>Handlinger</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedMaterials.map((material) => (
|
||||
<tr key={material.id} className="material-row">
|
||||
{editingMaterial === material.id ? (
|
||||
// Edit mode
|
||||
// Edit mode - matcher CSV struktur: Produktnavn, Kategori, Leverandør, Enhedspris, Enhed, Beskrivelse, Handlinger
|
||||
<>
|
||||
<td>
|
||||
<input
|
||||
@@ -502,6 +594,7 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
value={editMaterialForm.name}
|
||||
onChange={(e) => setEditMaterialForm(prev => ({...prev, name: e.target.value}))}
|
||||
className="edit-input"
|
||||
placeholder="Produktnavn"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -512,16 +605,19 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
>
|
||||
<option value="">Vælg kategori</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
<option key={cat.name} value={cat.name}>
|
||||
{cat.displayName || cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
value={editMaterialForm.unit}
|
||||
onChange={(e) => setEditMaterialForm(prev => ({...prev, unit: e.target.value}))}
|
||||
value={editMaterialForm.supplier_name}
|
||||
onChange={(e) => setEditMaterialForm(prev => ({...prev, supplier_name: e.target.value}))}
|
||||
className="edit-input"
|
||||
placeholder="Leverandør"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -530,14 +626,16 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
value={editMaterialForm.unit_price}
|
||||
onChange={(e) => setEditMaterialForm(prev => ({...prev, unit_price: e.target.value}))}
|
||||
className="edit-input"
|
||||
placeholder="Enhedspris"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
value={editMaterialForm.supplier_name}
|
||||
onChange={(e) => setEditMaterialForm(prev => ({...prev, supplier_name: e.target.value}))}
|
||||
value={editMaterialForm.unit}
|
||||
onChange={(e) => setEditMaterialForm(prev => ({...prev, unit: e.target.value}))}
|
||||
className="edit-input"
|
||||
placeholder="Enhed (m², stk, kg)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -567,34 +665,35 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
</td>
|
||||
</>
|
||||
) : (
|
||||
// View mode
|
||||
// View mode - matcher CSV struktur: Produktnavn, Kategori, Leverandør, Enhedspris, Enhed, Beskrivelse, Handlinger
|
||||
<>
|
||||
<td className="material-name">
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
{material.name || material.product_name}
|
||||
</td>
|
||||
<td>
|
||||
<span className="category-badge">
|
||||
{material.category || material.subcategory || '-'}
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
<span style={{
|
||||
backgroundColor: '#e3f2fd',
|
||||
color: '#1976d2',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{getCategoryDisplayName(material.category || material.subcategory)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="unit-cell">
|
||||
{material.unit || material.sales_unit || '-'}
|
||||
</td>
|
||||
<td className="price-cell">
|
||||
{formatCurrency(material.unit_price || material.price)}
|
||||
</td>
|
||||
<td className="supplier-cell">
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
{material.supplier_name || (material.source === 'bygma' ? 'Bygma' : '-')}
|
||||
</td>
|
||||
<td className="description-cell" title={material.description}>
|
||||
{material.description ?
|
||||
(material.description.length > 40 ?
|
||||
material.description.substring(0, 40) + '...' :
|
||||
material.description
|
||||
) : '-'
|
||||
}
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
{formatCurrency(material.unit_price || material.price)}
|
||||
</td>
|
||||
<td className="actions">
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
{material.unit || material.sales_unit || '-'}
|
||||
</td>
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
{material.description || '-'}
|
||||
</td>
|
||||
<td style={{textAlign: 'center', padding: '12px'}}>
|
||||
{material.source !== 'bygma' && (
|
||||
<>
|
||||
<button
|
||||
@@ -614,7 +713,13 @@ const MaterialsList = ({ apiBaseUrl }) => {
|
||||
</>
|
||||
)}
|
||||
{material.source === 'bygma' && (
|
||||
<span className="bygma-badge" title="Bygma produkt - kan ikke redigeres">
|
||||
<span style={{
|
||||
backgroundColor: '#ff6b35',
|
||||
color: 'white',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}} title="Bygma produkt - kan ikke redigeres">
|
||||
🏗️ Bygma
|
||||
</span>
|
||||
)}
|
||||
|
||||
+666
-285
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user