Commit graph

388 commits

Author SHA1 Message Date
742010a9a9 chore(login): simplify to login-only, remove register/reset tabs
The /login page previously had three tabs (Login / Register / Reset
Password) wired through useAuth(). The Register and Reset handlers
were removed in this pass — only the Login form remains, plus a
small UI/styling cleanup that was collateral.

- Drop Tabs imports + the active-tab state plumbing
- Drop regName/regEmail/regPassword/regConfirm + resetEmail state
- Drop handleRegister + handleReset handlers (login-only now)
- Drop useAuth().register / useAuth().requestPasswordReset destructuring
- Trim the local stylesheet comments that referenced the Base UI
  tabs triggers (no longer applicable)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 02:11:28 +02:00
bd328d40dc feat(deliveries): enrich DN row subline with customer/order/paperclip
The /deliveries To Load + Back from Driver + Checked In tabs each
render delivery-note rows showing only 'DN {number}' + a one-line
status subline. Operators couldn't identify the customer or tell
whether the signed copy was back without scrolling up to the
customer/order group header.

Replace the existing subline with a compact pieces list:
customer name (truncated, with title tooltip) · company name ·
parent order number · signed-copy indicator (paperclip icon +
filenames in title=) · delivery date (if set). Counter-pickup DNs
(no parent order) collapse to 'Counter pickup' in italic.

All data is already on the row payload from /api/v1/deliveries —
no server or schema changes.

Also removed the now-unused 'subline' const that the prior
implementation used for the old single-line status subline.

Verified visually via dev server: counter-pickup fallback renders
correctly ('Counter pickup' italic). Order-attached and signed-copy
code paths are structurally correct per the spec — would need a
DN with order + signed files in the live data to exercise them,
but no such DN was visible in the current dev data.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 02:09:49 +02:00
dc8436e832 feat(orders): surface order age as colour-coded badge
Replaces the bare formatDate(order.created) string in the order card
metadata slot with an OrderAgeBadge component. The badge shows 'today'
or 'Xd old' with colour coding by bucket (green/amber/orange/red).
The absolute date is preserved in the badge's title= tooltip.

Bucket math lives in src/lib/order-age.ts (covered by 14 unit tests).
This change is rendering-only — no server or schema impact.

