commit aa64688058bf3bd93fc2282c2090e561d273bf9c Author: Parlomachine Date: Mon Mar 23 19:33:24 2026 +0000 Initial ordrestyring skill - GraphQL client for Parlobyg diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..11f21e4 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,61 @@ +--- +name: ordrestyring +description: Access Ordrestyring job management system for Parlobyg. Use for cases, customers, quotes, time registration, invoicing, materials. Triggers on anything Ordrestyring-related. +--- + +# Ordrestyring Skill + +GraphQL API client for Ordrestyring โ€” the core job management system for Parlobyg (Danish construction company). + +## Quick Start + +```bash +# Query cases +python3 scripts/graphql_query.py cases --limit 10 + +# Query customers +python3 scripts/graphql_query.py customers --limit 10 + +# Query specific case +python3 scripts/graphql_query.py case --id 12345 + +# Query hours/time entries +python3 scripts/graphql_query.py hours --limit 50 + +# Query offers/quotes +python3 scripts/graphql_query.py offers --limit 10 +``` + +## Credentials + +- **Agreement ID:** 21528 +- **API Key:** `q3mjVDR6wAdPCvZT` +- **GraphQL Endpoint:** `https://graphql.ordrestyring.dk/graphql` +- **GraphiQL (interactive):** `https://graphql.ordrestyring.dk/graphiql` + +## Capabilities + +**Via GraphQL API:** +- ๐Ÿ“‹ **Cases (Sager)** โ€” search, status, economy +- ๐Ÿ‘ฅ **Customers (Kunder)** โ€” contact info, addresses, statements +- ๐Ÿงพ **Quotes & Invoices** โ€” quote lines, sales invoices +- โฑ๏ธ **Hours (Timer)** โ€” registered hours per case, worker, period +- ๐Ÿ”ง **Materials** โ€” material usage per case +- ๐Ÿ“Š **Finance** โ€” financePosts, aggregations + +**Pagination:** Max 200 per page + +**Safety:** Mutations are coded but gated. Read-only by default. + +## Web Login (fallback) + +For features not in API (e.g., Gantt views): +- URL: https://ordrestyring.dk +- Aftalenummer: 21528 +- Brugernavn: Mikkel +- (Password: ask Mikkel) + +## References + +- [GraphQL API Reference](references/graphql-api.md) +- [Web Access Notes](references/web-access.md) diff --git a/references/graphql-api.md b/references/graphql-api.md new file mode 100644 index 0000000..80f8fd3 --- /dev/null +++ b/references/graphql-api.md @@ -0,0 +1,117 @@ +# Ordrestyring GraphQL API Reference + +## Endpoint + +- **GraphQL:** `https://graphql.ordrestyring.dk/graphql` +- **GraphiQL (interactive):** `https://graphql.ordrestyring.dk/graphiql` + +## Authentication + +Headers required: +``` +Authorization: Bearer q3mjVDR6wAdPCvZT +X-Agreement-Id: 21528 +Content-Type: application/json +``` + +## Pagination + +Max 200 results per page. Use `first` and `offset` parameters. + +## Main Queries + +### Cases (Sager) +```graphql +query { + cases(first: 10, offset: 0) { + totalCount + nodes { + id + caseNumber + title + status + customer { id name } + createdAt + } + } +} +``` + +### Customers (Kunder) +```graphql +query { + customers(first: 10) { + totalCount + nodes { + id + number + name + email + phone + address { street city zip } + } + } +} +``` + +### Time Entries (Timer) +```graphql +query { + timeEntries(first: 50) { + totalCount + nodes { + id + date + hours + description + case { id caseNumber } + worker { id name } + } + } +} +``` + +### Offers (Tilbud) +```graphql +query { + offers(first: 10) { + totalCount + nodes { + id + offerNumber + title + status + totalAmount + customer { id name } + } + } +} +``` + +### Workers (Medarbejdere) +```graphql +query { + workers(first: 50) { + totalCount + nodes { + id + name + email + phone + } + } +} +``` + +## Live Data Stats (March 2026) + +- **Cases:** ~771 +- **Customers:** ~1,038 +- **Offers:** ~1,103 +- **Time Entries:** ~7,517 + +## Notes + +- Platform used by 30,000+ Danish craftsmen +- Features: cases, quotes, time registration, invoicing, materials inbox +- Missing from API: smart scheduling, AI email drafting (use web for these) diff --git a/references/web-access.md b/references/web-access.md new file mode 100644 index 0000000..b68fa59 --- /dev/null +++ b/references/web-access.md @@ -0,0 +1,21 @@ +# Ordrestyring Web Access + +For features not available via GraphQL API. + +## Login + +- **URL:** https://ordrestyring.dk +- **Aftalenummer:** 21528 +- **Brugernavn:** Mikkel +- **Adgangskode:** (ask Mikkel) + +## Features Only on Web + +- Gantt chart views +- Advanced scheduling +- Some report exports +- Indbakke (materials inbox) management + +## Browser Automation + +Use the browser tool with profile="clawd" to automate web tasks when needed. diff --git a/scripts/graphql_query.py b/scripts/graphql_query.py new file mode 100755 index 0000000..bb439e8 --- /dev/null +++ b/scripts/graphql_query.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +Ordrestyring GraphQL Client for Parlobyg + +Usage: + python3 graphql_query.py cases --limit 10 + python3 graphql_query.py customers --limit 10 + python3 graphql_query.py case --id 12345 + python3 graphql_query.py hours --limit 50 + python3 graphql_query.py offers --limit 10 + python3 graphql_query.py raw "{ cases(pagination: {limit: 5}) { items { id caseNumber } } }" +""" + +import argparse +import json +import sys +from urllib.request import Request, urlopen +from urllib.error import HTTPError, URLError + +# Configuration +ENDPOINT = "https://graphql.ordrestyring.dk/graphql" +API_KEY = "q3mjVDR6wAdPCvZT" +AGREEMENT_ID = "21528" + +def graphql_request(query: str, variables: dict = None) -> dict: + """Execute a GraphQL query and return the result.""" + payload = {"query": query} + if variables: + payload["variables"] = variables + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {API_KEY}", + "X-Agreement-Id": AGREEMENT_ID, + } + + data = json.dumps(payload).encode("utf-8") + req = Request(ENDPOINT, data=data, headers=headers, method="POST") + + try: + with urlopen(req, timeout=30) as response: + return json.loads(response.read().decode("utf-8")) + except HTTPError as e: + return {"errors": [{"message": f"HTTP {e.code}: {e.reason}"}]} + except URLError as e: + return {"errors": [{"message": f"Connection error: {e.reason}"}]} + +def query_cases(limit: int = 10, page: int = 1): + """Query cases/sager.""" + query = """ + query GetCases($pagination: Pagination!) { + cases(pagination: $pagination) { + total + count + currentPage + hasMorePages + items { + id + caseNumber + description + createdAt + customer { + id + name + } + caseType { + id + text + } + } + } + } + """ + return graphql_request(query, {"pagination": {"limit": limit, "page": page}}) + +def query_case(case_id: str): + """Query a specific case by ID.""" + query = """ + query GetCase($id: ID!) { + case(id: $id) { + id + caseNumber + description + createdAt + customer { + id + name + email + phoneNumber + } + workAddress { + address + postalCode + city + } + caseType { + id + text + } + } + } + """ + return graphql_request(query, {"id": case_id}) + +def query_customers(limit: int = 10, page: int = 1): + """Query customers/kunder.""" + query = """ + query GetCustomers($pagination: Pagination!) { + customers(pagination: $pagination) { + total + count + currentPage + hasMorePages + items { + id + number + name + email + phoneNumber + mobilePhoneNumber + fullAddress + city + postalCode + } + } + } + """ + return graphql_request(query, {"pagination": {"limit": limit, "page": page}}) + +def query_hours(limit: int = 50, page: int = 1): + """Query time entries/timer.""" + query = """ + query GetHours($pagination: Pagination!) { + hourRegistrations(pagination: $pagination) { + total + count + items { + id + date + hours + description + case { + id + caseNumber + } + worker { + id + name + } + } + } + } + """ + return graphql_request(query, {"pagination": {"limit": limit, "page": page}}) + +def query_offers(limit: int = 10, page: int = 1): + """Query offers/quotes.""" + query = """ + query GetOffers($pagination: Pagination!) { + offers(pagination: $pagination) { + total + count + items { + id + offerNumber + description + totalPrice + customer { + id + name + } + case { + id + caseNumber + } + createdAt + } + } + } + """ + return graphql_request(query, {"pagination": {"limit": limit, "page": page}}) + +def query_raw(query_string: str): + """Execute a raw GraphQL query.""" + return graphql_request(query_string) + +def main(): + parser = argparse.ArgumentParser(description="Ordrestyring GraphQL Client") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Cases + cases_parser = subparsers.add_parser("cases", help="Query cases") + cases_parser.add_argument("--limit", type=int, default=10) + cases_parser.add_argument("--page", type=int, default=1) + + # Single case + case_parser = subparsers.add_parser("case", help="Query a specific case") + case_parser.add_argument("--id", required=True, help="Case ID") + + # Customers + customers_parser = subparsers.add_parser("customers", help="Query customers") + customers_parser.add_argument("--limit", type=int, default=10) + customers_parser.add_argument("--page", type=int, default=1) + + # Hours + hours_parser = subparsers.add_parser("hours", help="Query time entries") + hours_parser.add_argument("--limit", type=int, default=50) + hours_parser.add_argument("--page", type=int, default=1) + + # Offers + offers_parser = subparsers.add_parser("offers", help="Query offers/quotes") + offers_parser.add_argument("--limit", type=int, default=10) + offers_parser.add_argument("--page", type=int, default=1) + + # Raw query + raw_parser = subparsers.add_parser("raw", help="Execute raw GraphQL query") + raw_parser.add_argument("query", help="GraphQL query string") + + args = parser.parse_args() + + if args.command == "cases": + result = query_cases(args.limit, args.page) + elif args.command == "case": + result = query_case(args.id) + elif args.command == "customers": + result = query_customers(args.limit, args.page) + elif args.command == "hours": + result = query_hours(args.limit, args.page) + elif args.command == "offers": + result = query_offers(args.limit, args.page) + elif args.command == "raw": + result = query_raw(args.query) + else: + parser.print_help() + sys.exit(1) + + print(json.dumps(result, indent=2, ensure_ascii=False)) + +if __name__ == "__main__": + main()