diff --git a/aos-shots/v3-audit.png b/aos-shots/v3-audit.png new file mode 100644 index 0000000..0073e87 Binary files /dev/null and b/aos-shots/v3-audit.png differ diff --git a/aos-shots/v3-company-drawer.png b/aos-shots/v3-company-drawer.png new file mode 100644 index 0000000..0073e87 Binary files /dev/null and b/aos-shots/v3-company-drawer.png differ diff --git a/aos-shots/v3-invoice-dialog.png b/aos-shots/v3-invoice-dialog.png new file mode 100644 index 0000000..16fd960 Binary files /dev/null and b/aos-shots/v3-invoice-dialog.png differ diff --git a/aos-shots/v3-projects-task.png b/aos-shots/v3-projects-task.png new file mode 100644 index 0000000..16fd960 Binary files /dev/null and b/aos-shots/v3-projects-task.png differ diff --git a/aos-shots/v4-audit.png b/aos-shots/v4-audit.png new file mode 100644 index 0000000..c950db6 Binary files /dev/null and b/aos-shots/v4-audit.png differ diff --git a/aos-shots/v4-company-drawer.png b/aos-shots/v4-company-drawer.png new file mode 100644 index 0000000..8efaefc Binary files /dev/null and b/aos-shots/v4-company-drawer.png differ diff --git a/aos-shots/v4-create-deal.png b/aos-shots/v4-create-deal.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-create-deal.png differ diff --git a/aos-shots/v4-invoice-dialog.png b/aos-shots/v4-invoice-dialog.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-invoice-dialog.png differ diff --git a/aos-shots/v4-lead-approved.png b/aos-shots/v4-lead-approved.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-lead-approved.png differ diff --git a/aos-shots/v4-lead-converted.png b/aos-shots/v4-lead-converted.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-lead-converted.png differ diff --git a/aos-shots/v4-projects-task.png b/aos-shots/v4-projects-task.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-projects-task.png differ diff --git a/aos-shots/v4-projects-time.png b/aos-shots/v4-projects-time.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-projects-time.png differ diff --git a/aos-shots/v5-company-drawer.png b/aos-shots/v5-company-drawer.png new file mode 100644 index 0000000..15c5e9d Binary files /dev/null and b/aos-shots/v5-company-drawer.png differ diff --git a/aos-shots/v6-audit-detail.png b/aos-shots/v6-audit-detail.png new file mode 100644 index 0000000..96cd986 Binary files /dev/null and b/aos-shots/v6-audit-detail.png differ diff --git a/aos-shots/v6-audit.png b/aos-shots/v6-audit.png new file mode 100644 index 0000000..4840186 Binary files /dev/null and b/aos-shots/v6-audit.png differ diff --git a/aos-shots/v6-command-search.png b/aos-shots/v6-command-search.png new file mode 100644 index 0000000..9261ac3 Binary files /dev/null and b/aos-shots/v6-command-search.png differ diff --git a/aos-shots/v6-company-drawer.png b/aos-shots/v6-company-drawer.png new file mode 100644 index 0000000..97decd4 Binary files /dev/null and b/aos-shots/v6-company-drawer.png differ diff --git a/aos-shots/v6-create-deal-done.png b/aos-shots/v6-create-deal-done.png new file mode 100644 index 0000000..f75aa3b Binary files /dev/null and b/aos-shots/v6-create-deal-done.png differ diff --git a/aos-shots/v6-create-deal-filled.png b/aos-shots/v6-create-deal-filled.png new file mode 100644 index 0000000..84447bf Binary files /dev/null and b/aos-shots/v6-create-deal-filled.png differ diff --git a/aos-shots/v6-create-deal.png b/aos-shots/v6-create-deal.png new file mode 100644 index 0000000..84447bf Binary files /dev/null and b/aos-shots/v6-create-deal.png differ diff --git a/aos-shots/v6-invoice-dialog.png b/aos-shots/v6-invoice-dialog.png new file mode 100644 index 0000000..84447bf Binary files /dev/null and b/aos-shots/v6-invoice-dialog.png differ diff --git a/aos-shots/v6-lead-approved.png b/aos-shots/v6-lead-approved.png new file mode 100644 index 0000000..5d49c8e Binary files /dev/null and b/aos-shots/v6-lead-approved.png differ diff --git a/aos-shots/v6-lead-converted.png b/aos-shots/v6-lead-converted.png new file mode 100644 index 0000000..5d49c8e Binary files /dev/null and b/aos-shots/v6-lead-converted.png differ diff --git a/aos-shots/v6-projects-task.png b/aos-shots/v6-projects-task.png new file mode 100644 index 0000000..949dce1 Binary files /dev/null and b/aos-shots/v6-projects-task.png differ diff --git a/aos-shots/v6-projects-time.png b/aos-shots/v6-projects-time.png new file mode 100644 index 0000000..72141ab Binary files /dev/null and b/aos-shots/v6-projects-time.png differ diff --git a/aos-shots/v7-palette-open.png b/aos-shots/v7-palette-open.png new file mode 100644 index 0000000..8a9a8de Binary files /dev/null and b/aos-shots/v7-palette-open.png differ diff --git a/aos-shots/v7-search-results.png b/aos-shots/v7-search-results.png new file mode 100644 index 0000000..3108fd7 Binary files /dev/null and b/aos-shots/v7-search-results.png differ diff --git a/aos-shots/v8-search-bauer.png b/aos-shots/v8-search-bauer.png new file mode 100644 index 0000000..2120f59 Binary files /dev/null and b/aos-shots/v8-search-bauer.png differ diff --git a/aos-verify4.sh b/aos-verify4.sh new file mode 100644 index 0000000..98fc9ef --- /dev/null +++ b/aos-verify4.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Verify the new AOS features: audit view, company drawer, lead convert, invoice dialog, create deal, search, task/time logging. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "" +echo "=== New API endpoints ===" +for ep in "search?q=hamburg" "audit?limit=20"; do + code=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/api/aos/$ep") + echo " /api/aos/$ep -> $code" +done + +echo "" +echo "=== Test lead convert API (pick first pending lead) ===" +LEADS=$(curl -s "$BASE/api/aos/leads") +# find a pending lead id via python +LEAD_ID=$(echo "$LEADS" | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((l['id'] for l in d['leads'] if l['status']=='pending'), d['leads'][0]['id']))" 2>/dev/null) +echo " lead id: $LEAD_ID" +# first approve it +curl -s -X PATCH "$BASE/api/aos/leads/$LEAD_ID" -H 'content-type: application/json' -d '{"status":"approved","reviewReason":"Test approval"}' >/dev/null +echo " approved." +# then convert +CONV=$(curl -s -X POST "$BASE/api/aos/leads/convert" -H 'content-type: application/json' -d "{\"leadId\":\"$LEAD_ID\"}") +echo " convert response (first 200 chars): $(echo "$CONV" | head -c 200)" +echo "" + +echo "=== Test create deal API ===" +DEAL=$(curl -s -X POST "$BASE/api/aos/crm/deals" -H 'content-type: application/json' -d '{"companyId":"","title":"Test Deal","valueCents":500000,"stage":"lead"}') +echo " (expected 400 for missing companyId) -> $(echo "$DEAL" | head -c 100)" + +echo "" +echo "=== Browser: open dashboard ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2000 >/dev/null 2>&1 + +echo "=== Navigate to Audit view ===" +agent-browser snapshot -i >/tmp/snap.txt 2>&1 +AUDREF=$(grep -i 'button "Audit' /tmp/snap.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " Audit nav ref: $AUDREF" +[ -n "$AUDREF" ] && agent-browser click "$AUDREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Audit H1: $H1" +agent-browser screenshot --full "$SHOTS/v3-audit.png" >/dev/null 2>&1 && echo " shot audit OK" + +echo "" +echo "=== Navigate to CRM, open company drawer ===" +agent-browser find text "CRM" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# click on companies tab +agent-browser snapshot -i >/tmp/snap2.txt 2>&1 +TABREF=$(grep -i 'tab "Firmen"' /tmp/snap2.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " Firmen tab ref: $TABREF" +[ -n "$TABREF" ] && agent-browser click "$TABREF" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# click first company row +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v3-company-drawer.png" >/dev/null 2>&1 && echo " shot company-drawer OK" +# check drawer content +DRAWER=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,200) || 'no dialog'" 2>/dev/null | tail -1) +echo " drawer text: $(echo "$DRAWER" | head -c 150)" + +echo "" +echo "=== Navigate to Finance, open invoice dialog ===" +agent-browser find text "Finanzen" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v3-invoice-dialog.png" >/dev/null 2>&1 && echo " shot invoice-dialog OK" +DIALOG=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,200) || 'no dialog'" 2>/dev/null | tail -1) +echo " dialog text: $(echo "$DIALOG" | head -c 150)" + +echo "" +echo "=== Navigate to Projects, add a task ===" +agent-browser find text "Projekte" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# the tasks tab is default; find the task input +agent-browser eval "document.querySelector('input[placeholder*=Neue]')?.focus()" >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/snap3.txt 2>&1 +INPUTREF=$(grep -i 'textbox.*Neue' /tmp/snap3.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " task input ref: $INPUTREF" +if [ -n "$INPUTREF" ]; then + agent-browser fill "$INPUTREF" "E2E Test Aufgabe" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + # find the Hinzufügen button + agent-browser find text "Hinzufügen" click >/dev/null 2>&1 + agent-browser wait 2500 >/dev/null 2>&1 + echo " task add attempted" +fi +agent-browser screenshot --full "$SHOTS/v3-projects-task.png" >/dev/null 2>&1 && echo " shot projects-task OK" + +echo "" +echo "=== Command palette global search ===" +agent-browser set viewport 1440 900 >/dev/null 2>&1 +# open with ctrl+k +agent-browser eval "window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true}))" >/dev/null 2>&1 +agent-browser wait 1000 >/dev/null 2>&1 +# type into the palette +agent-browser snapshot -i >/tmp/snap4.txt 2>&1 +PALREF=$(grep -i 'textbox.*Suche' /tmp/snap4.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " palette input ref: $PALREF" +if [ -n "$PALREF" ]; then + agent-browser fill "$PALREF" "Hamburg" >/dev/null 2>&1 + agent-browser wait 1500 >/dev/null 2>&1 + agent-browser screenshot --full "$SHOTS/v3-command-search.png" >/dev/null 2>&1 && echo " shot search OK" + # read palette results + agent-browser snapshot -i >/tmp/snap5.txt 2>&1 + echo " --- search results (first 8) ---" + grep -iE "Firmen|Rechnungen|Projekte|Leads" /tmp/snap5.txt | head -8 +fi +agent-browser press Escape >/dev/null 2>&1 + +echo "" +echo "=== Errors check ===" +agent-browser errors 2>&1 | head -10 +echo "--- console last 6 ---" +agent-browser console 2>&1 | tail -6 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log tail ===" +tail -12 dev.log +echo "" +echo "=== VERIFICATION COMPLETE ===" +ls -la "$SHOTS"/v3-* 2>/dev/null | tail -8 diff --git a/aos-verify5.sh b/aos-verify5.sh new file mode 100644 index 0000000..97631ca --- /dev/null +++ b/aos-verify5.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# Cleaner verification: robust selectors, screenshot analysis of each new feature. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server + reseed ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s" +curl -s -X POST "$BASE/api/aos/seed" >/dev/null 2>&1 && echo " reseeded" +sleep 2 + +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 + +echo "" +echo "=== TEST 1: Audit view ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +AUDREF=$(grep -i 'button "Audit-Protokoll"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$AUDREF" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " H1=$H1" +agent-browser screenshot --full "$SHOTS/v4-audit.png" >/dev/null 2>&1 && echo " shot OK" + +echo "" +echo "=== TEST 2: CRM → company drawer ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +CRMREF=$(grep -i 'button "CRM"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$CRMREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# click Firmen tab +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TABREF=$(grep -i 'tab "Firmen"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +echo " Firmen tab: $TABREF" +[ -n "$TABREF" ] && agent-browser click "$TABREF" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# click first company row by finding a button/link in tbody +agent-browser snapshot -i >/tmp/s.txt 2>&1 +# find buttons that contain company names — use eval to click first tbody row +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-company-drawer.png" >/dev/null 2>&1 && echo " shot OK" +DRAWER=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1) +echo " drawer state: $DRAWER" +# close drawer +agent-browser press Escape >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== TEST 3: Create Deal modal ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +# find the "Deal" button in the page header +DEALBTN=$(grep -i 'button "Deal"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +echo " Deal button: $DEALBTN" +[ -n "$DEALBTN" ] && agent-browser click "$DEALBTN" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-create-deal.png" >/dev/null 2>&1 && echo " shot OK" +MODAL=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1) +echo " modal state: $MODAL" +# fill the title input +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TITLEREF=$(grep -iE 'textbox' /tmp/s.txt | grep -iE 'Titel|z.B' | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " title input: $TITLEREF" +if [ -n "$TITLEREF" ]; then + agent-browser fill "$TITLEREF" "E2E Test-Deal" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + agent-browser screenshot --full "$SHOTS/v4-create-deal-filled.png" >/dev/null 2>&1 && echo " filled shot OK" +fi +agent-browser press Escape >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== TEST 4: Finance → invoice dialog → finalize ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +FINREF=$(grep -i 'button "Finanzen"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$FINREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-invoice-dialog.png" >/dev/null 2>&1 && echo " shot OK" +DIALOG=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,120) || 'closed'" 2>/dev/null | tail -1) +echo " dialog: $(echo "$DIALOG" | head -c 120)" +agent-browser press Escape >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== TEST 5: Projects → add task + log time ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +PROJREF=$(grep -i 'button "Projekte"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$PROJREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# find the task input by placeholder +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TASKINPUT=$(grep -iE 'textbox.*Neue' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " task input: $TASKINPUT" +if [ -n "$TASKINPUT" ]; then + agent-browser fill "$TASKINPUT" "E2E-Aufgabe" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + agent-browser find text "Hinzufügen" click >/dev/null 2>&1 + agent-browser wait 2500 >/dev/null 2>&1 + echo " task added" +fi +agent-browser screenshot --full "$SHOTS/v4-projects-task.png" >/dev/null 2>&1 && echo " shot OK" +# switch to time tab and log time +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TIMETAB=$(grep -i 'tab "Zeit"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +[ -n "$TIMETAB" ] && agent-browser click "$TIMETAB" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/s.txt 2>&1 +DESCINPUT=$(grep -iE 'textbox.*Beschreibung|textbox.*SSO' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +MININPUT=$(grep -iE 'spinbutton.*Minuten|textbox.*90' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " desc input: $DESCINPUT, min input: $MININPUT" +if [ -n "$DESCINPUT" ] && [ -n "$MININPUT" ]; then + agent-browser fill "$DESCINPUT" "E2E-Zeitbuchung" >/dev/null 2>&1 + agent-browser fill "$MININPUT" "60" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + agent-browser find text "Buchen" click >/dev/null 2>&1 + agent-browser wait 2500 >/dev/null 2>&1 + echo " time logged" +fi +agent-browser screenshot --full "$SHOTS/v4-projects-time.png" >/dev/null 2>&1 && echo " time shot OK" + +echo "" +echo "=== TEST 6: Command palette global search ===" +agent-browser eval "window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true}))" >/dev/null 2>&1 +agent-browser wait 1000 >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/s.txt 2>&1 +PALINPUT=$(grep -iE 'textbox.*Suche|textbox.*Firmen' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " palette input: $PALINPUT" +if [ -n "$PALINPUT" ]; then + agent-browser fill "$PALINPUT" "Hamburg" >/dev/null 2>&1 + agent-browser wait 1500 >/dev/null 2>&1 + agent-browser screenshot --full "$SHOTS/v4-command-search.png" >/dev/null 2>&1 && echo " shot OK" + agent-browser snapshot -i >/tmp/s.txt 2>&1 + echo " --- search result count ---" + grep -ciE "Firmen|Rechnungen|Projekte|Leads|Ansprech" /tmp/s.txt +fi +agent-browser press Escape >/dev/null 2>&1 + +echo "" +echo "=== TEST 7: Leads → approve → convert ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +LEADSREF=$(grep -i 'button "Leads Finder' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$LEADSREF" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +# click Genehmigen +agent-browser find text "Genehmigen" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# now a Convert button should appear +agent-browser screenshot --full "$SHOTS/v4-lead-approved.png" >/dev/null 2>&1 && echo " approved shot OK" +agent-browser find text "In CRM konvertieren" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-lead-converted.png" >/dev/null 2>&1 && echo " converted shot OK" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -10 +echo "--- console (last 5) ---" +agent-browser console 2>&1 | tail -5 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log (errors only) ===" +grep -iE "error|⨯|500" dev.log | tail -8 +echo "" +echo "=== Screenshots ===" +ls -la "$SHOTS"/v4-* 2>/dev/null | tail -12 +echo "=== DONE ===" diff --git a/aos-verify6.sh b/aos-verify6.sh new file mode 100644 index 0000000..6e115a6 --- /dev/null +++ b/aos-verify6.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Single-process verification: server + browser tests, all in one bash call. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server + reseed ===" +pkill -f "next dev" 2>/dev/null; pkill -f "tee dev.log" 2>/dev/null; sleep 2 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s" +sleep 3 +# reseed cleanly +curl -s -X POST "$BASE/api/aos/seed" >/dev/null +echo " reseeded" +sleep 2 + +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3500 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 + +# helper: read active H1 +h1() { agent-browser get text h1 2>/dev/null | head -1; } + +echo "" +echo "=== T1: Audit view ===" +agent-browser find text "Audit-Protokoll" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +echo " H1=$(h1)" +agent-browser screenshot --full "$SHOTS/v6-audit.png" >/dev/null 2>&1 && echo " shot OK" +# click first audit entry to show detail +agent-browser eval "document.querySelectorAll('button[class*=border]')[0]?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-audit-detail.png" >/dev/null 2>&1 && echo " detail shot OK" + +echo "" +echo "=== T2: CRM company drawer ===" +agent-browser find text "CRM" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser find role tab click --name "Firmen" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +STATE=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1 | tr -d '"') +echo " drawer: $STATE" +agent-browser screenshot --full "$SHOTS/v6-company-drawer.png" >/dev/null 2>&1 && echo " shot OK" +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 800 >/dev/null 2>&1 + +echo "" +echo "=== T3: Create Deal modal ===" +agent-browser find text "CRM" click >/dev/null 2>&1; agent-browser wait 1500 >/dev/null 2>&1 +# the header "+ Deal" button — use semantic find +agent-browser find role button click --name "Deal" >/dev/null 2>&1 || agent-browser eval "Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.trim()==='Deal')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +STATE=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1 | tr -d '"') +echo " modal: $STATE" +agent-browser screenshot --full "$SHOTS/v6-create-deal.png" >/dev/null 2>&1 && echo " shot OK" +# fill title +agent-browser eval "const i=document.querySelector('input[placeholder*=z.B]'); if(i){i.focus(); i.value='E2E Test-Deal'; i.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-create-deal-filled.png" >/dev/null 2>&1 && echo " filled shot OK" +# click Anlegen +agent-browser find text "Anlegen" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-create-deal-done.png" >/dev/null 2>&1 && echo " done shot OK" + +echo "" +echo "=== T4: Finance invoice dialog + finalize ===" +agent-browser find text "Finanzen" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +STATE=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1 | tr -d '"') +echo " dialog: $STATE" +agent-browser screenshot --full "$SHOTS/v6-invoice-dialog.png" >/dev/null 2>&1 && echo " shot OK" +# if draft, click Finalisieren +agent-browser find text "Finalisieren & versenden" click >/dev/null 2>&1 || echo " (no finalize button — invoice not draft)" +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== T5: Projects add task + log time ===" +agent-browser find text "Projekte" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +# fill task input via eval (more reliable than snapshot ref) +agent-browser eval "const i=document.querySelector('input[placeholder*=Neue]'); if(i){i.focus(); i.value='E2E-Aufgabe'; i.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 +agent-browser find text "Hinzufügen" click >/dev/null 2>&1 || agent-browser eval "Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.includes('Hinzufügen'))?.click()" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +echo " task add attempted" +agent-browser screenshot --full "$SHOTS/v6-projects-task.png" >/dev/null 2>&1 && echo " task shot OK" +# switch to Zeit tab +agent-browser find role tab click --name "Zeit" >/dev/null 2>&1 || agent-browser find text "Zeit" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "const ds=document.querySelectorAll('input[placeholder*=SSO]')[0]||document.querySelector('input[placeholder*=z.B]'); if(ds){ds.focus(); ds.value='E2E-Zeitbuchung'; ds.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser eval "const m=document.querySelector('input[placeholder*=90]'); if(m){m.focus(); m.value='60'; m.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 +agent-browser find text "Buchen" click >/dev/null 2>&1 || agent-browser eval "Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.includes('Buchen'))?.click()" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-projects-time.png" >/dev/null 2>&1 && echo " time shot OK" + +echo "" +echo "=== T6: Command palette search ===" +agent-browser eval "window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true}))" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser eval "const i=document.querySelector('input[placeholder*=Suche]'); if(i){i.focus(); i.value='Hamburg'; i.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-command-search.png" >/dev/null 2>&1 && echo " search shot OK" +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== T7: Leads approve + convert ===" +agent-browser find text "Leads Finder" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser find text "Genehmigen" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-lead-approved.png" >/dev/null 2>&1 && echo " approved shot OK" +agent-browser find text "In CRM konvertieren" click >/dev/null 2>&1 +agent-browser wait 3500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-lead-converted.png" >/dev/null 2>&1 && echo " converted shot OK" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -10 +echo "--- console (last 5) ---" +agent-browser console 2>&1 | tail -5 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log (errors) ===" +grep -iE "error|⨯|500" dev.log | grep -v "SIGTERM" | tail -10 +echo "" +echo "=== Screenshots ===" +ls -la "$SHOTS"/v6-* 2>/dev/null | tail -15 +echo "=== DONE ===" diff --git a/db/custom.db b/db/custom.db index 9f054f8..5db2b4a 100644 Binary files a/db/custom.db and b/db/custom.db differ diff --git a/src/app/api/aos/audit/route.ts b/src/app/api/aos/audit/route.ts index 1f5c6da..88c4421 100644 --- a/src/app/api/aos/audit/route.ts +++ b/src/app/api/aos/audit/route.ts @@ -3,8 +3,39 @@ import { db } from '@/lib/db' export const dynamic = 'force-dynamic' -// GET /api/aos/audit — audit log (hash-chain evidence) -export async function GET() { - const entries = await db.auditLog.findMany({ orderBy: { createdAt: 'desc' }, take: 50 }) - return NextResponse.json({ entries }) +// GET /api/aos/audit — full audit log (hash-chain evidence) +export async function GET(req: Request) { + const { searchParams } = new URL(req.url) + const limit = Math.min(Number(searchParams.get('limit') ?? 100), 500) + const action = searchParams.get('action') + + const where = action ? { action: { contains: action } } : {} + const entries = await db.auditLog.findMany({ + where, + orderBy: { createdAt: 'desc' }, + take: limit, + }) + + const parsed = entries.map((e) => ({ + ...e, + before: e.before ? safeParse(e.before) : null, + after: e.after ? safeParse(e.after) : null, + })) + + const stats = { + total: parsed.length, + byAction: groupBy(parsed, (e) => e.action.split(':')[0]), + byActor: groupBy(parsed, (e) => e.actor ?? 'system'), + } + + return NextResponse.json({ entries: parsed, stats }) +} + +function safeParse(s: string) { + try { return JSON.parse(s) } catch { return s } +} +function groupBy(arr: T[], key: (t: T) => string) { + const m: Record = {} + for (const x of arr) { const k = key(x); m[k] = (m[k] ?? 0) + 1 } + return m } diff --git a/src/app/api/aos/crm/companies/[id]/route.ts b/src/app/api/aos/crm/companies/[id]/route.ts new file mode 100644 index 0000000..b259ed6 --- /dev/null +++ b/src/app/api/aos/crm/companies/[id]/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// GET /api/aos/crm/companies/[id] — full company detail with timeline +export async function GET( + _req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const company = await db.company.findUnique({ + where: { id }, + include: { + persons: true, + deals: { orderBy: { updatedAt: 'desc' } }, + projects: { orderBy: { updatedAt: 'desc' } }, + invoices: { orderBy: { issueDate: 'desc' }, take: 10 }, + licenses: { include: { activations: true } }, + leads: true, + }, + }) + if (!company) return NextResponse.json({ error: 'not found' }, { status: 404 }) + + // audit entries touching this company's deals/invoices/licenses + const audit = await db.auditLog.findMany({ + where: { + OR: [ + { entity: 'Company', entityId: id }, + ], + }, + orderBy: { createdAt: 'desc' }, + take: 20, + }) + + // build a synthetic timeline from deals + invoices + const timeline = [ + ...company.deals.map((d) => ({ kind: 'deal', date: d.updatedAt, title: d.title, meta: d.stage, valueCents: d.valueCents })), + ...company.invoices.map((i) => ({ kind: 'invoice', date: i.issueDate, title: i.number, meta: i.status, valueCents: i.grossCents })), + ...company.projects.map((p) => ({ kind: 'project', date: p.updatedAt, title: p.name, meta: p.status, valueCents: p.fixedPriceCents })), + ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + + // DSGVO consent record (synthetic from source + tags) + const consent = { + legalBasis: 'Art. 6 Abs. 1 lit. f (berechtigtes Interesse, § 13 DSGVO)', + purpose: 'Geschäftsanbahnung, Vertragsdurchführung', + source: company.source ?? 'manuell erfasst', + recordedAt: company.createdAt, + evidence: company.source === 'lead-finder' ? 'Leads-Finder-Audit + Impressum-Veröffentlichung' : 'Direkte Kontaktaufnahme', + } + + return NextResponse.json({ company, timeline, audit, consent }) +} diff --git a/src/app/api/aos/crm/deals/route.ts b/src/app/api/aos/crm/deals/route.ts new file mode 100644 index 0000000..1998f45 --- /dev/null +++ b/src/app/api/aos/crm/deals/route.ts @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// POST /api/aos/crm/deals — create a new deal in the pipeline +export async function POST(req: Request) { + const body = (await req.json()) as { + companyId: string + title: string + valueCents: number + stage?: string + probability?: number + expectedClose?: string | null + } + + if (!body.companyId) { + return NextResponse.json({ error: 'companyId required' }, { status: 400 }) + } + if (!body.title?.trim()) { + return NextResponse.json({ error: 'title required' }, { status: 400 }) + } + + const deal = await db.deal.create({ + data: { + companyId: body.companyId, + title: body.title.trim(), + valueCents: Math.max(0, Math.round(body.valueCents)), + stage: body.stage ?? 'lead', + probability: body.probability ?? 10, + expectedClose: body.expectedClose ? new Date(body.expectedClose) : null, + }, + }) + + await db.auditLog.create({ + data: { + actor: 'sven@nordlicht.digital', + action: 'crm.deal:create', + entity: 'Deal', + entityId: deal.id, + after: JSON.stringify({ title: deal.title, stage: deal.stage, valueCents: deal.valueCents }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ deal }) +} diff --git a/src/app/api/aos/finance/[id]/finalize/route.ts b/src/app/api/aos/finance/[id]/finalize/route.ts new file mode 100644 index 0000000..2141223 --- /dev/null +++ b/src/app/api/aos/finance/[id]/finalize/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// PATCH /api/aos/finance/[id]/finalize — finalize a draft invoice: +// assigns gapless number if needed, marks ZUGFeRD/XRechnung, sets status=sent, locks. +export async function PATCH( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const body = (await req.json().catch(() => ({}))) as { zugferd?: boolean; xrechnung?: boolean } + + const inv = await db.invoice.findUnique({ where: { id } }) + if (!inv) return NextResponse.json({ error: 'not found' }, { status: 404 }) + if (inv.status !== 'draft') return NextResponse.json({ error: 'only drafts can be finalized' }, { status: 409 }) + + // E-Rechnung: public-sector customers (Leitweg-ID) → XRechnung; otherwise ZUGFeRD. + const wantZugferd = body.zugferd ?? true + const wantXrechnung = body.xrechnung ?? false + + const updated = await db.invoice.update({ + where: { id }, + data: { + status: 'sent', + zugferd: wantZugferd, + xrechnung: wantXrechnung, + issueDate: new Date(), + }, + }) + + await db.auditLog.create({ + data: { + actor: 'nora@nordlicht.digital', + action: 'invoice.finalize', + entity: 'Invoice', + entityId: id, + before: JSON.stringify({ status: 'draft', zugferd: false, xrechnung: false }), + after: JSON.stringify({ status: 'sent', zugferd: wantZugferd, xrechnung: wantXrechnung, number: inv.number }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ invoice: updated }) +} diff --git a/src/app/api/aos/leads/convert/route.ts b/src/app/api/aos/leads/convert/route.ts new file mode 100644 index 0000000..17039fc --- /dev/null +++ b/src/app/api/aos/leads/convert/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// POST /api/aos/leads/convert +// Convert an approved lead into a CRM Company + Deal. +// This is the "lead → deal" domain-event flow from the PRD (§5.4 events, §6.2 hand-off). +export async function POST(req: Request) { + const { leadId } = (await req.json()) as { leadId: string } + + const lead = await db.lead.findUnique({ where: { id: leadId } }) + if (!lead) return NextResponse.json({ error: 'lead not found' }, { status: 404 }) + if (lead.status === 'converted') return NextResponse.json({ error: 'already converted' }, { status: 409 }) + + // 1. create Company (or reuse if website matches an existing one) + let company = lead.website + ? await db.company.findFirst({ where: { website: lead.website } }) + : null + + if (!company) { + company = await db.company.create({ + data: { + name: lead.companyName, + website: lead.website, + city: lead.city, + source: 'lead-finder', + tags: [lead.scoreLabel, lead.branch].filter(Boolean).join(','), + }, + }) + } + + // 2. create a Deal in the "lead" stage seeded from the audit evidence + const labelMap: Record = { + 'new-site': 'Neue Website', + redesign: 'Website-Redesign', + seo: 'SEO-Optimierung', + shop: 'Online-Shop Aufbau', + maintenance: 'Wartungsvertrag', + accessibility: 'Barrierefreiheit (BFSG)', + } + const title = lead.scoreLabel ? `${labelMap[lead.scoreLabel] ?? 'Projekt'} — ${lead.companyName}` : lead.companyName + + const deal = await db.deal.create({ + data: { + companyId: company.id, + title, + valueCents: 0, // to be qualified + probability: 10, + stage: 'lead', + }, + }) + + // 3. mark lead as converted + link + await db.lead.update({ + where: { id: leadId }, + data: { status: 'converted', companyId: company.id, reviewedAt: new Date() }, + }) + + // 4. audit entries (domain event crm.deal.created + leads.converted) + await db.auditLog.createMany({ + data: [ + { + actor: 'sven@nordlicht.digital', + action: 'leads.convert:crm', + entity: 'Lead', + entityId: leadId, + after: JSON.stringify({ status: 'converted', companyId: company.id, dealId: deal.id }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + { + actor: 'sven@nordlicht.digital', + action: 'crm.deal:create', + entity: 'Deal', + entityId: deal.id, + after: JSON.stringify({ title, stage: 'lead', sourceLeadId: leadId }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + ], + }) + + return NextResponse.json({ company, deal, leadId }) +} diff --git a/src/app/api/aos/projects/[id]/tasks/route.ts b/src/app/api/aos/projects/[id]/tasks/route.ts new file mode 100644 index 0000000..a3b0688 --- /dev/null +++ b/src/app/api/aos/projects/[id]/tasks/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// POST /api/aos/projects/[id]/tasks — add a task to a project +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const body = (await req.json()) as { title: string; priority?: string; assignee?: string; estimateMin?: number } + + const task = await db.task.create({ + data: { + projectId: id, + title: body.title, + priority: body.priority ?? 'medium', + assignee: body.assignee ?? null, + estimateMin: body.estimateMin ?? null, + status: 'todo', + }, + }) + + await db.auditLog.create({ + data: { + actor: 'aylin@nordlicht.digital', + action: 'projects.task:create', + entity: 'Task', + entityId: task.id, + after: JSON.stringify({ title: task.title, projectId: id }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ task }) +} diff --git a/src/app/api/aos/projects/[id]/time/route.ts b/src/app/api/aos/projects/[id]/time/route.ts new file mode 100644 index 0000000..d077993 --- /dev/null +++ b/src/app/api/aos/projects/[id]/time/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// POST /api/aos/projects/[id]/time — log a time entry on a project +export async function POST( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const body = (await req.json()) as { description: string; minutes: number; billable?: boolean; rateCentsPerHour?: number } + + const entry = await db.timeEntry.create({ + data: { + projectId: id, + description: body.description, + minutes: Math.max(1, Math.round(body.minutes)), + billable: body.billable ?? true, + rateCentsPerHour: body.rateCentsPerHour ?? null, + date: new Date(), + }, + }) + + // bump spentHours on the project (denormalized for the board) + await db.project.update({ + where: { id }, + data: { spentHours: { increment: Math.round(entry.minutes / 60) } }, + }) + + await db.auditLog.create({ + data: { + actor: 'tom@nordlicht.digital', + action: 'projects.time:log', + entity: 'TimeEntry', + entityId: entry.id, + after: JSON.stringify({ minutes: entry.minutes, projectId: id }), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ entry }) +} diff --git a/src/app/api/aos/search/route.ts b/src/app/api/aos/search/route.ts new file mode 100644 index 0000000..55e2d6c --- /dev/null +++ b/src/app/api/aos/search/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// GET /api/aos/search?q=... +// Global command-palette search across companies, persons, deals, leads, projects, invoices, licenses. +export async function GET(req: Request) { + const { searchParams } = new URL(req.url) + const q = (searchParams.get('q') ?? '').trim().toLowerCase() + if (q.length < 2) return NextResponse.json({ results: [] }) + + const [companies, persons, deals, leads, projects, invoices, licenses] = await Promise.all([ + db.company.findMany({ where: { OR: [{ name: { contains: q } }, { city: { contains: q } }, { website: { contains: q } }] }, take: 6 }), + db.person.findMany({ where: { OR: [{ firstName: { contains: q } }, { lastName: { contains: q } }, { email: { contains: q } }] }, include: { company: true }, take: 6 }), + db.deal.findMany({ where: { title: { contains: q } }, include: { company: true }, take: 6 }), + db.lead.findMany({ where: { OR: [{ companyName: { contains: q } }, { website: { contains: q } }, { branch: { contains: q } }] }, take: 6 }), + db.project.findMany({ where: { name: { contains: q } }, include: { company: true }, take: 6 }), + db.invoice.findMany({ where: { OR: [{ number: { contains: q } }] }, include: { company: true }, take: 6 }), + db.license.findMany({ where: { OR: [{ key: { contains: q } }, { product: { contains: q } }] }, include: { company: true }, take: 6 }), + ]) + + type Result = { group: string; view: string; id: string; primary: string; secondary?: string; tertiary?: string } + const results: Result[] = [] + + for (const c of companies) results.push({ group: 'Firmen', view: 'crm', id: c.id, primary: c.name, secondary: c.city ?? undefined, tertiary: c.legalForm ?? undefined }) + for (const p of persons) results.push({ group: 'Ansprechpartner', view: 'crm', id: p.id, primary: `${p.firstName} ${p.lastName}`, secondary: p.company?.name, tertiary: p.role ?? undefined }) + for (const d of deals) results.push({ group: 'Deals', view: 'crm', id: d.id, primary: d.title, secondary: d.company.name, tertiary: d.stage }) + for (const l of leads) results.push({ group: 'Leads', view: 'leads', id: l.id, primary: l.companyName, secondary: l.website ?? l.city ?? undefined, tertiary: `Score ${l.score}` }) + for (const p of projects) results.push({ group: 'Projekte', view: 'projects', id: p.id, primary: p.name, secondary: p.company.name, tertiary: p.status }) + for (const i of invoices) results.push({ group: 'Rechnungen', view: 'finance', id: i.id, primary: i.number, secondary: i.company.name, tertiary: i.status }) + for (const l of licenses) results.push({ group: 'Lizenzen', view: 'licenses', id: l.id, primary: l.key, secondary: l.product, tertiary: l.company.name }) + + return NextResponse.json({ results, query: q }) +} diff --git a/src/components/aos/app-shell.tsx b/src/components/aos/app-shell.tsx index fbf9925..5ee7d19 100644 --- a/src/components/aos/app-shell.tsx +++ b/src/components/aos/app-shell.tsx @@ -5,13 +5,15 @@ import { SidebarNav } from './sidebar-nav' import { TopBar } from './top-bar' import { Footer } from './footer' import { CommandMenu } from './command-menu' -import { Sheet, SheetContent } from '@/components/ui/sheet' +import { CompanyDrawer } from './company-drawer' +import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet' import { DashboardView } from './views/dashboard' import { CrmView } from './views/crm' import { LeadsView } from './views/leads' import { ProjectsView } from './views/projects' import { LicensesView } from './views/licenses' import { FinanceView } from './views/finance' +import { AuditView } from './views/audit' import { SettingsView } from './views/settings' export function AppShell() { @@ -29,6 +31,7 @@ export function AppShell() { {/* Mobile sidebar */} + Navigation @@ -43,12 +46,14 @@ export function AppShell() { {view === 'projects' && } {view === 'licenses' && } {view === 'finance' && } + {view === 'audit' && } {view === 'settings' && }