Note: dev-server smoke check was blocked by a pre-existing data bug
where the /api/v1/orders/list endpoint returns 0 orders across all tabs
for both admin and david.els users, despite the tab counts showing
22/18/14/17/168/0 — investigated, unrelated to this change. The
OrderAgeBadge will render correctly once that bug is fixed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 02:05:07 +02:00
27efc8628c feat(dashboard2): show last month's selling value alongside the delta
Selling card now reads "+12% vs previous · R 200,000" instead of just
"+12% vs previous". Optional previousValue prop on RepKpiCard; the other
3 cards don't pass it, so their format is unchanged.
2026-09-01 02:04:25 +02:00
42916f2948 fix(dashboard2): surface PocketBase error details in /api/v1/rep-dashboard
PB SDK rejects filters with a generic "Failed to fetch." wrapper, but the
real reason lives in error.response.data.message (or .data.message).
Walk the chain so the page's inline error UI shows the actual PB message
instead of a useless generic toast. Also log the full error server-side.
2026-09-01 02:00:55 +02:00
f84d14fe4d fix(dashboard2): show error inline instead of getting stuck on Loading
- Track explicit `error` state; render an inline error panel with a Try-again button when the fetch fails and we have no data to display (previously the toast fired but the page sat on the Loading… screen forever)
- Add `content-type: application/json` header (matches /dashboard convention)
- Switch gate from `loading || !data` to `loading && !data` so we don't hide data once it's loaded
- Defer the initial fetch via setTimeout(0) and only start the realtime subscriptions AFTER the first successful load, so a slow first fetch doesn't race with realtime events
2026-09-01 01:58:38 +02:00
c9096dfd84 feat(dashboard2): trim Row 1 to Selling-only with last-month delta
- Drop Cost and Markup gauge — Selling is the headline the rep cares about
- monthRevenue is now {sellingThisMonth, sellingLastMonth}
- One combined PB query covers both months; classify each row by date in code so the delta costs zero extra round-trips
- RepKpiCard gains a format="currency" prop so Selling renders ZAR-formatted with a delta arrow (mirrors the quote-count cards)
- Row 1 grid adjusts from 6-col to 4-col to match the new card count
2026-09-01 01:55:52 +02:00
f7938375f6 feat(dashboard2): move Selling/Cost/Markup up to Row 1 alongside quote KPIs
All 6 headline numbers now sit on a single row at the top (grid-cols-1 / sm:grid-cols-3 / lg:grid-cols-6). Gauge is shrunk to size=140 so it fits a 6-col grid; sublabel trimmed to single-line currency pair so it doesn't wrap inside the narrow card.
2026-09-01 01:52:22 +02:00
9abc7d4d03 feat(dashboard2): split Selling/Cost into KPI cards, parallelize data fetches
- Row 3 now shows separate Selling + Cost KPI cards (ZAR-formatted) plus the Markup gauge
- Outstanding-quotes, outstanding-order-items probe, and month-revenue queries now run in parallel via Promise.all (was sequential, costing ~300ms)
- Outstanding-order-items probe capped at 200 rows so a single rep's data stays in a single PB round-trip instead of paginating
2026-09-01 01:49:57 +02:00
23050e3dc6 feat: add /dashboard2 rep-scoped dashboard page 2026-09-01 01:36:53 +02:00
afb396b03b feat: add /api/v1/rep-dashboard route 2026-09-01 01:36:35 +02:00
9cf8e39b65 test(invoicing): mock useAuth to keep rep-filter a no-op in tests 2026-08-31 23:38:06 +02:00
ed07617827 feat(delivery-notes): filter list to the logged-in rep's own notes 2026-08-31 23:28:02 +02:00
29eb003cd6 feat(invoicing): filter rows to the logged-in rep's own orders 2026-08-31 23:27:23 +02:00
8faa4437b6 feat(quotes): filter list to the logged-in rep's own quotes 2026-08-31 23:26:42 +02:00
defa481661 feat(orders): filter list to the logged-in rep's own orders 2026-08-31 23:25:44 +02:00
392a76fef7 fix(boss-dashboard): show raw claim count + fix stale window on tab switch
"Claimed" was gated to emailed/approved/sent quotes, undercounting reps
who claim a lot but haven't pushed everything out yet (e.g. Karin showed
1 instead of her actual 4 claimed today). Swap in quotesLoaded so it
reflects total quotes processed, any status.

