Initial ordrestyring skill - GraphQL client for Parlobyg
This commit is contained in:
commit
aa64688058
4 changed files with 439 additions and 0 deletions
61
SKILL.md
Normal file
61
SKILL.md
Normal file
|
|
@ -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)
|
||||||
117
references/graphql-api.md
Normal file
117
references/graphql-api.md
Normal file
|
|
@ -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)
|
||||||
21
references/web-access.md
Normal file
21
references/web-access.md
Normal file
|
|
@ -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.
|
||||||
240
scripts/graphql_query.py
Executable file
240
scripts/graphql_query.py
Executable file
|
|
@ -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()
|
||||||
Loading…
Reference in a new issue