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