Also fixes tab switching (Today/Week/Month) needing a manual refresh to
show the right numbers: selectWindow() called fetchData() before the
activeWindowRef mirroring effect had run, so the fetch used the
previous tab's window. Now set synchronously in the click handler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:38:41 +02:00
e1f14d9c48 fix(boss-dashboard): make /m installable as a PWA
manifest-mobile.json had an empty icons array, which blocks Chrome's
install prompt entirely. Point it at the existing android-chrome
192/512 icons and override appleWebApp.title to "Boss" so iOS
Add-to-Home-Screen matches the manifest's short_name instead of
inheriting the root layout's "WFDASH".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:14:48 +02:00
e722788ab0 refactor(boss-dashboard): simplify rep card to loaded/claimed/orders
Drop the separate quotesLoaded metric and relabel quotesDoneFor as
"loaded" and quotesDoneBy as "claimed" — two near-duplicate numbers
per rep card was more confusing than useful.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:14:43 +02:00
f05fc5d9cf fix(boss-dashboard): filter to active reps only + fix PB date-filter bug
The reps list seeded a row for every `users` record (admin, test
accounts, drivers, duplicate entries), not just the 6 real sales reps.
Also, window filters built with toISOString() used a "T"-separated
date PB's filter engine doesn't match against `created`, so every
window silently returned zero rows — the whole dashboard read 0
regardless of real activity. Same fix pattern already used in
order-delivery-note-link-service.ts / order-order-duplicate-link-service.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:14:40 +02:00
40993ce8f8 refactor: remove check-progress and run-backfill scripts; add DELETE endpoint for quote_items
This commit deletes the check-progress.mjs and run-backfill.mjs files, which were previously used for order processing and backfilling tasks. Additionally, it introduces a DELETE endpoint for quote_items, allowing for the deletion of quote items by ID, along with corresponding tests to ensure proper functionality and error handling.
2026-08-31 18:36:15 +02:00
9f27e9d4f1 feat(stock): beforeunload warning when pricing draft is dirty 2026-08-31 17:15:42 +02:00
7aaed60860 feat(stock): persist pricing draft to localStorage 2026-08-31 17:14:34 +02:00
759e05048c feat(stock): edit-pricing mode + dirty rows + apply bar 2026-08-31 17:12:28 +02:00
104cf4cc6c feat(stock): missing-pricing-only filter on /stock 2026-08-31 17:08:26 +02:00
f63fb7d8fc feat(stock): show pricing columns toggle on /stock 2026-08-31 16:57:25 +02:00
Claude
a4bfef123b fix(stock): return 400 (not 500) for malformed PB id in set-pricing route
Code-review feedback from Task 4: the verbatim plan source had a
`throw new Error(...)` inside the per-row .map() that escaped
synchronously as an HTTP 500 instead of the structured 400 envelope
the rest of the route uses for validation errors. Swap .map() for a
for-of loop that returns the structured 400.

Verified:
- Bad id → 400 {code: "validation_error", ...}
- Happy path → 200 unchanged
2026-08-31 16:56:03 +02:00
6f8c6da0fe feat(stock): add POST /api/v1/stock/items/set-pricing 2026-08-31 16:54:03 +02:00
e66437322f Revert "feat(orders): add Duplicates tab surfacing flagged duplicate orders"
This reverts commit b4855a9c9f.
2026-08-31 16:34:37 +02:00
8ae4592108 feat(orders): add since=YYYY-MM-DD to backfill endpoint for scoped re-scans
Lets the operator scope a backfill to recent orders so off-peak
sanity passes don't disturb the active workflow. e.g.:

  POST /api/v1/orders/backfill-duplicate-checks?limit=200&since=2026-08-24

rescans the last 7 days up to 200 rows. Combines with limit/skip;
rejects malformed dates with 400 instead of silently no-op'ing.
2026-08-31 11:34:46 +02:00
b4855a9c9f feat(orders): add Duplicates tab surfacing flagged duplicate orders
Cross-cutting review tab at the end of the strip — lists every order
with any of the three duplicate-detection flags set:
  - order_number_duplicate
  - potential_duplicate_order
  - potential_delivery_note

Implementation:
  - src/lib/item-status-ui.ts: add 'duplicates' to TabGroup + visual +
    TAB_GROUP_ORDER (placed last so it doesn't compete with
    active-workflow tabs). Empty array in TAB_GROUP_ITEM_STATUSES
    (it's not an item-status group — the duplicates tab is
    cross-cutting on order-header fields).
  - src/lib/server/orders-service.ts: add duplicateOrderFilter() +
    orderHasAnyDuplicateFlag() helpers. listOrdersWithItems routes
    tabGroup='duplicates' to a PB filter on the orders collection
    (separate from the item-level tabGroupFilter the other tabs use).
    getOrderItemStatusTabCounts counts duplicates independently so
    the badge stays accurate even when the active-workflow counts
    are filtered out.
  - src/app/(app)/orders/page.tsx: add the tab to the strip + an
    empty state with the purple #a855f7 visual.
