diff --git a/AGENTS.md b/AGENTS.md
index ca6dc62..7fb1f17 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,8 +4,9 @@ Instructions for AI coding agents and developers.
## What this is
-The **frontend** SDK for the MGPL Diagnostics backend (`createMgplApi()` from invoice-kit, or
-`createReportApi()` from report-kit). It is **headless and has zero dependencies**: a typed `fetch` client,
+The **frontend** SDK for the MGPL Diagnostics backend: the GraphQL API of **mgpl-diagnostics/api** (one
+endpoint, `POST /graphql`, for everything) and the HTTP API of `createMgplApi()` (invoice-kit) /
+`createReportApi()` (report-kit). It is **headless and has zero dependencies**: a typed `fetch` client,
browser helpers, and optional **unstyled** React hooks. All rendering (reports, invoices, PDFs) happens on
the backend. Apps build their own UI/UX on top.
@@ -17,8 +18,12 @@ the backend. Apps build their own UI/UX on top.
3. **The contract is mirrored, not imported.** `src/types.ts` mirrors report-kit's `src/shared/api.ts` /
`types.ts` and invoice-kit's `api.ts`. Never import from the backend kits (that would pull the PDF
engine into browsers). report-studio's `contract-check.ts` type-checks both sides; update them together.
-4. **Routes** are in `ROUTES` in `src/index.ts` and must match report-kit `src/shared/api.ts` + invoice-kit `INVOICE_ROUTES`.
-5. `src/react.tsx` gets a `"use client"` banner from tsup (see `tsup.config.ts`); the main entry must not,
+4. **GraphQL operations are plain strings** in `src/graphql/operations.ts` (no GraphQL library). Their
+ selections match `src/graphql/types.ts`. mgpl-diagnostics/api validates every operation against its
+ schema and checks that every root field has one; when the API adds or renames a field, update
+ `operations.ts`, `types.ts` and `client.ts` here, release, then bump the client ref in api's `kits.json`.
+5. **Routes** are in `ROUTES` in `src/index.ts` and must match report-kit `src/shared/api.ts` + invoice-kit `INVOICE_ROUTES`.
+6. `src/react.tsx` gets a `"use client"` banner from tsup (see `tsup.config.ts`); the main entry must not,
so it stays usable from Server Components, workers and Node scripts.
## Map
@@ -26,6 +31,12 @@ the backend. Apps build their own UI/UX on top.
```
src/types.ts contract types (Patient, ReportRequest, TemplateCatalog, InvoiceInput, InvoiceTotals, …)
src/index.ts createMgplClient (reports.*, invoices.*, health, url), MgplApiError, ROUTES, downloadBlob/openBlob/printBlob
+src/graphql/
+ transport.ts createTransport (POST, Bearer token, onToken), MgplGraphQLError (code from extensions.code)
+ operations.ts FRAGMENTS + OPERATIONS (one per Query/Mutation field), withFragments
+ client.ts createMgplGraphQLClient (auth, users, organization, catalog, patients, … documents), documentBlob, csvBlob
+ types.ts the API's GraphQL types as selected by operations.ts, plus inputs
+ index.ts the "/graphql" entry (types flat); the main entry re-exports the client, types under `Gql`
src/react.tsx useAsync, useTemplates, useReportHtml, useInvoiceHtml, useInvoiceTotals, usePdfUrl, HtmlFrame
```
diff --git a/README.md b/README.md
index 004b81b..81fb419 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Your UI, your design system, your UX. The backend renders the reports and invoic
[](#react-optional)
[](LICENSE)
-[Quick start](#quick-start) · [SDK](#the-sdk) · [React](#react-optional) · [Errors & auth](#errors-and-auth) · [Docs](docs/api.md)
+[Quick start](#quick-start) · [GraphQL](#graphql-the-whole-backend) · [SDK](#the-sdk) · [React](#react-optional) · [Errors & auth](#errors-and-auth) · [Docs](docs/api.md)
@@ -59,7 +59,59 @@ const pdf = await mgpl.reports.pdf({
downloadBlob(pdf, "MGPL1025.pdf");
```
-## The SDK
+## GraphQL: the whole backend
+
+[mgpl-diagnostics/api](https://github.com/mgpl-diagnostics/api) serves everything (sign-in, patients,
+bookings, samples, results, billing, books, GST, receivables, settlements, cash and bank, documents) at
+**one endpoint, `POST /graphql`**. `createMgplGraphQLClient` has a typed method for each of its 138
+operations, with no GraphQL library: the operations are plain strings, validated against the server's
+schema by the api's contract test.
+
+```mermaid
+flowchart LR
+ UI["Your UI"] --> G["createMgplGraphQLClient()"]
+ G -- "POST /graphql
Authorization: Bearer token" --> API["mgpl-diagnostics/api"]
+ API --> K["auth · db · catalog · report · invoice · tally · finance kits"]
+```
+
+```ts
+import { createMgplGraphQLClient, documentBlob, openBlob, MgplGraphQLError } from "@mgpl-diagnostics/client";
+
+const mgpl = createMgplGraphQLClient({
+ url: "https://api.your-lab.in/graphql",
+ token: sessionStorage.getItem("mgpl-token"), // optional starting token
+ onToken: (t) => (t ? sessionStorage.setItem("mgpl-token", t) : sessionStorage.removeItem("mgpl-token")),
+});
+
+await mgpl.auth.login({ email: "desk@your-lab.in", password }); // keeps the token
+const [hit] = await mgpl.catalog.search("cbc");
+const patient = await mgpl.patients.register({ branchId, name: "Kavita Sharma", sex: "female", ageYears: 32 });
+const { booking } = await mgpl.bookings.create({ branchId, patientId: patient.id, tests: [{ testCode: hit!.test.code! }] });
+const invoice = await mgpl.invoices.issue({
+ branchId, patientId: patient.id, bookingId: booking.id,
+ items: [{ code: "CBC" }], // name and MRP from the catalog
+ payments: [{ amountPaise: 30000, mode: "upi" }],
+});
+openBlob(documentBlob(await mgpl.documents.invoice(invoice.id))); // the PDF arrives as base64
+
+try {
+ await mgpl.periods.setLockDate({ upTo: "2026-09-30" });
+} catch (e) {
+ if (e instanceof MgplGraphQLError && e.code === "FORBIDDEN") alert("Ask an accountant");
+}
+```
+
+Namespaces: `auth`, `users`, `organization`, `catalog`, `patients`, `doctors`, `bookings`, `samples`,
+`reports`, `invoices`, `books`, `gst`, `receivables`, `settlements`, `cash`, `bank`, `periods`, `exports`,
+`documents`, plus `request(query, variables)` for a custom selection. Money is integer paise; days are
+`"YYYY-MM-DD"`. Full list: [docs/graphql.md](docs/graphql.md). The types are flat in
+`@mgpl-diagnostics/client/graphql`, or under the `Gql` namespace in the main entry.
+
+## The SDK (report and invoice HTTP API)
+
+The REST client below talks to `createMgplApi()` (invoice-kit) or `createReportApi()` (report-kit) when an
+app mounts them directly, as report-studio does. New apps should use the GraphQL client above.
+
| Call | Backend route | Returns |
|---|---|---|
@@ -118,7 +170,7 @@ export function Preview({ request }) {
`HtmlFrame` isolates the document (its CSS can't touch yours), disables scripts, sizes itself to the content and
exposes `print()`.
-## Errors and auth
+## Errors and auth (HTTP API)
```ts
import { createMgplClient, MgplApiError } from "@mgpl-diagnostics/client";
@@ -143,11 +195,12 @@ For a backend on another origin, enable CORS there: `createMgplApi({ cors: "http
```bash
npm install
npm run typecheck
-npm run build # dist/index.js + dist/react.js (the React entry is marked "use client")
+npm run build # dist/index.js, dist/graphql.js, dist/react.js (the React entry is marked "use client")
```
-End-to-end tests of the SDK against the real backend live in invoice-kit (`npm run test:api`).
-Docs: [docs/api.md](docs/api.md) · [AGENTS.md](AGENTS.md) · AI index [llms.txt](llms.txt) / [llms-full.txt](llms-full.txt).
+End-to-end tests of the HTTP SDK against the real backend live in invoice-kit (`npm run test:api`). The
+GraphQL operations are validated against the server's schema by mgpl-diagnostics/api (`npm test`).
+Docs: [docs/graphql.md](docs/graphql.md) · [docs/api.md](docs/api.md) · [AGENTS.md](AGENTS.md) · AI index [llms.txt](llms.txt) / [llms-full.txt](llms-full.txt).
## Part of MGPL Diagnostics
@@ -155,6 +208,7 @@ Docs: [docs/api.md](docs/api.md) · [AGENTS.md](AGENTS.md) · AI index [llms.txt
|---|---|---|
| [report-kit](https://github.com/mgpl-diagnostics/report-kit) | backend | reports, letterhead, templates |
| [invoice-kit](https://github.com/mgpl-diagnostics/invoice-kit) | backend | patient invoices + combined API |
+| [api](https://github.com/mgpl-diagnostics/api) | backend | the GraphQL server: one endpoint for everything |
| **client** | frontend | headless SDK, zero dependencies |
| [report-studio](https://github.com/mgpl-diagnostics/report-studio) | app | Next.js reference app |
diff --git a/docs/api.md b/docs/api.md
index ff8e745..85476ce 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,5 +1,8 @@
# API reference: @mgpl-diagnostics/client
+This page covers the HTTP client (`createMgplClient`) for report-kit / invoice-kit's API. The GraphQL
+client for mgpl-diagnostics/api is in [graphql.md](graphql.md).
+
## createMgplClient(options?)
```ts
diff --git a/docs/graphql.md b/docs/graphql.md
new file mode 100644
index 0000000..2e52ec5
--- /dev/null
+++ b/docs/graphql.md
@@ -0,0 +1,320 @@
+# GraphQL client: @mgpl-diagnostics/client
+
+`createMgplGraphQLClient` talks to **mgpl-diagnostics/api**, the backend that serves everything at one
+endpoint (`POST /graphql`). It has no dependencies: every operation is a plain string in
+`src/graphql/operations.ts`, and the api's test suite validates each one against the server's schema.
+
+```mermaid
+sequenceDiagram
+ participant App as Your app
+ participant C as createMgplGraphQLClient
+ participant API as api (POST /graphql)
+ App->>C: auth.login({ email, password })
+ C->>API: mutation Login
+ API-->>C: { token, expiresAt, user }
+ C->>C: setToken(token), onToken(token)
+ App->>C: invoices.issue({...})
+ C->>API: mutation IssueInvoice (Authorization: Bearer token)
+ API-->>C: data.issueInvoice
+ C-->>App: Invoice
+```
+
+## Options
+
+```ts
+createMgplGraphQLClient({
+ url?: string; // default "/graphql"
+ fetch?: typeof fetch; // tests, SSR, retry wrappers
+ token?: string | null; // starting session token
+ onToken?: (token: string | null) => void; // persist it where you want (or nowhere)
+ headers?: HeadersInit | (() => HeadersInit | Promise);
+ credentials?: RequestCredentials;
+});
+```
+
+The client keeps the token in memory. `auth.login` sets it, `auth.logout` ends the session on the server
+and clears it, and `setToken` / `getToken` let the app manage it directly.
+
+## Conventions
+
+- **Money is integer paise** (`totalPaise: 35400` is Rs 354.00). Catalog `mrp` and `priceList().rate` are rupees.
+- **DateTime** is an ISO string; **Day** is `"YYYY-MM-DD"` in IST. The financial year runs April to March (`"2026-27"`).
+- **Lists** take `limit` / `offset`; the server caps `limit` at 200.
+- **Documents** come back as `{ format, fileName, mimeType, byteLength, base64, html }`: a PDF in `base64`,
+ HTML (self-contained, fonts inlined) in `html`. `documentBlob(doc)` turns either into a Blob for
+ `downloadBlob`, `openBlob` or `printBlob`. CSV exports come as `{ fileName, mimeType, content }`; use `csvBlob(file)`.
+- The organisation and the acting user always come from the session. No method takes an `orgId`.
+
+## Errors
+
+Every failure throws `MgplGraphQLError` with a stable `code`:
+
+| code | Meaning |
+|---|---|
+| `UNAUTHENTICATED` | No token, or it expired. Sign in again. |
+| `FORBIDDEN` | The user lacks the permission (at that branch). |
+| `NOT_FOUND` | The id does not exist in this organisation. |
+| `BAD_USER_INPUT` | Validation failed; `message` says what. |
+| `CONFLICT` | Duplicate or wrong state (e.g. releasing an unverified report). |
+| `PERIOD_LOCKED` | The date is on or before a lock date. |
+| `CREDIT_LIMIT_EXCEEDED` | A customer invoice would exceed the credit limit. |
+| `INVALID_CREDENTIALS`, `ACCOUNT_DISABLED`, `INVITE_PENDING`, `ORG_REQUIRED`, `RATE_LIMITED` (with `extensions.retryAfterMs`), `WEAK_PASSWORD`, `INVALID_TOKEN` | auth-kit's codes, unchanged. |
+| `NETWORK_ERROR` / `INTERNAL_SERVER_ERROR` | No response, or a server fault (details are masked). |
+
+`error.errors` holds every GraphQL error, `error.status` the HTTP status, `error.data` any partial data.
+
+## Custom selections
+
+The typed methods select a fixed set of fields. For anything else, send your own document:
+
+```ts
+const data = await mgpl.request<{ patient: { name: string; invoices: { invoiceNo: string; duePaise: number }[] } }>(
+ `query ($id: ID!) { patient(id: $id) { name invoices(limit: 5) { invoiceNo duePaise } } }`,
+ { id },
+);
+```
+
+`OPERATIONS[name].document` and `FRAGMENTS` are exported too, for GraphQL tools that want the strings.
+
+## Methods
+
+Each method runs one operation and returns its root field. Permissions are listed in the api's
+docs (`docs/04-api-reference.md`).
+
+### client (root)
+
+| Method | GraphQL |
+|---|---|
+| `health` | query `health` |
+| `ping` | mutation `ping` |
+
+### auth
+
+| Method | GraphQL |
+|---|---|
+| `login` | mutation `login` |
+| `logout` | mutation `logout` |
+| `viewer` | query `viewer` |
+| `can` | query `viewer { can }` |
+| `sessions` | query `mySessions` |
+| `logoutOtherSessions` | mutation `logoutOtherSessions` |
+| `revokeSession` | mutation `revokeSession` |
+| `changePassword` | mutation `changePassword` |
+| `requestPasswordReset` | mutation `requestPasswordReset` |
+| `resetPassword` | mutation `resetPassword` |
+
+### users
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `users` |
+| `get` | query `user` |
+| `create` | mutation `createUser` |
+| `setRoles` | mutation `setUserRoles` |
+| `disable` | mutation `disableUser` |
+| `enable` | mutation `enableUser` |
+| `setPassword` | mutation `setUserPassword` |
+| `roles` | query `roles` |
+| `upsertRole` | mutation `upsertRole` |
+| `permissions` | query `permissions` |
+
+### organization
+
+| Method | GraphQL |
+|---|---|
+| `get` | query `organization` |
+| `update` | mutation `updateOrganization` |
+| `branches` | query `branches` |
+| `branch` | query `branch` |
+| `createBranch` | mutation `createBranch` |
+| `updateBranch` | mutation `updateBranch` |
+| `auditLog` | query `auditLog` |
+
+### catalog
+
+| Method | GraphQL |
+|---|---|
+| `search` | query `catalogSearch` |
+| `test` | query `catalogTest` |
+| `tests` | query `catalogTests` |
+| `departments` | query `catalogDepartments` |
+| `sampleTypes` | query `catalogSampleTypes` |
+| `priceQuote` | query `priceQuote` |
+
+### patients
+
+| Method | GraphQL |
+|---|---|
+| `search` | query `patients` |
+| `get` | query `patient` |
+| `register` | mutation `registerPatient` |
+| `update` | mutation `updatePatient` |
+
+### doctors
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `doctors` |
+| `create` | mutation `createDoctor` |
+| `update` | mutation `updateDoctor` |
+
+### bookings
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `bookings` |
+| `get` | query `booking` |
+| `create` | mutation `createBooking` |
+| `cancel` | mutation `cancelBooking` |
+
+### samples
+
+| Method | GraphQL |
+|---|---|
+| `get` | query `sample` |
+| `byBarcode` | query `sampleByBarcode` |
+| `collect` | mutation `collectSample` |
+| `receive` | mutation `receiveSample` |
+| `reject` | mutation `rejectSample` |
+
+### reports
+
+| Method | GraphQL |
+|---|---|
+| `get` | query `report` |
+| `create` | mutation `createReport` |
+| `saveResults` | mutation `saveResults` |
+| `verify` | mutation `verifyReport` |
+| `release` | mutation `releaseReport` |
+| `amend` | mutation `amendReport` |
+
+### invoices
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `invoices` |
+| `get` | query `invoice` |
+| `issue` | mutation `issueInvoice` |
+| `recordPayment` | mutation `recordPayment` |
+| `refund` | mutation `refundPayment` |
+| `cancel` | mutation `cancelInvoice` |
+
+### books
+
+| Method | GraphQL |
+|---|---|
+| `groups` | query `accountGroups` |
+| `ledgers` | query `ledgers` |
+| `ledger` | query `ledger` |
+| `voucher` | query `voucher` |
+| `ledgerStatement` | query `ledgerStatement` |
+| `cashBook` | query `cashBook` |
+| `trialBalance` | query `trialBalance` |
+| `profitAndLoss` | query `profitAndLoss` |
+| `balanceSheet` | query `balanceSheet` |
+| `dayBook` | query `dayBook` |
+| `outstandingReceivables` | query `outstandingReceivables` |
+| `gstSummary` | query `gstSummary` |
+| `setupChartOfAccounts` | mutation `setupChartOfAccounts` |
+| `createLedger` | mutation `createLedger` |
+| `postVoucher` | mutation `postVoucher` |
+| `cancelVoucher` | mutation `cancelVoucher` |
+
+### gst
+
+| Method | GraphQL |
+|---|---|
+| `taxRules` | query `taxRules` |
+| `addTaxRule` | mutation `addTaxRule` |
+| `seedTaxRules` | mutation `seedTaxRules` |
+| `config` | query `financeConfig` |
+| `verifyWithCA` | query `verifyWithCA` |
+| `gstr1` | query `gstr1` |
+| `gstr3b` | query `gstr3b` |
+
+### receivables
+
+| Method | GraphQL |
+|---|---|
+| `customers` | query `customers` |
+| `customer` | query `customer` |
+| `createCustomer` | mutation `createCustomer` |
+| `updateCustomer` | mutation `updateCustomer` |
+| `issueInvoice` | mutation `issueCustomerInvoice` |
+| `statement` | query `customerStatement` |
+| `summary` | query `receivablesSummary` |
+| `creditNotes` | query `creditNotes` |
+| `creditNote` | query `creditNote` |
+| `issueCreditNote` | mutation `issueCreditNote` |
+
+### settlements
+
+| Method | GraphQL |
+|---|---|
+| `partners` | query `partners` |
+| `partner` | query `partner` |
+| `createPartner` | mutation `createPartner` |
+| `updatePartner` | mutation `updatePartner` |
+| `addAgreement` | mutation `addAgreement` |
+| `preview` | query `settlementPreview` |
+| `list` | query `settlements` |
+| `get` | query `settlement` |
+| `create` | mutation `createSettlement` |
+| `accrue` | mutation `accrueSettlement` |
+| `pay` | mutation `paySettlement` |
+| `cancel` | mutation `cancelSettlement` |
+
+### cash
+
+| Method | GraphQL |
+|---|---|
+| `closings` | query `cashClosings` |
+| `closeDay` | mutation `closeCashDay` |
+
+### bank
+
+| Method | GraphQL |
+|---|---|
+| `statements` | query `bankStatements` |
+| `lines` | query `bankLines` |
+| `import` | mutation `importBankStatement` |
+| `reconcile` | mutation `reconcileBank` |
+| `match` | mutation `matchBankLine` |
+| `setStatus` | mutation `setBankLineStatus` |
+| `reconciliation` | query `bankReconciliation` |
+
+### periods
+
+| Method | GraphQL |
+|---|---|
+| `lockDates` | query `lockDates` |
+| `setLockDate` | mutation `setLockDate` |
+| `closeFinancialYear` | mutation `closeFinancialYear` |
+
+### exports
+
+| Method | GraphQL |
+|---|---|
+| `trialBalance` | query `exportTrialBalance` |
+| `profitAndLoss` | query `exportProfitAndLoss` |
+| `balanceSheet` | query `exportBalanceSheet` |
+| `dayBook` | query `exportDayBook` |
+| `ledger` | query `exportLedger` |
+| `gstr1` | query `exportGstr1` |
+| `gstr3b` | query `exportGstr3b` |
+| `settlement` | query `exportSettlement` |
+
+### documents
+
+| Method | GraphQL |
+|---|---|
+| `templates` | query `reportTemplates` |
+| `priceList` | query `priceList` |
+| `reportSamples` | query `documentSamples` |
+| `invoiceSamples` | query `invoiceSamples` |
+| `renderReport` | query `renderReport` |
+| `renderInvoice` | query `renderInvoice` |
+| `invoiceTotals` | query `invoiceTotals` |
+| `letterhead` | query `letterhead` |
+| `invoice` | query `invoiceDocument` |
+| `bookingReports` | query `bookingReportDocument` |
diff --git a/llms-full.txt b/llms-full.txt
index f0661f9..c4a5c6c 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -1,15 +1,21 @@
# @mgpl-diagnostics/client — full documentation and source
-> Generated by scripts/llms-full.mjs. Do not edit by hand. 9 files.
+> Generated by scripts/llms-full.mjs. Do not edit by hand. 15 files.
## Contents
- README.md
- AGENTS.md
- docs/api.md
+- docs/graphql.md
- llms.txt
- package.json
- scripts/llms-full.mjs
+- src/graphql/client.ts
+- src/graphql/index.ts
+- src/graphql/operations.ts
+- src/graphql/transport.ts
+- src/graphql/types.ts
- src/index.ts
- src/react.tsx
- src/types.ts
@@ -35,7 +41,7 @@ Your UI, your design system, your UX. The backend renders the reports and invoic
[](#react-optional)
[](LICENSE)
-[Quick start](#quick-start) · [SDK](#the-sdk) · [React](#react-optional) · [Errors & auth](#errors-and-auth) · [Docs](docs/api.md)
+[Quick start](#quick-start) · [GraphQL](#graphql-the-whole-backend) · [SDK](#the-sdk) · [React](#react-optional) · [Errors & auth](#errors-and-auth) · [Docs](docs/api.md)
@@ -80,7 +86,59 @@ const pdf = await mgpl.reports.pdf({
downloadBlob(pdf, "MGPL1025.pdf");
```
-## The SDK
+## GraphQL: the whole backend
+
+[mgpl-diagnostics/api](https://github.com/mgpl-diagnostics/api) serves everything (sign-in, patients,
+bookings, samples, results, billing, books, GST, receivables, settlements, cash and bank, documents) at
+**one endpoint, `POST /graphql`**. `createMgplGraphQLClient` has a typed method for each of its 138
+operations, with no GraphQL library: the operations are plain strings, validated against the server's
+schema by the api's contract test.
+
+```mermaid
+flowchart LR
+ UI["Your UI"] --> G["createMgplGraphQLClient()"]
+ G -- "POST /graphql
Authorization: Bearer token" --> API["mgpl-diagnostics/api"]
+ API --> K["auth · db · catalog · report · invoice · tally · finance kits"]
+```
+
+```ts
+import { createMgplGraphQLClient, documentBlob, openBlob, MgplGraphQLError } from "@mgpl-diagnostics/client";
+
+const mgpl = createMgplGraphQLClient({
+ url: "https://api.your-lab.in/graphql",
+ token: sessionStorage.getItem("mgpl-token"), // optional starting token
+ onToken: (t) => (t ? sessionStorage.setItem("mgpl-token", t) : sessionStorage.removeItem("mgpl-token")),
+});
+
+await mgpl.auth.login({ email: "desk@your-lab.in", password }); // keeps the token
+const [hit] = await mgpl.catalog.search("cbc");
+const patient = await mgpl.patients.register({ branchId, name: "Kavita Sharma", sex: "female", ageYears: 32 });
+const { booking } = await mgpl.bookings.create({ branchId, patientId: patient.id, tests: [{ testCode: hit!.test.code! }] });
+const invoice = await mgpl.invoices.issue({
+ branchId, patientId: patient.id, bookingId: booking.id,
+ items: [{ code: "CBC" }], // name and MRP from the catalog
+ payments: [{ amountPaise: 30000, mode: "upi" }],
+});
+openBlob(documentBlob(await mgpl.documents.invoice(invoice.id))); // the PDF arrives as base64
+
+try {
+ await mgpl.periods.setLockDate({ upTo: "2026-09-30" });
+} catch (e) {
+ if (e instanceof MgplGraphQLError && e.code === "FORBIDDEN") alert("Ask an accountant");
+}
+```
+
+Namespaces: `auth`, `users`, `organization`, `catalog`, `patients`, `doctors`, `bookings`, `samples`,
+`reports`, `invoices`, `books`, `gst`, `receivables`, `settlements`, `cash`, `bank`, `periods`, `exports`,
+`documents`, plus `request(query, variables)` for a custom selection. Money is integer paise; days are
+`"YYYY-MM-DD"`. Full list: [docs/graphql.md](docs/graphql.md). The types are flat in
+`@mgpl-diagnostics/client/graphql`, or under the `Gql` namespace in the main entry.
+
+## The SDK (report and invoice HTTP API)
+
+The REST client below talks to `createMgplApi()` (invoice-kit) or `createReportApi()` (report-kit) when an
+app mounts them directly, as report-studio does. New apps should use the GraphQL client above.
+
| Call | Backend route | Returns |
|---|---|---|
@@ -139,7 +197,7 @@ export function Preview({ request }) {
`HtmlFrame` isolates the document (its CSS can't touch yours), disables scripts, sizes itself to the content and
exposes `print()`.
-## Errors and auth
+## Errors and auth (HTTP API)
```ts
import { createMgplClient, MgplApiError } from "@mgpl-diagnostics/client";
@@ -164,11 +222,12 @@ For a backend on another origin, enable CORS there: `createMgplApi({ cors: "http
```bash
npm install
npm run typecheck
-npm run build # dist/index.js + dist/react.js (the React entry is marked "use client")
+npm run build # dist/index.js, dist/graphql.js, dist/react.js (the React entry is marked "use client")
```
-End-to-end tests of the SDK against the real backend live in invoice-kit (`npm run test:api`).
-Docs: [docs/api.md](docs/api.md) · [AGENTS.md](AGENTS.md) · AI index [llms.txt](llms.txt) / [llms-full.txt](llms-full.txt).
+End-to-end tests of the HTTP SDK against the real backend live in invoice-kit (`npm run test:api`). The
+GraphQL operations are validated against the server's schema by mgpl-diagnostics/api (`npm test`).
+Docs: [docs/graphql.md](docs/graphql.md) · [docs/api.md](docs/api.md) · [AGENTS.md](AGENTS.md) · AI index [llms.txt](llms.txt) / [llms-full.txt](llms-full.txt).
## Part of MGPL Diagnostics
@@ -176,6 +235,7 @@ Docs: [docs/api.md](docs/api.md) · [AGENTS.md](AGENTS.md) · AI index [llms.txt
|---|---|---|
| [report-kit](https://github.com/mgpl-diagnostics/report-kit) | backend | reports, letterhead, templates |
| [invoice-kit](https://github.com/mgpl-diagnostics/invoice-kit) | backend | patient invoices + combined API |
+| [api](https://github.com/mgpl-diagnostics/api) | backend | the GraphQL server: one endpoint for everything |
| **client** | frontend | headless SDK, zero dependencies |
| [report-studio](https://github.com/mgpl-diagnostics/report-studio) | app | Next.js reference app |
@@ -192,8 +252,9 @@ Instructions for AI coding agents and developers.
## What this is
-The **frontend** SDK for the MGPL Diagnostics backend (`createMgplApi()` from invoice-kit, or
-`createReportApi()` from report-kit). It is **headless and has zero dependencies**: a typed `fetch` client,
+The **frontend** SDK for the MGPL Diagnostics backend: the GraphQL API of **mgpl-diagnostics/api** (one
+endpoint, `POST /graphql`, for everything) and the HTTP API of `createMgplApi()` (invoice-kit) /
+`createReportApi()` (report-kit). It is **headless and has zero dependencies**: a typed `fetch` client,
browser helpers, and optional **unstyled** React hooks. All rendering (reports, invoices, PDFs) happens on
the backend. Apps build their own UI/UX on top.
@@ -205,8 +266,12 @@ the backend. Apps build their own UI/UX on top.
3. **The contract is mirrored, not imported.** `src/types.ts` mirrors report-kit's `src/shared/api.ts` /
`types.ts` and invoice-kit's `api.ts`. Never import from the backend kits (that would pull the PDF
engine into browsers). report-studio's `contract-check.ts` type-checks both sides; update them together.
-4. **Routes** are in `ROUTES` in `src/index.ts` and must match report-kit `src/shared/api.ts` + invoice-kit `INVOICE_ROUTES`.
-5. `src/react.tsx` gets a `"use client"` banner from tsup (see `tsup.config.ts`); the main entry must not,
+4. **GraphQL operations are plain strings** in `src/graphql/operations.ts` (no GraphQL library). Their
+ selections match `src/graphql/types.ts`. mgpl-diagnostics/api validates every operation against its
+ schema and checks that every root field has one; when the API adds or renames a field, update
+ `operations.ts`, `types.ts` and `client.ts` here, release, then bump the client ref in api's `kits.json`.
+5. **Routes** are in `ROUTES` in `src/index.ts` and must match report-kit `src/shared/api.ts` + invoice-kit `INVOICE_ROUTES`.
+6. `src/react.tsx` gets a `"use client"` banner from tsup (see `tsup.config.ts`); the main entry must not,
so it stays usable from Server Components, workers and Node scripts.
## Map
@@ -214,6 +279,12 @@ the backend. Apps build their own UI/UX on top.
```
src/types.ts contract types (Patient, ReportRequest, TemplateCatalog, InvoiceInput, InvoiceTotals, …)
src/index.ts createMgplClient (reports.*, invoices.*, health, url), MgplApiError, ROUTES, downloadBlob/openBlob/printBlob
+src/graphql/
+ transport.ts createTransport (POST, Bearer token, onToken), MgplGraphQLError (code from extensions.code)
+ operations.ts FRAGMENTS + OPERATIONS (one per Query/Mutation field), withFragments
+ client.ts createMgplGraphQLClient (auth, users, organization, catalog, patients, … documents), documentBlob, csvBlob
+ types.ts the API's GraphQL types as selected by operations.ts, plus inputs
+ index.ts the "/graphql" entry (types flat); the main entry re-exports the client, types under `Gql`
src/react.tsx useAsync, useTemplates, useReportHtml, useInvoiceHtml, useInvoiceTotals, usePdfUrl, HtmlFrame
```
@@ -238,6 +309,9 @@ All rights reserved (see LICENSE).
# API reference: @mgpl-diagnostics/client
+This page covers the HTTP client (`createMgplClient`) for report-kit / invoice-kit's API. The GraphQL
+client for mgpl-diagnostics/api is in [graphql.md](graphql.md).
+
## createMgplClient(options?)
```ts
@@ -314,6 +388,332 @@ usePdfUrl(load | null, key): { …, url }
```
+---
+
+## File: docs/graphql.md
+
+# GraphQL client: @mgpl-diagnostics/client
+
+`createMgplGraphQLClient` talks to **mgpl-diagnostics/api**, the backend that serves everything at one
+endpoint (`POST /graphql`). It has no dependencies: every operation is a plain string in
+`src/graphql/operations.ts`, and the api's test suite validates each one against the server's schema.
+
+```mermaid
+sequenceDiagram
+ participant App as Your app
+ participant C as createMgplGraphQLClient
+ participant API as api (POST /graphql)
+ App->>C: auth.login({ email, password })
+ C->>API: mutation Login
+ API-->>C: { token, expiresAt, user }
+ C->>C: setToken(token), onToken(token)
+ App->>C: invoices.issue({...})
+ C->>API: mutation IssueInvoice (Authorization: Bearer token)
+ API-->>C: data.issueInvoice
+ C-->>App: Invoice
+```
+
+## Options
+
+```ts
+createMgplGraphQLClient({
+ url?: string; // default "/graphql"
+ fetch?: typeof fetch; // tests, SSR, retry wrappers
+ token?: string | null; // starting session token
+ onToken?: (token: string | null) => void; // persist it where you want (or nowhere)
+ headers?: HeadersInit | (() => HeadersInit | Promise);
+ credentials?: RequestCredentials;
+});
+```
+
+The client keeps the token in memory. `auth.login` sets it, `auth.logout` ends the session on the server
+and clears it, and `setToken` / `getToken` let the app manage it directly.
+
+## Conventions
+
+- **Money is integer paise** (`totalPaise: 35400` is Rs 354.00). Catalog `mrp` and `priceList().rate` are rupees.
+- **DateTime** is an ISO string; **Day** is `"YYYY-MM-DD"` in IST. The financial year runs April to March (`"2026-27"`).
+- **Lists** take `limit` / `offset`; the server caps `limit` at 200.
+- **Documents** come back as `{ format, fileName, mimeType, byteLength, base64, html }`: a PDF in `base64`,
+ HTML (self-contained, fonts inlined) in `html`. `documentBlob(doc)` turns either into a Blob for
+ `downloadBlob`, `openBlob` or `printBlob`. CSV exports come as `{ fileName, mimeType, content }`; use `csvBlob(file)`.
+- The organisation and the acting user always come from the session. No method takes an `orgId`.
+
+## Errors
+
+Every failure throws `MgplGraphQLError` with a stable `code`:
+
+| code | Meaning |
+|---|---|
+| `UNAUTHENTICATED` | No token, or it expired. Sign in again. |
+| `FORBIDDEN` | The user lacks the permission (at that branch). |
+| `NOT_FOUND` | The id does not exist in this organisation. |
+| `BAD_USER_INPUT` | Validation failed; `message` says what. |
+| `CONFLICT` | Duplicate or wrong state (e.g. releasing an unverified report). |
+| `PERIOD_LOCKED` | The date is on or before a lock date. |
+| `CREDIT_LIMIT_EXCEEDED` | A customer invoice would exceed the credit limit. |
+| `INVALID_CREDENTIALS`, `ACCOUNT_DISABLED`, `INVITE_PENDING`, `ORG_REQUIRED`, `RATE_LIMITED` (with `extensions.retryAfterMs`), `WEAK_PASSWORD`, `INVALID_TOKEN` | auth-kit's codes, unchanged. |
+| `NETWORK_ERROR` / `INTERNAL_SERVER_ERROR` | No response, or a server fault (details are masked). |
+
+`error.errors` holds every GraphQL error, `error.status` the HTTP status, `error.data` any partial data.
+
+## Custom selections
+
+The typed methods select a fixed set of fields. For anything else, send your own document:
+
+```ts
+const data = await mgpl.request<{ patient: { name: string; invoices: { invoiceNo: string; duePaise: number }[] } }>(
+ `query ($id: ID!) { patient(id: $id) { name invoices(limit: 5) { invoiceNo duePaise } } }`,
+ { id },
+);
+```
+
+`OPERATIONS[name].document` and `FRAGMENTS` are exported too, for GraphQL tools that want the strings.
+
+## Methods
+
+Each method runs one operation and returns its root field. Permissions are listed in the api's
+docs (`docs/04-api-reference.md`).
+
+### client (root)
+
+| Method | GraphQL |
+|---|---|
+| `health` | query `health` |
+| `ping` | mutation `ping` |
+
+### auth
+
+| Method | GraphQL |
+|---|---|
+| `login` | mutation `login` |
+| `logout` | mutation `logout` |
+| `viewer` | query `viewer` |
+| `can` | query `viewer { can }` |
+| `sessions` | query `mySessions` |
+| `logoutOtherSessions` | mutation `logoutOtherSessions` |
+| `revokeSession` | mutation `revokeSession` |
+| `changePassword` | mutation `changePassword` |
+| `requestPasswordReset` | mutation `requestPasswordReset` |
+| `resetPassword` | mutation `resetPassword` |
+
+### users
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `users` |
+| `get` | query `user` |
+| `create` | mutation `createUser` |
+| `setRoles` | mutation `setUserRoles` |
+| `disable` | mutation `disableUser` |
+| `enable` | mutation `enableUser` |
+| `setPassword` | mutation `setUserPassword` |
+| `roles` | query `roles` |
+| `upsertRole` | mutation `upsertRole` |
+| `permissions` | query `permissions` |
+
+### organization
+
+| Method | GraphQL |
+|---|---|
+| `get` | query `organization` |
+| `update` | mutation `updateOrganization` |
+| `branches` | query `branches` |
+| `branch` | query `branch` |
+| `createBranch` | mutation `createBranch` |
+| `updateBranch` | mutation `updateBranch` |
+| `auditLog` | query `auditLog` |
+
+### catalog
+
+| Method | GraphQL |
+|---|---|
+| `search` | query `catalogSearch` |
+| `test` | query `catalogTest` |
+| `tests` | query `catalogTests` |
+| `departments` | query `catalogDepartments` |
+| `sampleTypes` | query `catalogSampleTypes` |
+| `priceQuote` | query `priceQuote` |
+
+### patients
+
+| Method | GraphQL |
+|---|---|
+| `search` | query `patients` |
+| `get` | query `patient` |
+| `register` | mutation `registerPatient` |
+| `update` | mutation `updatePatient` |
+
+### doctors
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `doctors` |
+| `create` | mutation `createDoctor` |
+| `update` | mutation `updateDoctor` |
+
+### bookings
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `bookings` |
+| `get` | query `booking` |
+| `create` | mutation `createBooking` |
+| `cancel` | mutation `cancelBooking` |
+
+### samples
+
+| Method | GraphQL |
+|---|---|
+| `get` | query `sample` |
+| `byBarcode` | query `sampleByBarcode` |
+| `collect` | mutation `collectSample` |
+| `receive` | mutation `receiveSample` |
+| `reject` | mutation `rejectSample` |
+
+### reports
+
+| Method | GraphQL |
+|---|---|
+| `get` | query `report` |
+| `create` | mutation `createReport` |
+| `saveResults` | mutation `saveResults` |
+| `verify` | mutation `verifyReport` |
+| `release` | mutation `releaseReport` |
+| `amend` | mutation `amendReport` |
+
+### invoices
+
+| Method | GraphQL |
+|---|---|
+| `list` | query `invoices` |
+| `get` | query `invoice` |
+| `issue` | mutation `issueInvoice` |
+| `recordPayment` | mutation `recordPayment` |
+| `refund` | mutation `refundPayment` |
+| `cancel` | mutation `cancelInvoice` |
+
+### books
+
+| Method | GraphQL |
+|---|---|
+| `groups` | query `accountGroups` |
+| `ledgers` | query `ledgers` |
+| `ledger` | query `ledger` |
+| `voucher` | query `voucher` |
+| `ledgerStatement` | query `ledgerStatement` |
+| `cashBook` | query `cashBook` |
+| `trialBalance` | query `trialBalance` |
+| `profitAndLoss` | query `profitAndLoss` |
+| `balanceSheet` | query `balanceSheet` |
+| `dayBook` | query `dayBook` |
+| `outstandingReceivables` | query `outstandingReceivables` |
+| `gstSummary` | query `gstSummary` |
+| `setupChartOfAccounts` | mutation `setupChartOfAccounts` |
+| `createLedger` | mutation `createLedger` |
+| `postVoucher` | mutation `postVoucher` |
+| `cancelVoucher` | mutation `cancelVoucher` |
+
+### gst
+
+| Method | GraphQL |
+|---|---|
+| `taxRules` | query `taxRules` |
+| `addTaxRule` | mutation `addTaxRule` |
+| `seedTaxRules` | mutation `seedTaxRules` |
+| `config` | query `financeConfig` |
+| `verifyWithCA` | query `verifyWithCA` |
+| `gstr1` | query `gstr1` |
+| `gstr3b` | query `gstr3b` |
+
+### receivables
+
+| Method | GraphQL |
+|---|---|
+| `customers` | query `customers` |
+| `customer` | query `customer` |
+| `createCustomer` | mutation `createCustomer` |
+| `updateCustomer` | mutation `updateCustomer` |
+| `issueInvoice` | mutation `issueCustomerInvoice` |
+| `statement` | query `customerStatement` |
+| `summary` | query `receivablesSummary` |
+| `creditNotes` | query `creditNotes` |
+| `creditNote` | query `creditNote` |
+| `issueCreditNote` | mutation `issueCreditNote` |
+
+### settlements
+
+| Method | GraphQL |
+|---|---|
+| `partners` | query `partners` |
+| `partner` | query `partner` |
+| `createPartner` | mutation `createPartner` |
+| `updatePartner` | mutation `updatePartner` |
+| `addAgreement` | mutation `addAgreement` |
+| `preview` | query `settlementPreview` |
+| `list` | query `settlements` |
+| `get` | query `settlement` |
+| `create` | mutation `createSettlement` |
+| `accrue` | mutation `accrueSettlement` |
+| `pay` | mutation `paySettlement` |
+| `cancel` | mutation `cancelSettlement` |
+
+### cash
+
+| Method | GraphQL |
+|---|---|
+| `closings` | query `cashClosings` |
+| `closeDay` | mutation `closeCashDay` |
+
+### bank
+
+| Method | GraphQL |
+|---|---|
+| `statements` | query `bankStatements` |
+| `lines` | query `bankLines` |
+| `import` | mutation `importBankStatement` |
+| `reconcile` | mutation `reconcileBank` |
+| `match` | mutation `matchBankLine` |
+| `setStatus` | mutation `setBankLineStatus` |
+| `reconciliation` | query `bankReconciliation` |
+
+### periods
+
+| Method | GraphQL |
+|---|---|
+| `lockDates` | query `lockDates` |
+| `setLockDate` | mutation `setLockDate` |
+| `closeFinancialYear` | mutation `closeFinancialYear` |
+
+### exports
+
+| Method | GraphQL |
+|---|---|
+| `trialBalance` | query `exportTrialBalance` |
+| `profitAndLoss` | query `exportProfitAndLoss` |
+| `balanceSheet` | query `exportBalanceSheet` |
+| `dayBook` | query `exportDayBook` |
+| `ledger` | query `exportLedger` |
+| `gstr1` | query `exportGstr1` |
+| `gstr3b` | query `exportGstr3b` |
+| `settlement` | query `exportSettlement` |
+
+### documents
+
+| Method | GraphQL |
+|---|---|
+| `templates` | query `reportTemplates` |
+| `priceList` | query `priceList` |
+| `reportSamples` | query `documentSamples` |
+| `invoiceSamples` | query `invoiceSamples` |
+| `renderReport` | query `renderReport` |
+| `renderInvoice` | query `renderInvoice` |
+| `invoiceTotals` | query `invoiceTotals` |
+| `letterhead` | query `letterhead` |
+| `invoice` | query `invoiceDocument` |
+| `bookingReports` | query `bookingReportDocument` |
+
+
---
## File: llms.txt
@@ -323,6 +723,7 @@ usePdfUrl(load | null, key): { …, url }
> Headless, zero-dependency TypeScript SDK for browsers (or any fetch runtime) that talks to the MGPL Diagnostics backend (`createMgplApi` / `createReportApi`). It requests lab reports and invoices as PDF Blobs, HTML strings or JSON, and fetches the test catalog and price list. It has no UI and no CSS; apps build their own UI/UX. Optional unstyled React hooks live in "@mgpl-diagnostics/client/react".
Key facts for agents:
+- GraphQL (mgpl-diagnostics/api, one endpoint for everything): `const mgpl = createMgplGraphQLClient({ url: "/graphql", token?, onToken? })`; `await mgpl.auth.login({ email, password })` keeps the token. Namespaces: auth, users, organization, catalog, patients, doctors, bookings, samples, reports, invoices, books, gst, receivables, settlements, cash, bank, periods, exports, documents; `request(query, vars)` for custom selections. Errors throw `MgplGraphQLError` with `code`. Money in paise; documents are base64 PDFs or HTML (`documentBlob`). Types flat in "@mgpl-diagnostics/client/graphql" or under `Gql`.
- `const mgpl = createMgplClient({ baseUrl: "/api/mgpl", headers?, credentials?, fetch? })`.
- Reports: `mgpl.reports.templates()`, `.pdf(req)`, `.html(req)`, `.letterheadPdf()`, `.samples()`. Invoices: `mgpl.invoices.totals(invoice)`, `.pdf(req)`, `.html(req)`, `.priceList()`.
- Build entry forms from `templates()`: each template lists `fields` (key, label, unit, range); send `{ patient, tests: [{ template, results: { [key]: value } }] }`.
@@ -333,6 +734,7 @@ Key facts for agents:
## Docs
- [README](README.md): why, quick start, SDK table, React hooks, errors and auth
+- [GraphQL client](docs/graphql.md): every method, the operation it runs, errors, conventions
- [API reference](docs/api.md): every method, option and type
- [AGENTS.md](AGENTS.md): rules (no deps, no UI, mirrored contract)
- [llms-full.txt](llms-full.txt): everything above plus the source
@@ -340,6 +742,9 @@ Key facts for agents:
## Source
- [src/index.ts](src/index.ts): createMgplClient, helpers
+- [src/graphql/client.ts](src/graphql/client.ts): createMgplGraphQLClient, documentBlob, csvBlob
+- [src/graphql/operations.ts](src/graphql/operations.ts): fragments and operations
+- [src/graphql/types.ts](src/graphql/types.ts): GraphQL result and input types
- [src/react.tsx](src/react.tsx): hooks and HtmlFrame
- [src/types.ts](src/types.ts): contract types
@@ -351,8 +756,8 @@ Key facts for agents:
```json
{
"name": "@mgpl-diagnostics/client",
- "version": "1.0.0",
- "description": "Headless browser SDK for the MGPL Diagnostics backend (reports + invoices). Zero dependencies, no UI — build your own.",
+ "version": "1.1.0",
+ "description": "Headless SDK for the MGPL Diagnostics backend: the GraphQL API (everything) and the report/invoice HTTP API. Zero dependencies, no UI \u2014 build your own.",
"type": "module",
"license": "SEE LICENSE IN LICENSE",
"repository": "github:mgpl-diagnostics/client",
@@ -364,6 +769,10 @@ Key facts for agents:
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
+ "./graphql": {
+ "types": "./dist/graphql.d.ts",
+ "import": "./dist/graphql.js"
+ },
"./react": {
"types": "./dist/react.d.ts",
"import": "./dist/react.js"
@@ -471,6 +880,1076 @@ console.log(`llms-full.txt: ${ordered.length} files, ${(out.length / 1024).toFix
````
+---
+
+## File: src/graphql/client.ts
+
+```ts
+// createMgplGraphQLClient: one typed method per operation of the backend's single GraphQL endpoint
+// (mgpl-diagnostics/api, POST /graphql). Zero dependencies: operations are plain strings.
+import type { InvoiceRequest, InvoiceTotals, ReportRequest } from "../types";
+import { OPERATIONS, type Operation } from "./operations";
+import { createTransport, type GraphQLClientOptions, type Variables } from "./transport";
+import type * as T from "./types";
+
+type Page = { limit?: number; offset?: number };
+type Range = { from: T.DateTime; to: T.DateTime };
+type Csv = { bom?: boolean };
+
+export function createMgplGraphQLClient(opts: GraphQLClientOptions = {}) {
+ const transport = createTransport(opts);
+
+ /** Runs one operation and returns its root field. */
+ async function run(operation: Operation, variables?: Variables, signal?: AbortSignal): Promise {
+ const data = await transport.request>(operation.document, variables, { signal });
+ return data[operation.field] as R;
+ }
+ const O = OPERATIONS;
+
+ return {
+ url: transport.url,
+ /** Any query or mutation, for what the typed methods don't cover (e.g. a custom selection). */
+ request: transport.request,
+ getToken: transport.getToken,
+ setToken: transport.setToken,
+
+ health: () => run(O.health),
+ ping: () => run(O.ping),
+
+ auth: {
+ /** Signs in and keeps the session token for every later call (see `onToken` to persist it). */
+ async login(input: T.LoginInput): Promise {
+ const result = await run(O.login, { input });
+ transport.setToken(result.token);
+ return result;
+ },
+ /** Ends this session on the server and forgets the token. */
+ async logout(): Promise {
+ try {
+ if (transport.getToken()) await run(O.logout);
+ } finally {
+ transport.setToken(null);
+ }
+ },
+ /** The signed-in user, organisation and permissions; null when the token is missing or expired. */
+ viewer: () => run(O.viewer),
+ /** Whether the signed-in user may do `permission` (at `branchId`, when given). */
+ can: async (permission: string, branchId?: T.ID) =>
+ (await run<{ can: boolean } | null>(O.can, { permission, branchId }))?.can ?? false,
+ sessions: () => run(O.mySessions),
+ logoutOtherSessions: () => run(O.logoutOtherSessions),
+ revokeSession: (id: T.ID) => run(O.revokeSession, { id }),
+ changePassword: (currentPassword: string, newPassword: string) =>
+ run(O.changePassword, { currentPassword, newPassword }),
+ /** Always resolves true; the server emails the link if the account exists. */
+ requestPasswordReset: (email: string, orgCode?: string) => run(O.requestPasswordReset, { email, orgCode }),
+ resetPassword: (token: string, newPassword: string) => run(O.resetPassword, { token, newPassword }),
+ },
+
+ users: {
+ list: () => run(O.users),
+ get: (id: T.ID) => run(O.user, { id }),
+ create: (input: T.CreateUserInput) => run(O.createUser, { input }),
+ setRoles: (userId: T.ID, roles: T.RoleAssignmentInput[]) => run(O.setUserRoles, { userId, roles }),
+ disable: (userId: T.ID) => run(O.disableUser, { userId }),
+ enable: (userId: T.ID) => run(O.enableUser, { userId }),
+ setPassword: (userId: T.ID, newPassword: string) => run(O.setUserPassword, { userId, newPassword }),
+ roles: () => run(O.roles),
+ upsertRole: (input: T.RoleInput) => run(O.upsertRole, { input }),
+ /** Every permission name the API checks. */
+ permissions: () => run(O.permissions),
+ },
+
+ organization: {
+ get: () => run(O.organization),
+ update: (input: T.OrganizationInput) => run(O.updateOrganization, { input }),
+ branches: () => run(O.branches),
+ branch: (id: T.ID) => run(O.branch, { id }),
+ createBranch: (input: T.BranchInput) => run(O.createBranch, { input }),
+ updateBranch: (id: T.ID, input: T.BranchPatch) => run(O.updateBranch, { id, input }),
+ auditLog: (filter: { entity?: string; entityId?: T.ID } & Page = {}) => run(O.auditLog, filter),
+ },
+
+ /** The test catalog. No sign-in needed. */
+ catalog: {
+ search: (query: string, o: { department?: string; sampleType?: string; limit?: number; fuzzy?: boolean } = {}) =>
+ run(O.catalogSearch, { query, ...o }),
+ /** By numeric id or code ("CBC", "LFT"); `sex` picks the reference range. */
+ test: (idOrCode: number | string, sex?: T.Sex) =>
+ run(O.catalogTest, typeof idOrCode === "number" ? { id: idOrCode, sex } : { code: idOrCode, sex }),
+ tests: (filter: { department?: string; sampleType?: string } & Page = {}) => run(O.catalogTests, filter),
+ departments: () => run(O.catalogDepartments),
+ sampleTypes: () => run(O.catalogSampleTypes),
+ priceQuote: (tests: string[]) => run(O.priceQuote, { tests }),
+ },
+
+ patients: {
+ /** Name, phone or UHID. */
+ search: (search: string, limit?: number) => run(O.patients, { search, limit }),
+ get: (id: T.ID) => run(O.patient, { id }),
+ register: (input: T.RegisterPatientInput) => run(O.registerPatient, { input }),
+ update: (id: T.ID, input: T.UpdatePatientInput) => run(O.updatePatient, { id, input }),
+ },
+
+ doctors: {
+ list: (activeOnly?: boolean) => run(O.doctors, { activeOnly }),
+ create: (input: T.DoctorInput) => run(O.createDoctor, { input }),
+ update: (id: T.ID, input: T.DoctorPatch) => run(O.updateDoctor, { id, input }),
+ },
+
+ bookings: {
+ list: (filter: { branchId?: T.ID; patientId?: T.ID; status?: T.BookingStatus } & Page = {}) =>
+ run(O.bookings, filter),
+ get: (id: T.ID) => run(O.booking, { id }),
+ /** Creates the booking and one sample per sample type. */
+ create: (input: T.CreateBookingInput) => run(O.createBooking, { input }),
+ cancel: (id: T.ID) => run(O.cancelBooking, { id }),
+ },
+
+ samples: {
+ get: (id: T.ID) => run(O.sample, { id }),
+ byBarcode: (barcode: string) => run(O.sampleByBarcode, { barcode }),
+ collect: (id: T.ID) => run(O.collectSample, { id }),
+ receive: (id: T.ID) => run(O.receiveSample, { id }),
+ reject: (id: T.ID, reason: string) => run(O.rejectSample, { id, reason }),
+ },
+
+ reports: {
+ get: (id: T.ID) => run(O.report, { id }),
+ create: (bookingId: T.ID, testCode: string, testName?: string) =>
+ run(O.createReport, { bookingId, testCode, testName }),
+ /** Unit, range and flag are filled from the catalog when omitted. */
+ saveResults: (reportId: T.ID, results: T.ResultInput[]) => run(O.saveResults, { reportId, results }),
+ verify: (id: T.ID) => run(O.verifyReport, { id }),
+ release: (id: T.ID) => run(O.releaseReport, { id }),
+ amend: (id: T.ID) => run(O.amendReport, { id }),
+ },
+
+ invoices: {
+ list: (filter: { branchId?: T.ID; patientId?: T.ID; customerId?: T.ID; status?: T.InvoiceStatus } & Page = {}) =>
+ run(O.invoices, filter),
+ get: (id: T.ID) => run(O.invoice, { id }),
+ /** Items can be catalog codes alone; name and MRP come from the catalog. Posts to the books. */
+ issue: (input: T.IssueInvoiceInput) => run(O.issueInvoice, { input }),
+ recordPayment: (invoiceId: T.ID, payment: T.PaymentInput) => run(O.recordPayment, { invoiceId, payment }),
+ refund: (invoiceId: T.ID, payment: T.PaymentInput) => run(O.refundPayment, { invoiceId, payment }),
+ cancel: (id: T.ID, reason: string) => run(O.cancelInvoice, { id, reason }),
+ },
+
+ books: {
+ groups: () => run(O.accountGroups),
+ ledgers: () => run(O.ledgers),
+ ledger: (id: T.ID) => run(O.ledger, { id }),
+ voucher: (id: T.ID) => run(O.voucher, { id }),
+ ledgerStatement: (ledgerId: T.ID, range: Range, branchId?: T.ID) =>
+ run(O.ledgerStatement, { ledgerId, ...range, branchId }),
+ cashBook: (range: Range, branchId?: T.ID) => run(O.cashBook, { ...range, branchId }),
+ trialBalance: (o: { asOf?: T.DateTime; branchId?: T.ID } = {}) => run(O.trialBalance, o),
+ profitAndLoss: (range: Range, branchId?: T.ID) => run(O.profitAndLoss, { ...range, branchId }),
+ balanceSheet: (asOf: T.DateTime, branchId?: T.ID) => run(O.balanceSheet, { asOf, branchId }),
+ dayBook: (range: Range, o: { branchId?: T.ID; type?: T.VoucherType } = {}) => run(O.dayBook, { ...range, ...o }),
+ outstandingReceivables: (o: { asOf?: T.DateTime; branchId?: T.ID } = {}) => run(O.outstandingReceivables, o),
+ gstSummary: (range: Range, branchId?: T.ID) => run(O.gstSummary, { ...range, branchId }),
+ /** Seeds the standard chart and finance ledgers. Safe to repeat. */
+ setupChartOfAccounts: () => run(O.setupChartOfAccounts),
+ createLedger: (input: T.LedgerInput) => run(O.createLedger, { input }),
+ postVoucher: (input: T.PostVoucherInput) => run(O.postVoucher, { input }),
+ cancelVoucher: (id: T.ID) => run(O.cancelVoucher, { id }),
+ },
+
+ gst: {
+ taxRules: () => run(O.taxRules),
+ addTaxRule: (input: T.TaxRuleInput) => run(O.addTaxRule, { input }),
+ /** finance-kit's starting rules. Every rate must be verified with the lab's CA. */
+ seedTaxRules: () => run(O.seedTaxRules),
+ config: () => run(O.financeConfig),
+ verifyWithCA: () => run(O.verifyWithCA),
+ gstr1: (range: Range, gstin?: string) => run(O.gstr1, { ...range, gstin }),
+ gstr3b: (range: Range, gstin?: string) => run(O.gstr3b, { ...range, gstin }),
+ },
+
+ receivables: {
+ customers: (activeOnly?: boolean) => run(O.customers, { activeOnly }),
+ customer: (id: T.ID) => run(O.customer, { id }),
+ createCustomer: (input: T.CustomerInput) => run(O.createCustomer, { input }),
+ updateCustomer: (id: T.ID, input: T.CustomerPatch) => run(O.updateCustomer, { id, input }),
+ /** A credit invoice to a hospital, insurer, TPA or corporate (checks the credit limit). */
+ issueInvoice: (input: T.CustomerInvoiceInput) => run(O.issueCustomerInvoice, { input }),
+ statement: (customerId: T.ID, range: Range) => run(O.customerStatement, { customerId, ...range }),
+ summary: (asOf: T.DateTime) => run(O.receivablesSummary, { asOf }),
+ creditNotes: (filter: { customerId?: T.ID; invoiceId?: T.ID } & Page = {}) => run(O.creditNotes, filter),
+ creditNote: (id: T.ID) => run(O.creditNote, { id }),
+ issueCreditNote: (input: T.CreditNoteInput) => run(O.issueCreditNote, { input }),
+ },
+
+ settlements: {
+ partners: (branchId?: T.ID) => run(O.partners, { branchId }),
+ partner: (id: T.ID) => run(O.partner, { id }),
+ createPartner: (input: T.PartnerInput) => run(O.createPartner, { input }),
+ updatePartner: (id: T.ID, input: T.PartnerPatch) => run(O.updatePartner, { id, input }),
+ addAgreement: (input: T.AgreementInput) => run(O.addAgreement, { input }),
+ /** period: "YYYY-MM". Computes without saving. */
+ preview: (partnerId: T.ID, period: string, deductions?: T.SettlementDeductionInput[]) =>
+ run(O.settlementPreview, { partnerId, period, deductions }),
+ list: (filter: { branchId?: T.ID; partnerId?: T.ID; status?: T.SettlementStatus } & Page = {}) =>
+ run(O.settlements, filter),
+ get: (id: T.ID) => run(O.settlement, { id }),
+ create: (partnerId: T.ID, period: string, deductions?: T.SettlementDeductionInput[]) =>
+ run(O.createSettlement, { partnerId, period, deductions }),
+ accrue: (id: T.ID) => run(O.accrueSettlement, { id }),
+ pay: (input: T.PaySettlementInput) => run(O.paySettlement, { input }),
+ cancel: (id: T.ID) => run(O.cancelSettlement, { id }),
+ },
+
+ cash: {
+ closings: (branchId: T.ID, page: Page = {}) => run(O.cashClosings, { branchId, ...page }),
+ closeDay: (input: T.CloseCashDayInput) => run(O.closeCashDay, { input }),
+ },
+
+ bank: {
+ statements: (filter: { ledgerId?: T.ID } & Page = {}) => run(O.bankStatements, filter),
+ lines: (filter: { ledgerId?: T.ID; statementId?: T.ID; status?: T.BankLineStatus } & Page = {}) =>
+ run(O.bankLines, filter),
+ import: (input: T.BankImportInput) => run(O.importBankStatement, { input }),
+ reconcile: (ledgerId: T.ID, statementId?: T.ID) => run(O.reconcileBank, { ledgerId, statementId }),
+ match: (lineId: T.ID, voucherIds: T.ID[]) => run(O.matchBankLine, { lineId, voucherIds }),
+ setStatus: (lineId: T.ID, status: "ignored" | "unmatched") => run(O.setBankLineStatus, { lineId, status }),
+ reconciliation: (ledgerId: T.ID, asOf: T.DateTime) => run(O.bankReconciliation, { ledgerId, asOf }),
+ },
+
+ periods: {
+ lockDates: () => run(O.lockDates),
+ /** upTo: "YYYY-MM-DD", or null to unlock. Without branchId it locks the whole organisation. */
+ setLockDate: (input: T.LockDateInput) => run(O.setLockDate, { input }),
+ /** fy: "2026-27". Posts closing vouchers on 31 March and locks the year. */
+ closeFinancialYear: (fy: string) => run(O.closeFinancialYear, { fy }),
+ },
+
+ /** CSV files ending with TOTAL rows. Save one with `csvBlob(file)` + downloadBlob. */
+ exports: {
+ trialBalance: (o: { asOf?: T.DateTime; branchId?: T.ID } & Csv = {}) => run(O.exportTrialBalance, o),
+ profitAndLoss: (range: Range, o: { branchId?: T.ID } & Csv = {}) => run(O.exportProfitAndLoss, { ...range, ...o }),
+ balanceSheet: (asOf: T.DateTime, o: { branchId?: T.ID } & Csv = {}) => run(O.exportBalanceSheet, { asOf, ...o }),
+ dayBook: (range: Range, o: { branchId?: T.ID } & Csv = {}) => run(O.exportDayBook, { ...range, ...o }),
+ ledger: (ledgerId: T.ID, range: Range, o: { branchId?: T.ID } & Csv = {}) =>
+ run(O.exportLedger, { ledgerId, ...range, ...o }),
+ /** One file per section: b2b, b2cl, b2cs, cdnr, cdnur, nil, hsn, docs. */
+ gstr1: (range: Range, o: { gstin?: string } & Csv = {}) => run(O.exportGstr1, { ...range, ...o }),
+ gstr3b: (range: Range, o: { gstin?: string } & Csv = {}) => run(O.exportGstr3b, { ...range, ...o }),
+ settlement: (settlementId: T.ID, o: Csv = {}) => run(O.exportSettlement, { settlementId, ...o }),
+ },
+
+ /** Reports and invoices rendered by the server (report-kit / invoice-kit). PDFs come as base64. */
+ documents: {
+ templates: () => run(O.reportTemplates),
+ priceList: () => run(O.priceList),
+ reportSamples: () => run(O.documentSamples),
+ invoiceSamples: () => run(O.invoiceSamples),
+ /** Any report request (patient + tests | profile | report | sample), as with the REST client. */
+ renderReport: (request: ReportRequest, o: T.RenderOptions = {}) => run(O.renderReport, { request, ...o }),
+ /** Any invoice request ({ invoice } or { sample }). */
+ renderInvoice: (request: InvoiceRequest, o: Omit = {}) =>
+ run(O.renderInvoice, { request, ...o }),
+ /** invoice-kit's calculation, without rendering (amounts in rupees). */
+ invoiceTotals: (request: InvoiceRequest) => run(O.invoiceTotals, { request }),
+ letterhead: (format?: T.DocumentFormat) => run(O.letterhead, { format }),
+ /** A stored invoice, printed. */
+ invoice: (invoiceId: T.ID, o: Omit = {}) =>
+ run(O.invoiceDocument, { invoiceId, ...o }),
+ /** A booking's verified or released reports printed together (or only `reportIds`). */
+ bookingReports: (bookingId: T.ID, o: Omit & { reportIds?: T.ID[] } = {}) =>
+ run(O.bookingReportDocument, { bookingId, ...o }),
+ },
+ };
+}
+
+export type MgplGraphQLClient = ReturnType;
+
+/** A rendered document as a Blob: PDF from base64, HTML from its markup. Use with downloadBlob/openBlob/printBlob. */
+export function documentBlob(doc: Pick): Blob {
+ if (doc.base64 != null) {
+ const bin = atob(doc.base64);
+ const bytes = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
+ return new Blob([bytes], { type: doc.mimeType });
+ }
+ return new Blob([doc.html ?? ""], { type: doc.mimeType });
+}
+
+/** A CSV export as a Blob (UTF-8). */
+export function csvBlob(file: T.CsvFile): Blob {
+ return new Blob([file.content], { type: file.mimeType });
+}
+```
+
+
+---
+
+## File: src/graphql/index.ts
+
+```ts
+// "@mgpl-diagnostics/client/graphql": the GraphQL SDK with every type flat.
+// The main entry re-exports the client and helpers too, with these types under the `Gql` namespace.
+export { createMgplGraphQLClient, documentBlob, csvBlob, type MgplGraphQLClient } from "./client";
+export { createTransport, MgplGraphQLError, type GraphQLClientOptions, type GraphQLErrorItem, type Transport, type Variables } from "./transport";
+export { OPERATIONS, FRAGMENTS, withFragments, type Operation, type OperationName } from "./operations";
+export type * from "./types";
+```
+
+
+---
+
+## File: src/graphql/operations.ts
+
+```ts
+// Every operation the client sends, as plain GraphQL strings (no runtime GraphQL library).
+// The selections match src/graphql/types.ts. The API's contract test validates each one against
+// the server's schema (mgpl-diagnostics/api, test/client-contract.ts), so a renamed field fails there.
+
+/** Reusable selections. An operation gets the fragments it spreads (and theirs) appended. */
+export const FRAGMENTS: Record = {
+ AddressFields: `fragment AddressFields on Address { line1 line2 city district state pincode }`,
+ BranchRef: `fragment BranchRef on Branch { id code name }`,
+ UserFields: `fragment UserFields on User { id name email phone status roles { role branchId } lastLoginAt createdAt }`,
+ RoleFields: `fragment RoleFields on Role { id key name permissions system }`,
+ SessionFields: `fragment SessionFields on Session { id current createdAt lastSeenAt expiresAt ip userAgent }`,
+ BranchFields: `fragment BranchFields on Branch { id code name kind phone gstin stateCode active address { ...AddressFields } }`,
+ OrganizationFields: `fragment OrganizationFields on Organization {
+ id name code legalName gstin phone email address { ...AddressFields } branches { ...BranchFields }
+ }`,
+ AuditEntryFields: `fragment AuditEntryFields on AuditEntry { id action entity entityId branchId user { id name } at }`,
+
+ CatalogTestFields: `fragment CatalogTestFields on CatalogTest {
+ id name code shortCode department sampleType mrp pricePaise isProfile parameterCount
+ }`,
+
+ PatientFields: `fragment PatientFields on Patient {
+ id uhid title name sex dob ageYears phone email address { ...AddressFields } createdAt branch { ...BranchRef }
+ }`,
+ DoctorFields: `fragment DoctorFields on Doctor { id name qualification phone clinic active }`,
+ SampleFields: `fragment SampleFields on Sample { id barcode sampleType status collectedAt receivedAt rejectReason }`,
+ ReportFields: `fragment ReportFields on Report {
+ id testCode testName status version verifiedAt releasedAt updatedAt results { parameter value unit flag range }
+ }`,
+ BookingFields: `fragment BookingFields on Booking {
+ id bookingNo status notes createdAt
+ patient { id uhid name } doctor { id name } branch { ...BranchRef }
+ tests { testCode name pricePaise sampleType }
+ sampleCollection { type scheduledAt }
+ samples { ...SampleFields }
+ reports { id testCode testName status }
+ invoice { id invoiceNo status totalPaise duePaise }
+ }`,
+
+ InvoiceFields: `fragment InvoiceFields on Invoice {
+ id invoiceNo status placeOfSupply buyerGstin
+ subtotalPaise discountPaise taxPaise roundOffPaise totalPaise paidPaise creditedPaise duePaise
+ issuedAt cancelReason createdAt
+ patient { id uhid name } branch { ...BranchRef } customer { id code name }
+ items { code name category qty ratePaise discountPaise taxPercent hsnSac taxability amountPaise }
+ payments { id kind amountPaise mode reference receivedAt }
+ }`,
+
+ AccountGroupFields: `fragment AccountGroupFields on AccountGroup { id name nature affectsGrossProfit parent { id name } }`,
+ LedgerFields: `fragment LedgerFields on Ledger { id name systemKey gstin openingPaise active group { id name nature } }`,
+ VoucherFields: `fragment VoucherFields on Voucher {
+ id type voucherNo date narration status invoiceId
+ entries { ledger { id name } debitPaise creditPaise invoiceId }
+ }`,
+ StatementRowFields: `fragment StatementRowFields on StatementRow { date voucherNo voucherType debitPaise creditPaise balancePaise invoiceId }`,
+ LedgerStatementFields: `fragment LedgerStatementFields on LedgerStatement {
+ ledger { id name } openingPaise closingPaise totalDebitPaise totalCreditPaise rows { ...StatementRowFields }
+ }`,
+ GroupLineFields: `fragment GroupLineFields on GroupLine { group { id name } amountPaise ledgers { ledger { id name } amountPaise } }`,
+
+ TaxRuleFields: `fragment TaxRuleFields on TaxRule { id code category hsnSac taxability ratePercent effectiveFrom effectiveTo note }`,
+ RateTotalsFields: `fragment RateTotalsFields on RateTotals { ratePercent taxablePaise igstPaise cgstPaise sgstPaise cessPaise }`,
+ TaxTotalsFields: `fragment TaxTotalsFields on TaxTotals { taxablePaise igstPaise cgstPaise sgstPaise cessPaise }`,
+ HsnRowFields: `fragment HsnRowFields on HsnRow {
+ hsnSac ratePercent taxability qty totalValuePaise taxablePaise igstPaise cgstPaise sgstPaise cessPaise
+ }`,
+ Gstr1InvoiceFields: `fragment Gstr1InvoiceFields on Gstr1Invoice {
+ invoiceId invoiceNo day valuePaise placeOfSupply reverseCharge ctin items { ...RateTotalsFields }
+ }`,
+ Gstr1NoteFields: `fragment Gstr1NoteFields on Gstr1Note {
+ noteId noteNo day noteType invoiceNo invoiceDay valuePaise placeOfSupply ctin type items { ...RateTotalsFields }
+ }`,
+
+ CustomerFields: `fragment CustomerFields on Customer {
+ id code name kind legalName gstin stateCode pan phone email address { ...AddressFields }
+ creditLimitPaise creditDays active ledger { id name }
+ }`,
+ AgeingFields: `fragment AgeingFields on Ageing { notDue d1to30 d31to60 d61to90 d90plus }`,
+ CreditNoteFields: `fragment CreditNoteFields on CreditNote {
+ id noteNo date reason status taxablePaise taxPaise cgstPaise sgstPaise igstPaise roundOffPaise totalPaise
+ placeOfSupply buyerGstin invoice { id invoiceNo } customer { id code name }
+ lines { itemIndex name code hsnSac taxability taxPercent taxablePaise taxPaise cgstPaise sgstPaise igstPaise }
+ }`,
+
+ AgreementFields: `fragment AgreementFields on PartnerAgreement {
+ id kind shareBp rates { code sharePaise } defaultSharePaise categories effectiveFrom effectiveTo notes
+ }`,
+ PartnerFields: `fragment PartnerFields on Partner {
+ id code name kind legalName pan gstin stateCode phone email address { ...AddressFields }
+ tds { section rateBp } active branch { ...BranchRef } ledger { id name } agreements { ...AgreementFields }
+ }`,
+ SettlementLineFields: `fragment SettlementLineFields on SettlementLine {
+ invoiceId invoiceNo day code name netPaise sharePaise agreementId creditNoteId
+ }`,
+ SettlementFields: `fragment SettlementFields on Settlement {
+ id period from to status grossPaise sharePaise deductionsPaise tdsSection tdsRateBp tdsBasePaise tdsPaise netPaise
+ paidAt createdAt partner { id code name } branch { ...BranchRef }
+ lines { ...SettlementLineFields } deductions { label amountPaise ledgerId }
+ accrualVoucher { id voucherNo } payoutVoucher { id voucherNo }
+ }`,
+
+ CashClosingFields: `fragment CashClosingFields on CashClosing {
+ id day bookPaise countedPaise differencePaise denominations { valuePaise count } notes createdAt
+ branch { ...BranchRef } voucher { id voucherNo }
+ }`,
+ BankStatementFields: `fragment BankStatementFields on BankStatement {
+ id fileName from to openingPaise closingPaise lineCount createdAt branch { ...BranchRef } ledger { id name }
+ }`,
+ BankLineFields: `fragment BankLineFields on BankLine {
+ id statementId date description reference debitPaise creditPaise balancePaise status matchKind voucherIds matchedAt
+ settlement { ledgerId day grossPaise feePaise }
+ }`,
+ PeriodLockFields: `fragment PeriodLockFields on PeriodLock { id scope lockedUpTo reason updatedAt branch { ...BranchRef } }`,
+ CsvFileFields: `fragment CsvFileFields on CsvFile { fileName mimeType content }`,
+
+ DocumentFields: `fragment DocumentFields on Document { format fileName mimeType byteLength base64 html }`,
+};
+
+/** Appends every fragment the document spreads, transitively, once each. */
+export function withFragments(doc: string): string {
+ const seen = new Set();
+ const queue = [doc];
+ while (queue.length) {
+ for (const m of queue.pop()!.matchAll(/\.\.\.\s*([A-Z]\w*)/g)) {
+ const name = m[1]!;
+ if (seen.has(name) || !FRAGMENTS[name]) continue;
+ seen.add(name);
+ queue.push(FRAGMENTS[name]);
+ }
+ }
+ return [doc, ...[...seen].map((n) => FRAGMENTS[n])].join("\n");
+}
+
+export interface Operation {
+ /** The root field whose value the client returns. */
+ field: string;
+ document: string;
+}
+
+/**
+ * One root field per operation. `vars` declares the variables ("$id: ID!, $limit: Int"); each one is
+ * passed to the field under the same name. Omitted variables fall back to the schema's defaults.
+ */
+function op(kind: "query" | "mutation", field: string, vars: string, selection: string): Operation {
+ const names = [...vars.matchAll(/\$(\w+)\s*:/g)].map((m) => m[1]!);
+ const decl = vars ? `(${vars})` : "";
+ const args = names.length ? `(${names.map((n) => `${n}: $${n}`).join(", ")})` : "";
+ const name = field[0]!.toUpperCase() + field.slice(1);
+ const body = selection ? ` { ${selection} }` : "";
+ return { field, document: withFragments(`${kind} ${name}${decl} { ${field}${args}${body} }`) };
+}
+const q = (field: string, vars: string, selection = "") => op("query", field, vars, selection);
+const m = (field: string, vars: string, selection = "") => op("mutation", field, vars, selection);
+
+const PAGE = "$limit: Int, $offset: Int";
+const RANGE = "$from: DateTime!, $to: DateTime!";
+const CSV = "$bom: Boolean";
+
+export const OPERATIONS = {
+ // ---------------------------------------------------------------- system
+ health: q("health", "", "ok service version database time"),
+ ping: m("ping", ""),
+
+ // ---------------------------------------------------------------- auth
+ viewer: q("viewer", "", "user { ...UserFields } organization { ...OrganizationFields } permissions"),
+ can: {
+ field: "viewer",
+ document: `query Can($permission: String!, $branchId: ID) { viewer { can(permission: $permission, branchId: $branchId) } }`,
+ } as Operation,
+ login: m("login", "$input: LoginInput!", "token expiresAt user { ...UserFields }"),
+ logout: m("logout", ""),
+ logoutOtherSessions: m("logoutOtherSessions", ""),
+ revokeSession: m("revokeSession", "$id: ID!"),
+ changePassword: m("changePassword", "$currentPassword: String!, $newPassword: String!"),
+ requestPasswordReset: m("requestPasswordReset", "$email: String!, $orgCode: String"),
+ resetPassword: m("resetPassword", "$token: String!, $newPassword: String!"),
+ mySessions: q("mySessions", "", "...SessionFields"),
+
+ users: q("users", "", "...UserFields"),
+ user: q("user", "$id: ID!", "...UserFields"),
+ roles: q("roles", "", "...RoleFields"),
+ permissions: q("permissions", ""),
+ createUser: m("createUser", "$input: CreateUserInput!", "...UserFields"),
+ setUserRoles: m("setUserRoles", "$userId: ID!, $roles: [RoleAssignmentInput!]!", "...UserFields"),
+ disableUser: m("disableUser", "$userId: ID!", "...UserFields"),
+ enableUser: m("enableUser", "$userId: ID!", "...UserFields"),
+ setUserPassword: m("setUserPassword", "$userId: ID!, $newPassword: String!"),
+ upsertRole: m("upsertRole", "$input: RoleInput!", "...RoleFields"),
+
+ // ---------------------------------------------------------------- organisation
+ organization: q("organization", "", "...OrganizationFields"),
+ branches: q("branches", "", "...BranchFields"),
+ branch: q("branch", "$id: ID!", "...BranchFields"),
+ auditLog: q("auditLog", `$entity: String, $entityId: ID, ${PAGE}`, "...AuditEntryFields"),
+ updateOrganization: m("updateOrganization", "$input: OrganizationInput!", "...OrganizationFields"),
+ createBranch: m("createBranch", "$input: BranchInput!", "...BranchFields"),
+ updateBranch: m("updateBranch", "$id: ID!, $input: BranchPatch!", "...BranchFields"),
+
+ // ---------------------------------------------------------------- catalog (no sign-in needed)
+ catalogSearch: q(
+ "catalogSearch",
+ "$query: String!, $department: String, $sampleType: String, $limit: Int, $fuzzy: Boolean",
+ "score test { ...CatalogTestFields }",
+ ),
+ catalogTest: {
+ field: "catalogTest",
+ document: withFragments(`query CatalogTest($id: Int, $code: String, $sex: Sex) {
+ catalogTest(id: $id, code: $code) {
+ ...CatalogTestFields secondarySampleTypes
+ parameters { name unit method type options calculated formulaText referenceText range(sex: $sex) }
+ }
+}`),
+ } as Operation,
+ catalogTests: q("catalogTests", `$department: String, $sampleType: String, ${PAGE}`, "...CatalogTestFields"),
+ catalogDepartments: q("catalogDepartments", ""),
+ catalogSampleTypes: q("catalogSampleTypes", ""),
+ priceQuote: q("priceQuote", "$tests: [String!]!", "total totalPaise unknown tests { ...CatalogTestFields }"),
+
+ // ---------------------------------------------------------------- lab
+ patients: q("patients", "$search: String!, $limit: Int", "...PatientFields"),
+ patient: q("patient", "$id: ID!", "...PatientFields"),
+ registerPatient: m("registerPatient", "$input: RegisterPatientInput!", "...PatientFields"),
+ updatePatient: m("updatePatient", "$id: ID!, $input: UpdatePatientInput!", "...PatientFields"),
+
+ doctors: q("doctors", "$activeOnly: Boolean", "...DoctorFields"),
+ createDoctor: m("createDoctor", "$input: DoctorInput!", "...DoctorFields"),
+ updateDoctor: m("updateDoctor", "$id: ID!, $input: DoctorPatch!", "...DoctorFields"),
+
+ bookings: q("bookings", `$branchId: ID, $patientId: ID, $status: BookingStatus, ${PAGE}`, "...BookingFields"),
+ booking: q("booking", "$id: ID!", "...BookingFields"),
+ createBooking: m("createBooking", "$input: CreateBookingInput!", "booking { ...BookingFields } samples { ...SampleFields }"),
+ cancelBooking: m("cancelBooking", "$id: ID!", "...BookingFields"),
+
+ sample: q("sample", "$id: ID!", "...SampleFields"),
+ sampleByBarcode: q("sampleByBarcode", "$barcode: String!", "...SampleFields"),
+ collectSample: m("collectSample", "$id: ID!", "...SampleFields"),
+ receiveSample: m("receiveSample", "$id: ID!", "...SampleFields"),
+ rejectSample: m("rejectSample", "$id: ID!, $reason: String!", "...SampleFields"),
+
+ report: q("report", "$id: ID!", "...ReportFields"),
+ createReport: m("createReport", "$bookingId: ID!, $testCode: String!, $testName: String", "...ReportFields"),
+ saveResults: m("saveResults", "$reportId: ID!, $results: [ResultInput!]!", "...ReportFields"),
+ verifyReport: m("verifyReport", "$id: ID!", "...ReportFields"),
+ releaseReport: m("releaseReport", "$id: ID!", "...ReportFields"),
+ amendReport: m("amendReport", "$id: ID!", "...ReportFields"),
+
+ // ---------------------------------------------------------------- billing
+ invoices: q(
+ "invoices",
+ `$branchId: ID, $patientId: ID, $customerId: ID, $status: InvoiceStatus, ${PAGE}`,
+ "...InvoiceFields",
+ ),
+ invoice: q("invoice", "$id: ID!", "...InvoiceFields"),
+ issueInvoice: m("issueInvoice", "$input: IssueInvoiceInput!", "...InvoiceFields"),
+ recordPayment: m("recordPayment", "$invoiceId: ID!, $payment: PaymentInput!", "...InvoiceFields"),
+ refundPayment: m("refundPayment", "$invoiceId: ID!, $payment: PaymentInput!", "...InvoiceFields"),
+ cancelInvoice: m("cancelInvoice", "$id: ID!, $reason: String!", "...InvoiceFields"),
+
+ // ---------------------------------------------------------------- books
+ accountGroups: q("accountGroups", "", "...AccountGroupFields"),
+ ledgers: q("ledgers", "", "...LedgerFields"),
+ ledger: q("ledger", "$id: ID!", "...LedgerFields"),
+ voucher: q("voucher", "$id: ID!", "...VoucherFields"),
+ ledgerStatement: q("ledgerStatement", `$ledgerId: ID!, ${RANGE}, $branchId: ID`, "...LedgerStatementFields"),
+ cashBook: q("cashBook", `${RANGE}, $branchId: ID`, "...LedgerStatementFields"),
+ trialBalance: q(
+ "trialBalance",
+ "$asOf: DateTime, $branchId: ID",
+ "rows { ledger { id name } group { id name } primaryGroup { id name } debitPaise creditPaise } totalDebitPaise totalCreditPaise differencePaise",
+ ),
+ profitAndLoss: q(
+ "profitAndLoss",
+ `${RANGE}, $branchId: ID`,
+ "income { ...GroupLineFields } expenses { ...GroupLineFields } grossProfitPaise netProfitPaise",
+ ),
+ balanceSheet: q(
+ "balanceSheet",
+ "$asOf: DateTime!, $branchId: ID",
+ "assets { ...GroupLineFields } liabilities { ...GroupLineFields } profitPaise totalAssetsPaise totalLiabilitiesPaise differencePaise",
+ ),
+ dayBook: q("dayBook", `${RANGE}, $branchId: ID, $type: VoucherType`, "...VoucherFields"),
+ outstandingReceivables: q(
+ "outstandingReceivables",
+ "$asOf: DateTime, $branchId: ID",
+ "rows { invoice { id invoiceNo duePaise patient { name } } ageDays bucket } buckets { d0to30 d31to60 d61to90 d90plus } totalDuePaise",
+ ),
+ gstSummary: q("gstSummary", `${RANGE}, $branchId: ID`, "taxableValuePaise cgstPaise sgstPaise igstPaise totalTaxPaise"),
+ setupChartOfAccounts: m("setupChartOfAccounts", "", "...LedgerFields"),
+ createLedger: m("createLedger", "$input: LedgerInput!", "...LedgerFields"),
+ postVoucher: m("postVoucher", "$input: PostVoucherInput!", "...VoucherFields"),
+ cancelVoucher: m("cancelVoucher", "$id: ID!", "...VoucherFields"),
+
+ // ---------------------------------------------------------------- GST
+ taxRules: q("taxRules", "", "...TaxRuleFields"),
+ addTaxRule: m("addTaxRule", "$input: TaxRuleInput!", "...TaxRuleFields"),
+ seedTaxRules: m("seedTaxRules", "", "...TaxRuleFields"),
+ financeConfig: q(
+ "financeConfig",
+ "",
+ "b2clThresholdPaise tds { section rateBp noPanRateBp thresholdPaise } bankToleranceDays bankMaxFeeBp clearingLedgerKeys",
+ ),
+ verifyWithCA: q("verifyWithCA", ""),
+ gstr1: q(
+ "gstr1",
+ `${RANGE}, $gstin: String`,
+ `gstin period { from to }
+ b2b { ...Gstr1InvoiceFields } b2cl { ...Gstr1InvoiceFields }
+ b2cs { supplyType placeOfSupply ratePercent taxablePaise igstPaise cgstPaise sgstPaise cessPaise }
+ cdnr { ...Gstr1NoteFields } cdnur { ...Gstr1NoteFields }
+ nil { description nilRatedPaise exemptedPaise nonGstPaise }
+ hsn { b2b { ...HsnRowFields } b2c { ...HsnRowFields } }
+ docs { nature series from to total cancelled net }
+ totals { ...TaxTotalsFields } verifyWithCA`,
+ ),
+ gstr3b: q(
+ "gstr3b",
+ `${RANGE}, $gstin: String`,
+ `gstin period { from to } outwardTaxable { ...TaxTotalsFields } outwardNilExemptPaise outwardNonGstPaise
+ interStateUnregistered { placeOfSupply taxablePaise igstPaise } verifyWithCA`,
+ ),
+
+ // ---------------------------------------------------------------- receivables (B2B)
+ customers: q("customers", "$activeOnly: Boolean", "...CustomerFields"),
+ customer: q("customer", "$id: ID!", "...CustomerFields"),
+ createCustomer: m("createCustomer", "$input: CustomerInput!", "...CustomerFields"),
+ updateCustomer: m("updateCustomer", "$id: ID!, $input: CustomerPatch!", "...CustomerFields"),
+ issueCustomerInvoice: m("issueCustomerInvoice", "$input: CustomerInvoiceInput!", "...InvoiceFields"),
+ customerStatement: q(
+ "customerStatement",
+ `$customerId: ID!, ${RANGE}`,
+ `customer { ...CustomerFields } openingPaise rows { ...StatementRowFields } totalDebitPaise totalCreditPaise closingPaise
+ open { invoiceId invoiceNo day dueDay totalPaise paidPaise creditedPaise duePaise overdueDays bucket }
+ ageing { ...AgeingFields } openTotalPaise availableCreditPaise`,
+ ),
+ receivablesSummary: q(
+ "receivablesSummary",
+ "$asOf: DateTime!",
+ "rows { customer { id code name } balancePaise overduePaise ageing { ...AgeingFields } } totalPaise",
+ ),
+ creditNotes: q("creditNotes", `$customerId: ID, $invoiceId: ID, ${PAGE}`, "...CreditNoteFields"),
+ creditNote: q("creditNote", "$id: ID!", "...CreditNoteFields"),
+ issueCreditNote: m(
+ "issueCreditNote",
+ "$input: CreditNoteInput!",
+ "creditNote { ...CreditNoteFields } invoice { ...InvoiceFields } refundablePaise",
+ ),
+
+ // ---------------------------------------------------------------- partners and settlements
+ partners: q("partners", "$branchId: ID", "...PartnerFields"),
+ partner: q("partner", "$id: ID!", "...PartnerFields"),
+ createPartner: m("createPartner", "$input: PartnerInput!", "...PartnerFields"),
+ updatePartner: m("updatePartner", "$id: ID!, $input: PartnerPatch!", "...PartnerFields"),
+ addAgreement: m("addAgreement", "$input: AgreementInput!", "...AgreementFields"),
+ settlementPreview: q(
+ "settlementPreview",
+ "$partnerId: ID!, $period: String!, $deductions: [SettlementDeductionInput!]",
+ `period from to lines { ...SettlementLineFields } grossPaise sharePaise deductions { label amountPaise ledgerId }
+ deductionsPaise tds { section rateBp basePaise tdsPaise } netPaise uncoveredInvoices partner { id code name }`,
+ ),
+ settlements: q(
+ "settlements",
+ `$branchId: ID, $partnerId: ID, $status: SettlementStatus, ${PAGE}`,
+ "...SettlementFields",
+ ),
+ settlement: q("settlement", "$id: ID!", "...SettlementFields"),
+ createSettlement: m(
+ "createSettlement",
+ "$partnerId: ID!, $period: String!, $deductions: [SettlementDeductionInput!]",
+ "...SettlementFields",
+ ),
+ accrueSettlement: m("accrueSettlement", "$id: ID!", "...SettlementFields"),
+ paySettlement: m("paySettlement", "$input: PaySettlementInput!", "...SettlementFields"),
+ cancelSettlement: m("cancelSettlement", "$id: ID!", "...SettlementFields"),
+
+ // ---------------------------------------------------------------- cash and bank
+ cashClosings: q("cashClosings", `$branchId: ID!, ${PAGE}`, "...CashClosingFields"),
+ closeCashDay: m("closeCashDay", "$input: CloseCashDayInput!", "...CashClosingFields"),
+ bankStatements: q("bankStatements", `$ledgerId: ID, ${PAGE}`, "...BankStatementFields"),
+ bankLines: q(
+ "bankLines",
+ `$ledgerId: ID, $statementId: ID, $status: BankLineStatus, ${PAGE}`,
+ "...BankLineFields",
+ ),
+ importBankStatement: m(
+ "importBankStatement",
+ "$input: BankImportInput!",
+ "added skipped statement { ...BankStatementFields }",
+ ),
+ reconcileBank: m("reconcileBank", "$ledgerId: ID!, $statementId: ID", "matched unmatched settlements"),
+ matchBankLine: m("matchBankLine", "$lineId: ID!, $voucherIds: [ID!]!", "...BankLineFields"),
+ setBankLineStatus: m("setBankLineStatus", "$lineId: ID!, $status: BankLineManualStatus!", "...BankLineFields"),
+ bankReconciliation: q(
+ "bankReconciliation",
+ "$ledgerId: ID!, $asOf: DateTime!",
+ `bookPaise bankPaise paymentsNotPresentedPaise depositsNotCreditedPaise bankCreditsNotInBooksPaise
+ bankDebitsNotInBooksPaise expectedBankPaise differencePaise reconciled { lines amountPaise }
+ unmatchedLines { ...BankLineFields }
+ unreconciledEntries { id voucherId voucherType voucherNo date debitPaise creditPaise invoiceId }`,
+ ),
+
+ // ---------------------------------------------------------------- periods
+ lockDates: q("lockDates", "", "...PeriodLockFields"),
+ setLockDate: m("setLockDate", "$input: LockDateInput!", "...PeriodLockFields"),
+ closeFinancialYear: m("closeFinancialYear", "$fy: String!", "netProfitPaise vouchers { ...VoucherFields }"),
+
+ // ---------------------------------------------------------------- CSV exports
+ exportTrialBalance: q("exportTrialBalance", `$asOf: DateTime, $branchId: ID, ${CSV}`, "...CsvFileFields"),
+ exportProfitAndLoss: q("exportProfitAndLoss", `${RANGE}, $branchId: ID, ${CSV}`, "...CsvFileFields"),
+ exportBalanceSheet: q("exportBalanceSheet", `$asOf: DateTime!, $branchId: ID, ${CSV}`, "...CsvFileFields"),
+ exportDayBook: q("exportDayBook", `${RANGE}, $branchId: ID, ${CSV}`, "...CsvFileFields"),
+ exportLedger: q("exportLedger", `$ledgerId: ID!, ${RANGE}, $branchId: ID, ${CSV}`, "...CsvFileFields"),
+ exportGstr1: q("exportGstr1", `${RANGE}, $gstin: String, ${CSV}`, "...CsvFileFields"),
+ exportGstr3b: q("exportGstr3b", `${RANGE}, $gstin: String, ${CSV}`, "...CsvFileFields"),
+ exportSettlement: q("exportSettlement", `$settlementId: ID!, ${CSV}`, "...CsvFileFields"),
+
+ // ---------------------------------------------------------------- documents (PDF as base64, or HTML)
+ reportTemplates: q(
+ "reportTemplates",
+ "",
+ "templates { id name category specimen method fields { key label unit range } } profiles { id name tests }",
+ ),
+ priceList: q("priceList", "", "code name category rate taxPercent hsn"),
+ documentSamples: q("documentSamples", "", "id label group"),
+ invoiceSamples: q("invoiceSamples", "", "id label group"),
+ renderReport: q(
+ "renderReport",
+ "$request: JSON!, $format: DocumentFormat, $variant: ReportVariant, $design: ReportDesign, $fileName: String",
+ "...DocumentFields",
+ ),
+ renderInvoice: q(
+ "renderInvoice",
+ "$request: JSON!, $format: DocumentFormat, $variant: ReportVariant, $fileName: String",
+ "...DocumentFields",
+ ),
+ invoiceTotals: q("invoiceTotals", "$request: JSON!"),
+ letterhead: q("letterhead", "$format: DocumentFormat", "...DocumentFields"),
+ invoiceDocument: q(
+ "invoiceDocument",
+ "$invoiceId: ID!, $format: DocumentFormat, $variant: ReportVariant",
+ "...DocumentFields",
+ ),
+ bookingReportDocument: q(
+ "bookingReportDocument",
+ "$bookingId: ID!, $reportIds: [ID!], $format: DocumentFormat, $variant: ReportVariant, $design: ReportDesign",
+ "...DocumentFields",
+ ),
+} satisfies Record;
+
+export type OperationName = keyof typeof OPERATIONS;
+```
+
+
+---
+
+## File: src/graphql/transport.ts
+
+```ts
+// The GraphQL transport: one POST per operation to the backend's single endpoint (mgpl-diagnostics/api).
+
+export interface GraphQLClientOptions {
+ /** The endpoint: "/graphql" (same origin) or "https://api.your-lab.in/graphql". */
+ url?: string;
+ /** Custom fetch (tests, SSR, retry wrappers). */
+ fetch?: typeof fetch;
+ /** Starting session token (e.g. read from your own storage). `login` sets it; `logout` clears it. */
+ token?: string | null;
+ /** Called whenever the token changes, so the app can persist it where it wants (or nowhere). */
+ onToken?: (token: string | null) => void;
+ /** Extra headers on every request. */
+ headers?: HeadersInit | (() => HeadersInit | Promise);
+ /** e.g. "include" when the API sits behind a cookie-forwarding proxy. */
+ credentials?: RequestCredentials;
+}
+
+/** One GraphQL error as the server sends it. `code` is the stable part (see the API's docs/05-errors.md). */
+export interface GraphQLErrorItem {
+ message: string;
+ path?: (string | number)[];
+ extensions?: { code?: string; [key: string]: unknown };
+}
+
+/**
+ * Thrown when a response carries errors (or the HTTP request fails). `code` is the first error's
+ * code: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, CONFLICT, BAD_USER_INPUT, PERIOD_LOCKED, … .
+ */
+export class MgplGraphQLError extends Error {
+ readonly code: string;
+ constructor(
+ message: string,
+ readonly errors: GraphQLErrorItem[],
+ readonly status: number,
+ /** Whatever data came back next to the errors. */
+ readonly data?: unknown,
+ ) {
+ super(message);
+ this.name = "MgplGraphQLError";
+ this.code = errors[0]?.extensions?.code ?? (status >= 500 ? "INTERNAL_SERVER_ERROR" : "NETWORK_ERROR");
+ }
+}
+
+export type Variables = Record;
+
+export function createTransport(opts: GraphQLClientOptions = {}) {
+ const url = opts.url ?? "/graphql";
+ const doFetch = opts.fetch ?? ((...a: Parameters) => fetch(...a));
+ let token: string | null = opts.token ?? null;
+
+ return {
+ url,
+ getToken: () => token,
+ setToken(next: string | null) {
+ token = next;
+ opts.onToken?.(next);
+ },
+ /** Runs any operation. Throws MgplGraphQLError on errors; returns `data` otherwise. */
+ async request(query: string, variables?: Variables, init: { signal?: AbortSignal } = {}): Promise {
+ const headers = new Headers(typeof opts.headers === "function" ? await opts.headers() : opts.headers);
+ headers.set("content-type", "application/json");
+ headers.set("accept", "application/graphql-response+json, application/json");
+ if (token) headers.set("authorization", `Bearer ${token}`);
+ let res: Response;
+ try {
+ res = await doFetch(url, { method: "POST", headers, credentials: opts.credentials, body: JSON.stringify({ query, variables }), signal: init.signal });
+ } catch (e) {
+ throw new MgplGraphQLError((e as Error).message || "Network error", [], 0);
+ }
+ let body: { data?: T | null; errors?: GraphQLErrorItem[] } | undefined;
+ try {
+ body = (await res.json()) as typeof body;
+ } catch {
+ throw new MgplGraphQLError(`${res.status} ${res.statusText}`.trim(), [], res.status);
+ }
+ if (body?.errors?.length) throw new MgplGraphQLError(body.errors.map((e) => e.message).join("; "), body.errors, res.status, body.data);
+ if (!res.ok || body?.data == null) throw new MgplGraphQLError(`${res.status} ${res.statusText}`.trim(), [], res.status);
+ return body.data;
+ },
+ };
+}
+
+export type Transport = ReturnType;
+```
+
+
+---
+
+## File: src/graphql/types.ts
+
+```ts
+// What the client's operations return: the API's GraphQL types (mgpl-diagnostics/api, schema.graphql)
+// as selected by src/graphql/operations.ts. Money is integer paise (Rs 1 = 100). DateTime values are
+// ISO strings; Day values are "YYYY-MM-DD" (IST). The API's contract test validates every operation.
+
+export type ID = string;
+export type DateTime = string;
+export type Day = string;
+
+export type Sex = "male" | "female" | "other";
+export type PaymentMode = "cash" | "upi" | "card" | "netbanking" | "wallet" | "cheque" | "insurance";
+export type ItemCategory = "test" | "package" | "service" | "consumable" | "medicine" | "other";
+export type Taxability = "taxable" | "exempt" | "nil_rated" | "non_gst";
+export type VoucherType = "sales" | "receipt" | "payment" | "journal" | "contra" | "credit_note" | "debit_note" | "purchase" | "closing";
+export type BranchKind = "lab" | "collection_center";
+export type BookingStatus = "booked" | "sample_collected" | "in_process" | "completed" | "cancelled";
+export type CollectionType = "walk_in" | "home";
+export type SampleStatus = "pending" | "collected" | "received" | "rejected";
+export type ReportStatus = "draft" | "entered" | "verified" | "released" | "amended";
+/** "NONE" = within range. */
+export type ResultFlag = "L" | "H" | "A" | "NONE";
+export type InvoiceStatus = "draft" | "issued" | "partially_paid" | "paid" | "cancelled" | "refunded" | "credited";
+export type DocumentFormat = "pdf" | "html";
+export type ReportVariant = "header" | "non_header";
+export type ReportDesign = "sidebar" | "classic";
+export type CustomerKind = "hospital" | "insurer" | "tpa" | "corporate" | "other";
+export type PartnerKind = "collection_centre" | "franchise";
+export type AgreementKind = "revenue_share" | "fixed_rate";
+export type SettlementStatus = "draft" | "accrued" | "paid" | "cancelled";
+export type BankLineStatus = "unmatched" | "matched" | "ignored";
+
+export interface Address { line1?: string | null; line2?: string | null; city?: string | null; district?: string | null; state?: string | null; pincode?: string | null }
+export interface Ref { id: ID; name: string }
+export interface BranchRef { id: ID; code: string; name: string }
+
+// ------------------------------------------------------------------ auth, organisation
+
+export interface RoleAssignment { role: string; branchId: ID | null }
+export interface User { id: ID; name: string; email: string; phone: string | null; status: "active" | "disabled" | "invited"; roles: RoleAssignment[]; lastLoginAt: DateTime | null; createdAt: DateTime }
+export interface Role { id: ID; key: string; name: string; permissions: string[]; system: boolean }
+export interface Session { id: ID; current: boolean; createdAt: DateTime; lastSeenAt: DateTime; expiresAt: DateTime; ip: string | null; userAgent: string | null }
+export interface LoginResult { token: string; expiresAt: DateTime; user: User }
+export interface Branch { id: ID; code: string; name: string; kind: BranchKind; phone: string | null; gstin: string | null; stateCode: string | null; address: Address | null; active: boolean }
+export interface Organization { id: ID; name: string; code: string; legalName: string | null; gstin: string | null; phone: string | null; email: string | null; address: Address | null; branches: Branch[] }
+export interface Viewer { user: User; organization: Organization; permissions: string[] }
+export interface AuditEntry { id: ID; action: string; entity: string | null; entityId: ID | null; branchId: ID | null; user: { id: ID; name: string } | null; at: DateTime }
+export interface Health { ok: boolean; service: string; version: string; database: string; time: DateTime }
+
+// ------------------------------------------------------------------ catalog
+
+export interface CatalogParameter { name: string | null; unit: string | null; method: string | null; type: "numeric" | "text" | "options" | "image" | "title"; options: string[] | null; calculated: boolean; formulaText: string | null; referenceText: string | null; range: string }
+export interface CatalogTest { id: number; name: string; code: string | null; shortCode: string | null; department: string; sampleType: string | null; mrp: number | null; pricePaise: number | null; isProfile: boolean; parameterCount: number }
+export interface CatalogTestDetail extends CatalogTest { secondarySampleTypes: string[] | null; parameters: CatalogParameter[] }
+export interface CatalogHit { test: CatalogTest; score: number }
+export interface PriceQuote { total: number; totalPaise: number; tests: CatalogTest[]; unknown: string[] }
+
+// ------------------------------------------------------------------ lab
+
+export interface Patient { id: ID; uhid: string; title: string | null; name: string; sex: Sex; dob: DateTime | null; ageYears: number | null; phone: string | null; email: string | null; address: Address | null; createdAt: DateTime; branch: BranchRef | null }
+export interface Doctor { id: ID; name: string; qualification: string | null; phone: string | null; clinic: string | null; active: boolean }
+export interface Sample { id: ID; barcode: string; sampleType: string; status: SampleStatus; collectedAt: DateTime | null; receivedAt: DateTime | null; rejectReason: string | null }
+export interface ResultValue { parameter: string; value: string; unit: string | null; flag: ResultFlag | null; range: string | null }
+export interface Report { id: ID; testCode: string; testName: string; status: ReportStatus; version: number; verifiedAt: DateTime | null; releasedAt: DateTime | null; updatedAt: DateTime; results: ResultValue[] }
+export interface BookingTest { testCode: string; name: string; pricePaise: number; sampleType: string | null }
+export interface Booking {
+ id: ID; bookingNo: string; status: BookingStatus; notes: string | null; createdAt: DateTime;
+ patient: { id: ID; uhid: string; name: string }; doctor: Ref | null; branch: BranchRef;
+ tests: BookingTest[]; sampleCollection: { type: CollectionType; scheduledAt: DateTime | null };
+ samples: Sample[]; reports: { id: ID; testCode: string; testName: string; status: ReportStatus }[];
+ invoice: { id: ID; invoiceNo: string; status: InvoiceStatus; totalPaise: number; duePaise: number } | null;
+}
+
+// ------------------------------------------------------------------ billing
+
+export interface InvoiceItem { code: string | null; name: string; category: ItemCategory; qty: number; ratePaise: number; discountPaise: number; taxPercent: number; hsnSac: string | null; taxability: Taxability | null; amountPaise: number }
+export interface Payment { id: ID; kind: "payment" | "refund"; amountPaise: number; mode: PaymentMode; reference: string | null; receivedAt: DateTime }
+export interface Invoice {
+ id: ID; invoiceNo: string; status: InvoiceStatus; placeOfSupply: string | null; buyerGstin: string | null;
+ subtotalPaise: number; discountPaise: number; taxPaise: number; roundOffPaise: number; totalPaise: number; paidPaise: number; creditedPaise: number; duePaise: number;
+ issuedAt: DateTime | null; cancelReason: string | null; createdAt: DateTime;
+ patient: { id: ID; uhid: string; name: string }; branch: BranchRef; customer: { id: ID; code: string; name: string } | null;
+ items: InvoiceItem[]; payments: Payment[];
+}
+
+// ------------------------------------------------------------------ books
+
+export type AccountNature = "assets" | "liabilities" | "income" | "expenses";
+export interface AccountGroup { id: ID; name: string; nature: AccountNature; affectsGrossProfit: boolean; parent: { id: ID; name: string } | null }
+export interface Ledger { id: ID; name: string; systemKey: string | null; gstin: string | null; openingPaise: number; active: boolean; group: { id: ID; name: string; nature: AccountNature } }
+export interface VoucherEntry { ledger: { id: ID; name: string }; debitPaise: number; creditPaise: number; invoiceId: ID | null }
+export interface Voucher { id: ID; type: VoucherType; voucherNo: string; date: DateTime; narration: string | null; status: "posted" | "cancelled"; invoiceId: ID | null; entries: VoucherEntry[] }
+export interface StatementRow { date: DateTime; voucherNo: string; voucherType: VoucherType; debitPaise: number; creditPaise: number; balancePaise: number; invoiceId: ID | null }
+export interface LedgerStatement { ledger: { id: ID; name: string }; openingPaise: number; closingPaise: number; totalDebitPaise: number; totalCreditPaise: number; rows: StatementRow[] }
+export interface TrialBalance { rows: { ledger: { id: ID; name: string }; group: { id: ID; name: string }; primaryGroup: { id: ID; name: string }; debitPaise: number; creditPaise: number }[]; totalDebitPaise: number; totalCreditPaise: number; differencePaise: number }
+export interface GroupLine { group: { id: ID; name: string }; amountPaise: number; ledgers: { ledger: { id: ID; name: string }; amountPaise: number }[] }
+export interface ProfitAndLoss { income: GroupLine[]; expenses: GroupLine[]; grossProfitPaise: number; netProfitPaise: number }
+export interface BalanceSheet { assets: GroupLine[]; liabilities: GroupLine[]; profitPaise: number; totalAssetsPaise: number; totalLiabilitiesPaise: number; differencePaise: number }
+export interface Outstanding { rows: { invoice: { id: ID; invoiceNo: string; duePaise: number; patient: { name: string } }; ageDays: number; bucket: "D0_30" | "D31_60" | "D61_90" | "D90_PLUS" }[]; buckets: { d0to30: number; d31to60: number; d61to90: number; d90plus: number }; totalDuePaise: number }
+export interface GstSummary { taxableValuePaise: number; cgstPaise: number; sgstPaise: number; igstPaise: number; totalTaxPaise: number }
+
+// ------------------------------------------------------------------ finance
+
+export interface TaxRule { id: ID; code: string | null; category: ItemCategory | null; hsnSac: string; taxability: Taxability; ratePercent: number; effectiveFrom: DateTime; effectiveTo: DateTime | null; note: string | null }
+export interface FinanceConfig { b2clThresholdPaise: number; tds: { section: string; rateBp: number; noPanRateBp: number; thresholdPaise: number | null }; bankToleranceDays: number; bankMaxFeeBp: number; clearingLedgerKeys: string[] }
+export interface TaxTotals { taxablePaise: number; igstPaise: number; cgstPaise: number; sgstPaise: number; cessPaise: number }
+export interface RateTotals extends TaxTotals { ratePercent: number }
+export interface Gstr1Invoice { invoiceId: ID; invoiceNo: string; day: Day; valuePaise: number; placeOfSupply: string; reverseCharge: string; ctin: string | null; items: RateTotals[] }
+export interface Gstr1Note { noteId: ID; noteNo: string; day: Day; noteType: string; invoiceNo: string; invoiceDay: Day; valuePaise: number; placeOfSupply: string; ctin: string | null; type: string | null; items: RateTotals[] }
+export interface HsnRow extends TaxTotals { hsnSac: string; ratePercent: number; taxability: Taxability; qty: number; totalValuePaise: number }
+export interface Gstr1 {
+ gstin: string; period: { from: DateTime; to: DateTime };
+ b2b: Gstr1Invoice[]; b2cl: Gstr1Invoice[];
+ b2cs: (TaxTotals & { supplyType: string; placeOfSupply: string; ratePercent: number })[];
+ cdnr: Gstr1Note[]; cdnur: Gstr1Note[];
+ nil: { description: string; nilRatedPaise: number; exemptedPaise: number; nonGstPaise: number }[];
+ hsn: { b2b: HsnRow[]; b2c: HsnRow[] };
+ docs: { nature: string; series: string; from: string; to: string; total: number; cancelled: number; net: number }[];
+ totals: TaxTotals; verifyWithCA: string[];
+}
+export interface Gstr3b { gstin: string; period: { from: DateTime; to: DateTime }; outwardTaxable: TaxTotals; outwardNilExemptPaise: number; outwardNonGstPaise: number; interStateUnregistered: { placeOfSupply: string; taxablePaise: number; igstPaise: number }[]; verifyWithCA: string[] }
+
+export interface Customer { id: ID; code: string; name: string; kind: CustomerKind; legalName: string | null; gstin: string | null; stateCode: string | null; pan: string | null; phone: string | null; email: string | null; address: Address | null; creditLimitPaise: number | null; creditDays: number; active: boolean; ledger: { id: ID; name: string } }
+export interface Ageing { notDue: number; d1to30: number; d31to60: number; d61to90: number; d90plus: number }
+export interface OpenItem { invoiceId: ID; invoiceNo: string; day: Day; dueDay: Day; totalPaise: number; paidPaise: number; creditedPaise: number; duePaise: number; overdueDays: number; bucket: "not_due" | "D1_30" | "D31_60" | "D61_90" | "D90_PLUS" }
+export interface CustomerStatement { customer: Customer; openingPaise: number; rows: StatementRow[]; totalDebitPaise: number; totalCreditPaise: number; closingPaise: number; open: OpenItem[]; ageing: Ageing; openTotalPaise: number; availableCreditPaise: number | null }
+export interface ReceivablesSummary { rows: { customer: { id: ID; code: string; name: string }; balancePaise: number; overduePaise: number; ageing: Ageing }[]; totalPaise: number }
+export interface CreditNoteLine { itemIndex: number; name: string; code: string | null; hsnSac: string | null; taxability: Taxability | null; taxPercent: number; taxablePaise: number; taxPaise: number; cgstPaise: number; sgstPaise: number; igstPaise: number }
+export interface CreditNote { id: ID; noteNo: string; date: DateTime; reason: string; status: string; taxablePaise: number; taxPaise: number; cgstPaise: number; sgstPaise: number; igstPaise: number; roundOffPaise: number; totalPaise: number; placeOfSupply: string | null; buyerGstin: string | null; invoice: { id: ID; invoiceNo: string }; customer: { id: ID; code: string; name: string } | null; lines: CreditNoteLine[] }
+
+export interface PartnerAgreement { id: ID; kind: AgreementKind; shareBp: number | null; rates: { code: string; sharePaise: number }[] | null; defaultSharePaise: number | null; categories: ItemCategory[] | null; effectiveFrom: DateTime; effectiveTo: DateTime | null; notes: string | null }
+export interface Partner { id: ID; code: string; name: string; kind: PartnerKind; legalName: string | null; pan: string | null; gstin: string | null; stateCode: string | null; phone: string | null; email: string | null; address: Address | null; tds: { section: string; rateBp: number } | null; active: boolean; branch: BranchRef; ledger: { id: ID; name: string }; agreements: PartnerAgreement[] }
+export interface SettlementLine { invoiceId: ID; invoiceNo: string; day: Day; code: string | null; name: string; netPaise: number; sharePaise: number; agreementId: ID; creditNoteId: ID | null }
+export interface SettlementDeduction { label: string; amountPaise: number; ledgerId?: ID | null }
+export interface SettlementPreview { period: string; from: DateTime; to: DateTime; lines: SettlementLine[]; grossPaise: number; sharePaise: number; deductions: SettlementDeduction[]; deductionsPaise: number; tds: { section: string | null; rateBp: number; basePaise: number; tdsPaise: number }; netPaise: number; uncoveredInvoices: string[]; partner: { id: ID; code: string; name: string } }
+export interface Settlement { id: ID; period: string; from: DateTime; to: DateTime; status: SettlementStatus; grossPaise: number; sharePaise: number; deductionsPaise: number; tdsSection: string | null; tdsRateBp: number; tdsBasePaise: number; tdsPaise: number; netPaise: number; paidAt: DateTime | null; createdAt: DateTime; partner: { id: ID; code: string; name: string }; branch: BranchRef; lines: SettlementLine[]; deductions: SettlementDeduction[]; accrualVoucher: { id: ID; voucherNo: string } | null; payoutVoucher: { id: ID; voucherNo: string } | null }
+
+export interface CashClosing { id: ID; day: Day; bookPaise: number; countedPaise: number; differencePaise: number; denominations: { valuePaise: number; count: number }[] | null; notes: string | null; createdAt: DateTime; branch: BranchRef; voucher: { id: ID; voucherNo: string } | null }
+export interface BankStatement { id: ID; fileName: string | null; from: DateTime | null; to: DateTime | null; openingPaise: number | null; closingPaise: number | null; lineCount: number; createdAt: DateTime; branch: BranchRef; ledger: { id: ID; name: string } }
+export interface BankLine { id: ID; statementId: ID; date: DateTime; description: string; reference: string | null; debitPaise: number; creditPaise: number; balancePaise: number | null; status: BankLineStatus; matchKind: string | null; voucherIds: ID[]; matchedAt: DateTime | null; settlement: { ledgerId: ID; day: Day; grossPaise: number; feePaise: number } | null }
+export interface BankReconciliation { bookPaise: number; bankPaise: number | null; paymentsNotPresentedPaise: number; depositsNotCreditedPaise: number; bankCreditsNotInBooksPaise: number; bankDebitsNotInBooksPaise: number; expectedBankPaise: number; differencePaise: number | null; reconciled: { lines: number; amountPaise: number }; unmatchedLines: BankLine[]; unreconciledEntries: { id: ID; voucherId: ID; voucherType: VoucherType; voucherNo: string; date: DateTime; debitPaise: number; creditPaise: number; invoiceId: ID | null }[] }
+export interface PeriodLock { id: ID; scope: string; lockedUpTo: DateTime | null; reason: string | null; updatedAt: DateTime; branch: BranchRef | null }
+export interface CsvFile { fileName: string; mimeType: string; content: string }
+
+// ------------------------------------------------------------------ documents
+
+/** A rendered PDF (base64) or self-contained HTML. Turn a PDF into a Blob with documentBlob(). */
+export interface RenderedDocument { format: DocumentFormat; fileName: string; mimeType: string; byteLength: number; base64: string | null; html: string | null }
+export interface ReportTemplateInfo { id: string; name: string; category: string | null; specimen: string | null; method: string | null; fields: { key: string; label: string; unit: string | null; range: string[] | null }[] }
+export interface ReportTemplateCatalog { templates: ReportTemplateInfo[]; profiles: { id: string; name: string; tests: string[] }[] }
+export interface PriceListEntry { code: string; name: string; category: string; rate: number; taxPercent: number | null; hsn: string | null }
+
+// ------------------------------------------------------------------ inputs
+
+export interface RoleAssignmentInput { role: string; branchId?: ID | null }
+export interface CreateUserInput { name: string; email: string; phone?: string; password: string; roles: RoleAssignmentInput[] }
+export interface BranchInput { code: string; name: string; kind: BranchKind; phone?: string; gstin?: string; stateCode?: string; address?: Address; active?: boolean }
+export interface RegisterPatientInput { branchId: ID; title?: string; name: string; sex: Sex; dob?: DateTime; ageYears?: number; phone?: string; email?: string; address?: Address }
+export type UpdatePatientInput = Partial>;
+export interface DoctorInput { name: string; qualification?: string; phone?: string; clinic?: string; active?: boolean }
+/** A catalog code alone is enough: name, price and sample type come from the test catalog. */
+export interface BookingTestInput { testCode: string; name?: string; pricePaise?: number; sampleType?: string }
+export interface CreateBookingInput { branchId: ID; patientId: ID; doctorId?: ID; tests: BookingTestInput[]; collectionType?: CollectionType; scheduledAt?: DateTime; notes?: string }
+/** Unit, range and flag are filled from the catalog when omitted. */
+export interface ResultInput { parameter: string; value: string; unit?: string; flag?: ResultFlag; range?: string }
+/** A catalog code alone is enough: name and MRP come from the test catalog. */
+export interface InvoiceItemInput { code?: string; name?: string; category?: ItemCategory; qty?: number; ratePaise?: number; discountPaise?: number; taxPercent?: number; hsnSac?: string; taxability?: Taxability }
+export interface PaymentInput { amountPaise: number; mode: PaymentMode; reference?: string }
+export interface IssueInvoiceInput { branchId: ID; patientId: ID; bookingId?: ID; items: InvoiceItemInput[]; billDiscountPaise?: number; payments?: PaymentInput[]; applyTaxRules?: boolean }
+export interface PostVoucherInput { branchId: ID; type: VoucherType; date?: DateTime; narration?: string; entries: { ledgerId: ID; debitPaise?: number; creditPaise?: number; invoiceId?: ID }[] }
+export interface LedgerInput { name: string; groupId: ID; openingPaise?: number; gstin?: string }
+export interface TaxRuleInput { code?: string; category?: ItemCategory; hsnSac: string; taxability: Taxability; ratePercent: number; effectiveFrom: Day; note?: string }
+export interface CustomerInput { code: string; name: string; kind: CustomerKind; legalName?: string; gstin?: string; stateCode?: string; pan?: string; phone?: string; email?: string; address?: Address; creditLimitPaise?: number | null; creditDays?: number }
+export type CustomerPatch = Partial> & { active?: boolean };
+export interface CustomerInvoiceInput extends Omit { customerId: ID; placeOfSupply?: string; applyTaxRules?: boolean }
+export interface CreditNoteInput { invoiceId: ID; reason: string; lines?: { itemIndex: number; taxablePaise?: number }[]; date?: DateTime }
+export interface PartnerInput { code: string; name: string; kind: PartnerKind; branchId: ID; legalName?: string; pan?: string; gstin?: string; stateCode?: string; phone?: string; email?: string; address?: Address; tds?: { section: string; rateBp: number } }
+export type PartnerPatch = Partial> & { tds?: { section: string; rateBp: number } | null; active?: boolean };
+export interface AgreementInput { partnerId: ID; kind: AgreementKind; shareBp?: number; rates?: { code: string; sharePaise: number }[]; defaultSharePaise?: number; categories?: ItemCategory[]; effectiveFrom: Day; effectiveTo?: Day; notes?: string }
+export interface CloseCashDayInput { branchId: ID; day: Day; countedPaise: number; denominations?: { valuePaise: number; count: number }[]; notes?: string }
+export interface BankImportInput { branchId: ID; ledgerId: ID; csv: string; fileName?: string }
+export interface LockDateInput { branchId?: ID; upTo: Day | null; reason?: string }
+export interface RenderOptions { format?: DocumentFormat; variant?: ReportVariant; design?: ReportDesign; fileName?: string }
+
+// ------------------------------------------------------------------ mutation results
+
+export interface CreateBookingResult { booking: Booking; samples: Sample[] }
+export interface CreditNoteResult { creditNote: CreditNote; invoice: Invoice; refundablePaise: number }
+export interface BankImportResult { added: number; skipped: number; statement: BankStatement }
+export interface ReconcileResult { matched: number; unmatched: number; settlements: number }
+export interface YearEndResult { netProfitPaise: number; vouchers: Voucher[] }
+export interface DocumentSample { id: string; label: string; group: string | null }
+export interface LoginInput { email: string; password: string; orgCode?: string }
+export interface RoleInput { key: string; name: string; permissions: string[] }
+export type OrganizationInput = Partial> & { address?: Address };
+export type BranchPatch = Partial>;
+export type DoctorPatch = Partial;
+export interface SettlementDeductionInput { label: string; amountPaise: number; ledgerId?: ID }
+export interface PaySettlementInput { settlementId: ID; mode: PaymentMode; reference?: string; date?: DateTime }
+```
+
+
---
## File: src/index.ts
@@ -491,6 +1970,21 @@ import type {
export * from "./types";
+// The GraphQL SDK for mgpl-diagnostics/api (one endpoint for everything). Its types are under `Gql`
+// here, or flat in "@mgpl-diagnostics/client/graphql".
+export {
+ createMgplGraphQLClient,
+ documentBlob,
+ csvBlob,
+ createTransport,
+ MgplGraphQLError,
+ OPERATIONS,
+ FRAGMENTS,
+ withFragments,
+} from "./graphql/index";
+export type { MgplGraphQLClient, GraphQLClientOptions, GraphQLErrorItem, Operation, OperationName } from "./graphql/index";
+export type * as Gql from "./graphql/types";
+
/** Default mount point of the backend */
export const DEFAULT_BASE_URL = "/api/mgpl";
diff --git a/llms.txt b/llms.txt
index 448349e..4cc3516 100644
--- a/llms.txt
+++ b/llms.txt
@@ -3,6 +3,7 @@
> Headless, zero-dependency TypeScript SDK for browsers (or any fetch runtime) that talks to the MGPL Diagnostics backend (`createMgplApi` / `createReportApi`). It requests lab reports and invoices as PDF Blobs, HTML strings or JSON, and fetches the test catalog and price list. It has no UI and no CSS; apps build their own UI/UX. Optional unstyled React hooks live in "@mgpl-diagnostics/client/react".
Key facts for agents:
+- GraphQL (mgpl-diagnostics/api, one endpoint for everything): `const mgpl = createMgplGraphQLClient({ url: "/graphql", token?, onToken? })`; `await mgpl.auth.login({ email, password })` keeps the token. Namespaces: auth, users, organization, catalog, patients, doctors, bookings, samples, reports, invoices, books, gst, receivables, settlements, cash, bank, periods, exports, documents; `request(query, vars)` for custom selections. Errors throw `MgplGraphQLError` with `code`. Money in paise; documents are base64 PDFs or HTML (`documentBlob`). Types flat in "@mgpl-diagnostics/client/graphql" or under `Gql`.
- `const mgpl = createMgplClient({ baseUrl: "/api/mgpl", headers?, credentials?, fetch? })`.
- Reports: `mgpl.reports.templates()`, `.pdf(req)`, `.html(req)`, `.letterheadPdf()`, `.samples()`. Invoices: `mgpl.invoices.totals(invoice)`, `.pdf(req)`, `.html(req)`, `.priceList()`.
- Build entry forms from `templates()`: each template lists `fields` (key, label, unit, range); send `{ patient, tests: [{ template, results: { [key]: value } }] }`.
@@ -13,6 +14,7 @@ Key facts for agents:
## Docs
- [README](README.md): why, quick start, SDK table, React hooks, errors and auth
+- [GraphQL client](docs/graphql.md): every method, the operation it runs, errors, conventions
- [API reference](docs/api.md): every method, option and type
- [AGENTS.md](AGENTS.md): rules (no deps, no UI, mirrored contract)
- [llms-full.txt](llms-full.txt): everything above plus the source
@@ -20,5 +22,8 @@ Key facts for agents:
## Source
- [src/index.ts](src/index.ts): createMgplClient, helpers
+- [src/graphql/client.ts](src/graphql/client.ts): createMgplGraphQLClient, documentBlob, csvBlob
+- [src/graphql/operations.ts](src/graphql/operations.ts): fragments and operations
+- [src/graphql/types.ts](src/graphql/types.ts): GraphQL result and input types
- [src/react.tsx](src/react.tsx): hooks and HtmlFrame
- [src/types.ts](src/types.ts): contract types
diff --git a/package.json b/package.json
index b86a9d0..5466b22 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@mgpl-diagnostics/client",
- "version": "1.0.0",
- "description": "Headless browser SDK for the MGPL Diagnostics backend (reports + invoices). Zero dependencies, no UI — build your own.",
+ "version": "1.1.0",
+ "description": "Headless SDK for the MGPL Diagnostics backend: the GraphQL API (everything) and the report/invoice HTTP API. Zero dependencies, no UI \u2014 build your own.",
"type": "module",
"license": "SEE LICENSE IN LICENSE",
"repository": "github:mgpl-diagnostics/client",
@@ -13,6 +13,10 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
+ "./graphql": {
+ "types": "./dist/graphql.d.ts",
+ "import": "./dist/graphql.js"
+ },
"./react": {
"types": "./dist/react.d.ts",
"import": "./dist/react.js"
diff --git a/src/graphql/client.ts b/src/graphql/client.ts
new file mode 100644
index 0000000..6783874
--- /dev/null
+++ b/src/graphql/client.ts
@@ -0,0 +1,295 @@
+// createMgplGraphQLClient: one typed method per operation of the backend's single GraphQL endpoint
+// (mgpl-diagnostics/api, POST /graphql). Zero dependencies: operations are plain strings.
+import type { InvoiceRequest, InvoiceTotals, ReportRequest } from "../types";
+import { OPERATIONS, type Operation } from "./operations";
+import { createTransport, type GraphQLClientOptions, type Variables } from "./transport";
+import type * as T from "./types";
+
+type Page = { limit?: number; offset?: number };
+type Range = { from: T.DateTime; to: T.DateTime };
+type Csv = { bom?: boolean };
+
+export function createMgplGraphQLClient(opts: GraphQLClientOptions = {}) {
+ const transport = createTransport(opts);
+
+ /** Runs one operation and returns its root field. */
+ async function run(operation: Operation, variables?: Variables, signal?: AbortSignal): Promise {
+ const data = await transport.request>(operation.document, variables, { signal });
+ return data[operation.field] as R;
+ }
+ const O = OPERATIONS;
+
+ return {
+ url: transport.url,
+ /** Any query or mutation, for what the typed methods don't cover (e.g. a custom selection). */
+ request: transport.request,
+ getToken: transport.getToken,
+ setToken: transport.setToken,
+
+ health: () => run(O.health),
+ ping: () => run(O.ping),
+
+ auth: {
+ /** Signs in and keeps the session token for every later call (see `onToken` to persist it). */
+ async login(input: T.LoginInput): Promise {
+ const result = await run(O.login, { input });
+ transport.setToken(result.token);
+ return result;
+ },
+ /** Ends this session on the server and forgets the token. */
+ async logout(): Promise {
+ try {
+ if (transport.getToken()) await run(O.logout);
+ } finally {
+ transport.setToken(null);
+ }
+ },
+ /** The signed-in user, organisation and permissions; null when the token is missing or expired. */
+ viewer: () => run(O.viewer),
+ /** Whether the signed-in user may do `permission` (at `branchId`, when given). */
+ can: async (permission: string, branchId?: T.ID) =>
+ (await run<{ can: boolean } | null>(O.can, { permission, branchId }))?.can ?? false,
+ sessions: () => run(O.mySessions),
+ logoutOtherSessions: () => run(O.logoutOtherSessions),
+ revokeSession: (id: T.ID) => run(O.revokeSession, { id }),
+ changePassword: (currentPassword: string, newPassword: string) =>
+ run(O.changePassword, { currentPassword, newPassword }),
+ /** Always resolves true; the server emails the link if the account exists. */
+ requestPasswordReset: (email: string, orgCode?: string) => run(O.requestPasswordReset, { email, orgCode }),
+ resetPassword: (token: string, newPassword: string) => run(O.resetPassword, { token, newPassword }),
+ },
+
+ users: {
+ list: () => run(O.users),
+ get: (id: T.ID) => run(O.user, { id }),
+ create: (input: T.CreateUserInput) => run(O.createUser, { input }),
+ setRoles: (userId: T.ID, roles: T.RoleAssignmentInput[]) => run(O.setUserRoles, { userId, roles }),
+ disable: (userId: T.ID) => run(O.disableUser, { userId }),
+ enable: (userId: T.ID) => run(O.enableUser, { userId }),
+ setPassword: (userId: T.ID, newPassword: string) => run(O.setUserPassword, { userId, newPassword }),
+ roles: () => run(O.roles),
+ upsertRole: (input: T.RoleInput) => run(O.upsertRole, { input }),
+ /** Every permission name the API checks. */
+ permissions: () => run(O.permissions),
+ },
+
+ organization: {
+ get: () => run(O.organization),
+ update: (input: T.OrganizationInput) => run(O.updateOrganization, { input }),
+ branches: () => run(O.branches),
+ branch: (id: T.ID) => run(O.branch, { id }),
+ createBranch: (input: T.BranchInput) => run(O.createBranch, { input }),
+ updateBranch: (id: T.ID, input: T.BranchPatch) => run(O.updateBranch, { id, input }),
+ auditLog: (filter: { entity?: string; entityId?: T.ID } & Page = {}) => run(O.auditLog, filter),
+ },
+
+ /** The test catalog. No sign-in needed. */
+ catalog: {
+ search: (query: string, o: { department?: string; sampleType?: string; limit?: number; fuzzy?: boolean } = {}) =>
+ run(O.catalogSearch, { query, ...o }),
+ /** By numeric id or code ("CBC", "LFT"); `sex` picks the reference range. */
+ test: (idOrCode: number | string, sex?: T.Sex) =>
+ run(O.catalogTest, typeof idOrCode === "number" ? { id: idOrCode, sex } : { code: idOrCode, sex }),
+ tests: (filter: { department?: string; sampleType?: string } & Page = {}) => run(O.catalogTests, filter),
+ departments: () => run(O.catalogDepartments),
+ sampleTypes: () => run(O.catalogSampleTypes),
+ priceQuote: (tests: string[]) => run(O.priceQuote, { tests }),
+ },
+
+ patients: {
+ /** Name, phone or UHID. */
+ search: (search: string, limit?: number) => run(O.patients, { search, limit }),
+ get: (id: T.ID) => run(O.patient, { id }),
+ register: (input: T.RegisterPatientInput) => run(O.registerPatient, { input }),
+ update: (id: T.ID, input: T.UpdatePatientInput) => run(O.updatePatient, { id, input }),
+ },
+
+ doctors: {
+ list: (activeOnly?: boolean) => run(O.doctors, { activeOnly }),
+ create: (input: T.DoctorInput) => run(O.createDoctor, { input }),
+ update: (id: T.ID, input: T.DoctorPatch) => run(O.updateDoctor, { id, input }),
+ },
+
+ bookings: {
+ list: (filter: { branchId?: T.ID; patientId?: T.ID; status?: T.BookingStatus } & Page = {}) =>
+ run(O.bookings, filter),
+ get: (id: T.ID) => run(O.booking, { id }),
+ /** Creates the booking and one sample per sample type. */
+ create: (input: T.CreateBookingInput) => run(O.createBooking, { input }),
+ cancel: (id: T.ID) => run(O.cancelBooking, { id }),
+ },
+
+ samples: {
+ get: (id: T.ID) => run(O.sample, { id }),
+ byBarcode: (barcode: string) => run(O.sampleByBarcode, { barcode }),
+ collect: (id: T.ID) => run(O.collectSample, { id }),
+ receive: (id: T.ID) => run(O.receiveSample, { id }),
+ reject: (id: T.ID, reason: string) => run(O.rejectSample, { id, reason }),
+ },
+
+ reports: {
+ get: (id: T.ID) => run(O.report, { id }),
+ create: (bookingId: T.ID, testCode: string, testName?: string) =>
+ run(O.createReport, { bookingId, testCode, testName }),
+ /** Unit, range and flag are filled from the catalog when omitted. */
+ saveResults: (reportId: T.ID, results: T.ResultInput[]) => run(O.saveResults, { reportId, results }),
+ verify: (id: T.ID) => run(O.verifyReport, { id }),
+ release: (id: T.ID) => run(O.releaseReport, { id }),
+ amend: (id: T.ID) => run(O.amendReport, { id }),
+ },
+
+ invoices: {
+ list: (filter: { branchId?: T.ID; patientId?: T.ID; customerId?: T.ID; status?: T.InvoiceStatus } & Page = {}) =>
+ run(O.invoices, filter),
+ get: (id: T.ID) => run(O.invoice, { id }),
+ /** Items can be catalog codes alone; name and MRP come from the catalog. Posts to the books. */
+ issue: (input: T.IssueInvoiceInput) => run(O.issueInvoice, { input }),
+ recordPayment: (invoiceId: T.ID, payment: T.PaymentInput) => run(O.recordPayment, { invoiceId, payment }),
+ refund: (invoiceId: T.ID, payment: T.PaymentInput) => run(O.refundPayment, { invoiceId, payment }),
+ cancel: (id: T.ID, reason: string) => run(O.cancelInvoice, { id, reason }),
+ },
+
+ books: {
+ groups: () => run(O.accountGroups),
+ ledgers: () => run(O.ledgers),
+ ledger: (id: T.ID) => run(O.ledger, { id }),
+ voucher: (id: T.ID) => run(O.voucher, { id }),
+ ledgerStatement: (ledgerId: T.ID, range: Range, branchId?: T.ID) =>
+ run(O.ledgerStatement, { ledgerId, ...range, branchId }),
+ cashBook: (range: Range, branchId?: T.ID) => run(O.cashBook, { ...range, branchId }),
+ trialBalance: (o: { asOf?: T.DateTime; branchId?: T.ID } = {}) => run(O.trialBalance, o),
+ profitAndLoss: (range: Range, branchId?: T.ID) => run(O.profitAndLoss, { ...range, branchId }),
+ balanceSheet: (asOf: T.DateTime, branchId?: T.ID) => run(O.balanceSheet, { asOf, branchId }),
+ dayBook: (range: Range, o: { branchId?: T.ID; type?: T.VoucherType } = {}) => run(O.dayBook, { ...range, ...o }),
+ outstandingReceivables: (o: { asOf?: T.DateTime; branchId?: T.ID } = {}) => run(O.outstandingReceivables, o),
+ gstSummary: (range: Range, branchId?: T.ID) => run(O.gstSummary, { ...range, branchId }),
+ /** Seeds the standard chart and finance ledgers. Safe to repeat. */
+ setupChartOfAccounts: () => run(O.setupChartOfAccounts),
+ createLedger: (input: T.LedgerInput) => run(O.createLedger, { input }),
+ postVoucher: (input: T.PostVoucherInput) => run(O.postVoucher, { input }),
+ cancelVoucher: (id: T.ID) => run(O.cancelVoucher, { id }),
+ },
+
+ gst: {
+ taxRules: () => run(O.taxRules),
+ addTaxRule: (input: T.TaxRuleInput) => run(O.addTaxRule, { input }),
+ /** finance-kit's starting rules. Every rate must be verified with the lab's CA. */
+ seedTaxRules: () => run(O.seedTaxRules),
+ config: () => run(O.financeConfig),
+ verifyWithCA: () => run(O.verifyWithCA),
+ gstr1: (range: Range, gstin?: string) => run(O.gstr1, { ...range, gstin }),
+ gstr3b: (range: Range, gstin?: string) => run