2026-08-31 11:30:09 +02:00
0282d43109 feat(pos): add customer search behind /api/v1/pos/customers
Keeps the till free of PocketBase credentials — the mockup queried the
customers collection directly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:36:37 +02:00
b60cdd8547 feat(pos): add catalog, checkout and sales API routes
All three gated on POS_API_KEY. Checkout accepts all four tender types
and forwards client_ref for idempotent offline-queue replays.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:35:51 +02:00
a73f3b3f97 fix(orders): update order_number collision-checking logic
Refine the handling of order_number values in the MCP API. Custom free-text order_numbers are now allowed to repeat across orders without triggering collision checks, while only values formatted as ORD-YYYY-MM-NNN will be subject to collision verification. Update tests to reflect this new behavior and ensure proper functionality.
2026-08-31 01:24:56 +02:00
2dbe96da23 fix(orders): correct page math in backfill endpoint (perPage must equal limit) 2026-08-31 01:05:54 +02:00
463265fe36 fix(orders): honor skip param in backfill endpoint (was hardcoded to page 1)
The route accepted ?skip=N but always called getList(1, ...) — every
chunk re-processed the FIRST limit orders, leaving offsets > limit
unprocessed. Convert skip to page (floor(skip/limit) + 1) and add a
regression test asserting getList is called with the right page.
2026-08-31 00:55:56 +02:00
8888a4722c feat(orders): paginate backfill endpoint (?limit&?skip) for chunked operator runs
The backfill is N+1-bounded (each order = 1 + N candidate queries),
so a 5,300-order table takes ~3 hours at ~25 orders/min on a busy
PB instance. Splitting into 200-order chunks keeps each call under
Next.js's maxDuration (10 min) and gives the operator a clear
progress signal: every chunk returns nextSkip + done.
2026-08-30 23:30:24 +02:00
687c82a4b8 polish(orders): add no-company test for order-order matcher + backfill route prereq note 2026-08-30 22:34:12 +02:00
14347c4b4b test(orders): cover auth + matcher-isolation for backfill endpoint, document response shape 2026-08-30 22:30:52 +02:00
f0c5daa99e feat(orders): add backfill endpoint for duplicate-detection fields 2026-08-30 22:28:00 +02:00
51a53851de feat(orders): render order-number and item-duplicate pills 2026-08-30 22:26:50 +02:00
9996923bf3 fix(invoicing): guard the QuickBooks send against an all-excluded group
Review follow-up to the non-invoiceable-extras work. Task 6 of the plan
called for the billable-subset guard on `sendToQuickBooks` as well as
in the modals; it landed in the modals only.

`rows.length === 0` counts excluded rows, which ride along for the n8n
status flip but never bill. A group where every row is excluded passes
that check and ships an empty `items` array, so QuickBooks would create
a zero-line invoice while n8n still flipped every row to `invoiced`.

Not reachable through the current UI — both live modals disable submit
on this case. This is defence-in-depth for the callers that bypass
them: the (currently unrendered) bulk modal and any legacy call passing
`overrides: undefined`, which falls back to every row in the group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:28:44 +02:00
7a1239a187 fix(orders): bill the customer quantity, not the supplier order_qty
`order_items.order_qty` is the SUPPLIER-facing quantity — what prints
on the PO. Five call sites read it as the customer quantity, which is a
money bug in both directions:

  - supplier MOQ (customer 4, we buy 10) → invoiced 10, over-billing 6
  - shorted line (customer 100, supplier covers 40) → invoiced 40,
    under-billing 60 until the operator splits the remainder

The invoicing screen never fetched `quantity` at all (zero occurrences
in page.tsx), so it could not bill the customer quantity even in
principle. Added to the PB field projection, which is load-bearing:
drop it and PB returns undefined and every line silently falls back to
order_qty. A page-level test now fails if the projection drifts.

`maxSurplusFor` measured the surplus as `collected - order_qty`, which
on an MOQ line is always 0 — the overflow was mathematically unbookable
and vanished from inventory by construction, not by operator error.

Four call sites built delivery-note lines by hand; two passed
`quantity` and two forgot it, so the same DN printed the customer's 4
via the service layer and the supplier's 10 via the API routes. All
four now share `buildDeliveryNoteLine`, so they cannot diverge again.

`order_qty` had no doc comment at all next to an 11-line one on
`quantity` — that absence is why five subsystems guessed differently.
Now documented, with a new `src/lib/order-item-qty.ts` holding
`customerQty` / `supplierQty` as the single definition.

driver-stops.ts and collection-confirm/po-card.tsx read `order_qty`
CORRECTLY (they check what the supplier delivered against what we
ordered from them) and carry comments saying so, so the next cleanup
doesn't "fix" them into breaking.

23 new tests. Full suite: no regressions — the 15 failures are
identical before and after (verified by stashing and re-running).

Audit: docs/superpowers/specs/2026-08-30-quantity-handling-audit.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:28:33 +02:00
12657d126b feat(mcp): order_items.create accepts exclude_from_invoice + doc updates
Task 9 of the non-invoiceable-order-extras plan.

order_items.create MCP tool:
- Input schema gains an optional boolean 'exclude_from_invoice'.
- Tool description rewritten to explain the supplier-side component
  use case, the PO + driver + collection flow, the silent drop from
  the customer invoice and delivery note, and the force-zero selling
  price.
- operationData hint / nextActions block updated — the prior text
  documented a '409 order_locked' refusal that no longer fires
  (Task 2 removed the pending gate).
- Handler forwards the boolean to createOrderItemForOrder (which
  itself forces selling_price=0 when the flag is set — Task 2).

Also fixed a TypeScript strict-mode complaint in the T7 test that
the previous commit left behind (vi.fn mock-call tuple type
needed an explicit parameter so mock.calls[0] is typed).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:46:06 +02:00
f7a79e671a test(invoicing): lock in picking-slip inclusion of excluded rows
Task 8 of the non-invoiceable-order-extras plan.

Two regression tests, no production code change. The picking-slip
feature relies on a property of the existing print pipeline:
excluded rows DO receive item_status = 'invoiced' (Task 7 / n8n
batch flip) and DO carry the invoice number / DN number, so the
print queries pick them up automatically. The picker must still
pull the washers even though they never reach the customer.

- print-bundle: explicitly verifies the picking slip renderer is
  called with both billable and excluded rows (mocked
  PickingSlipPdf → assert lines array has both descriptions).
- delivery-note-print: explicitly verifies the PB filter does NOT
  carry an exclude_from_invoice clause — adding one would
  silently filter the picker slip and break Task 7's design.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:44:12 +02:00
cb3f1618c4 feat(invoicing): split delivery-note PDF lines for non-invoiceable rows
Task 7 of the non-invoiceable-order-extras plan.

The status flip + audit stamp + delivery_note header create all
run on EVERY row so the order closes normally — excluded rows are
real operational lines from the supplier's POV. The PDF line
list is built from billable.items only, so the customer never
sees excluded lines.

Added exclude_from_invoice to the getFullList projection (load-
bearing — omit it and the billable filter silently passes
undefined, every extra lands on the customer delivery note).

New 400 guard 'No billable lines for a delivery note' when every
row in the request is excluded. UI guard in Task 6 prevents the
operator from reaching this path, but the route has its own
defence so the picker doesn't get a guaranteed-to-fail submit
silently.

2 tests + 1 mutation-test prove the split:
- excluded rows still get status flip + DN number + PDF upload,
  but the renderer is called with ONLY billable items
- all-excluded returns 400 before any PATCH fires
- mutation (PDF built from items): the mixed test fails because
  the renderer sees 3 lines instead of 2

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:35:15 +02:00
08f6005fd5 feat(invoicing): muted rendering + billable-submit guard for non-invoiceable rows
Task 6 of the non-invoiceable-order-extras plan.

Both modals (QB submit + delivery note submit) — same shape change:

- Excluded rows render muted with a 'Not invoiced' chip in the
  row metadata. The muted state overrides 'bg-card' even when
  included, so the operator doesn't mistake them for billable.
- Their include-checkbox is checked and disabled — the row must
  ride along for the n8n status flip, and the operator must not
  be able to remove it or add it to the bill. onChange defensively
  bails when the flag is set, in addition to the disabled prop.
- New 'billableCount' guard: when every billable row has been
  unchecked, submit is replaced with 'No billable lines' label
  and the click handler toasts 'This order has no billable lines.'
  The existing 'noneIncluded' check still catches the operator
  unchecking everything (including the excluded rows the disabled
  checkbox blocks them from unchecking in practice); the new guard
  is the per-mode subset check.

The existing 'noneIncluded' check isn't sufficient because excluded
rows are always in 'includedRowIds' (they have to ride along), so
includedRowIds.size === 0 is unreachable in practice — the billable
subset can still be empty.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:30:19 +02:00
7320bfdce6 feat(invoicing): split order_item_ids from items for non-invoiceable rows
Task 5 of the non-invoiceable-order-extras plan.

buildQbInvoicePayload now keeps order_item_ids built from ALL
rows (so n8n's 'Build PocketBase Invoice Update' node flips
excluded rows to invoiced alongside their siblings — the order
closes normally), and builds items from BILLABLE rows only (so
QuickBooks never learns about the excluded lines).

billableRows helper exported for sharing with the invoice UI
(Task 6) and the delivery-note route (Task 7) — three callers,
one definition of 'billable'.

Added exclude_from_invoice to RawInvoicingRow AND the
INVOICING_FIELDS projection. The projection is load-bearing:
omit it and PB returns undefined, every extra silently bills
the customer. Confirmed by inspection and by the failing
'mixed set' test if the projection drifts.

4 tests + 1 mutation-test prove the split:
- mixed: excluded ids in order_item_ids, absent from items
- all-billable: identical output to the no-flag case
- all-excluded: empty items, full order_item_ids
- billableRows helper unit tests
- mutation (items built from input.rows): mixed and all-excluded
  tests fail

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:26:46 +02:00
8b23cc1b6a feat(orders): bypass margin gate for non-invoiceable extras
Task 4 of the non-invoiceable-order-extras plan.

The page-level margin gate (process/page.tsx) short-circuits when
the item has exclude_from_invoice. An excluded line has
selling_price forced to 0 (Task 2), so the computed markup is
always -100 and the gate would wedge every extra into
approval_needed and block the order. Bypassing the gate keeps the
operational flow unblocked.

Suppressed low-margin colouring for excluded rows — a red
'-100%' chip on every extra is noise. The 'Not invoiced' badge
(T3) replaces the price chip visually.

Server-side guard in requestOrderItemMarginApproval refuses to gate
an excluded item with code 'excluded_from_invoice' (status 422).
Belt-and-braces: the MCP path can't weaponise the approval queue
to push an excluded row through manager review. Checked BEFORE
the missing_prices guard so an excluded item with selling=0 throws
the descriptive error rather than the misleading missing_prices
one.

2 tests + 1 mutation-test prove both sides of the bypass:
- excluded: throws 'excluded_from_invoice', webhook NOT fired
- non-excluded: proceeds to approval_needed, webhook DOES fire
- mutation (guard disabled): the excluded test fails

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:21:28 +02:00
50e75e6955 feat(orders): add-item affordance + sheet on the process page
Task 3 of the non-invoiceable-order-extras plan.

- New POST /api/v1/orders/{id}/items route wraps
  createOrderItemForOrder. Same-origin browser auth (PB session
  cookie), mirrors the posture of /quote after the order-quote
  migration.
- AddItemSheet component on the process page: right-side Sheet
  (mirrors PriceEditSheet's layout) with description / quantity /
  cost / selling / supplier / notes + the load-bearing
  'exclude_from_invoice' checkbox. When ticked, the selling-price
  input is forced to 0 and disabled, a 'Not invoiced' badge replaces
  the margin chip so a -100% red chip on every extra is noise.
- Add Item button below the item card stack — always available,
  not gated on item_status. The empty-state copy above ('No line
  items on this order. Add items before processing.') now points at
  a real action.
- Existing excluded rows render with a muted 'Not invoiced' badge
  in the card stack so the operator can spot them at a glance
  without opening the sheet.
- The server (Task 2) forces selling_price=0 from the flag
  regardless of caller input — the client forcing here is for UX
  consistency.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:18:30 +02:00