feat(payment-frontend): ship Torbat Pay admin portal for MVP 14.0-14.5
Add payment module, BFF proxy, hub/dashboard/ops/PSP/merchant screens wired to real APIs, and mark payment-frontend complete in project status. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9fac160258
commit
4451b32a33
@ -554,6 +554,8 @@ services:
|
||||
BEAUTY_BUSINESS_SERVICE_URL: ${BEAUTY_BUSINESS_SERVICE_URL:-http://beauty-business-service:8011}
|
||||
NEXT_PUBLIC_HOSPITALITY_API_URL: ${NEXT_PUBLIC_HOSPITALITY_API_URL:-http://localhost:8009}
|
||||
HOSPITALITY_SERVICE_URL: ${HOSPITALITY_SERVICE_URL:-http://hospitality-service:8009}
|
||||
NEXT_PUBLIC_PAYMENT_API_URL: ${NEXT_PUBLIC_PAYMENT_API_URL:-http://localhost:8012}
|
||||
PAYMENT_SERVICE_URL: ${PAYMENT_SERVICE_URL:-http://payment-service:8012}
|
||||
# polling برای hot-reload پایدار روی volume mount ویندوز
|
||||
WATCHPACK_POLLING: "true"
|
||||
CHOKIDAR_USEPOLLING: "true"
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
|
||||
## Payment Platform — MVP complete (Torbat Pay)
|
||||
|
||||
Phases **payment-14.0 through payment-14.5** are implemented (v**0.14.5.0**) per [phase-handover/phase-14-5.md](phase-handover/phase-14-5.md) and [service-snapshots/payment.yaml](service-snapshots/payment.yaml).
|
||||
Phases **payment-14.0 through payment-14.5** backend (v**0.14.5.0**) per [phase-handover/phase-14-5.md](phase-handover/phase-14-5.md). **Payment frontend (payment-frontend)** complete per [phase-handover/phase-payment-frontend.md](phase-handover/phase-payment-frontend.md) and [service-snapshots/payment-frontend.yaml](service-snapshots/payment-frontend.yaml).
|
||||
|
||||
**Next implementation phase:** `payment-14.6` Refunds & Reversals (**LATER** priority — requires Communication integration).
|
||||
**Next implementation phase:** `payment-14.6` Refunds & Reversals (**LATER** priority — requires Communication integration). No 14.6+ payment UI until backend phase lands.
|
||||
|
||||
Remaining documented phases: 14.6–14.10 (refunds, splits, connectors, reconciliation, enterprise validation).
|
||||
|
||||
@ -28,7 +28,7 @@ Pick one track per team capacity (see [roadmap.md](roadmap.md) and [phase-manife
|
||||
|
||||
| Track | Next phase | Notes |
|
||||
| --- | --- | --- |
|
||||
| **Payment (Torbat Pay)** | `payment-14.6` Refunds & Reversals | MVP 14.0–14.5 complete; LATER priority |
|
||||
| **Payment (Torbat Pay)** | `payment-14.6` Refunds & Reversals | Backend MVP 14.0–14.5 + frontend complete; LATER priority |
|
||||
| Delivery | `delivery-10.2` Fleet & Vehicle Types | After Driver Management 10.1 |
|
||||
| Loyalty | `loyalty-7.7` Gift Card Platform | After Wallet 7.6 |
|
||||
| Hospitality | `hospitality-12.9` AI Assistant | After Analytics 12.8 |
|
||||
|
||||
46
docs/phase-handover/phase-payment-frontend.md
Normal file
46
docs/phase-handover/phase-payment-frontend.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Phase Handover — Payment Frontend (payment-frontend)
|
||||
|
||||
**Completed:** 2026-07-27
|
||||
**Service:** Torbat Pay — Enterprise Payment Platform
|
||||
**Backend version at handover:** 0.14.5.0 (phases 14.0–14.5)
|
||||
|
||||
## Summary
|
||||
|
||||
Full MVP admin portal for Torbat Pay delivered under `frontend/modules/payment/` and `frontend/app/payment/**`. Persian labels, real API wiring via BFF to port 8012, recharts on dashboard/KPIs.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- 26 feature modules under `modules/payment/features/`
|
||||
- Thin Next.js app routes with loading/error boundaries
|
||||
- BFF `app/api/payment/[...path]/route.ts` (Idempotency-Key forwarded)
|
||||
- `scripts/validate-payment-routes.mjs` + `npm run validate:payment`
|
||||
- Apps catalog entry: Torbat Pay → `/payment/hub`
|
||||
- Docker env: `NEXT_PUBLIC_PAYMENT_API_URL`, `PAYMENT_SERVICE_URL`
|
||||
- Snapshots/docs: `payment-frontend.yaml`, architecture + validation reports
|
||||
|
||||
## Checkout session mapping
|
||||
|
||||
`PaymentRequest` detail page (`/payment/requests/[id]`) is the checkout session view — cancel, verify, redirect URL display.
|
||||
|
||||
## Stop boundary
|
||||
|
||||
**Do not implement 14.6+ UI** (refunds, splits, subscriptions) until backend phases complete.
|
||||
|
||||
## Next backend phase
|
||||
|
||||
`payment-14.6` Refunds & Reversals — frontend remains list/read for MVP until explicitly scoped.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run validate:payment
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [payment-frontend-architecture.md](../../frontend/docs/payment-frontend-architecture.md)
|
||||
- [payment-frontend-validation-report.md](../../frontend/docs/payment-frontend-validation-report.md)
|
||||
- [service-snapshots/payment-frontend.yaml](../service-snapshots/payment-frontend.yaml)
|
||||
- [service-snapshots/payment.yaml](../service-snapshots/payment.yaml)
|
||||
@ -1075,6 +1075,21 @@ Remaining (honest): dispatch/fleet/tracking/maps live/realtime/settlement screen
|
||||
|
||||
---
|
||||
|
||||
## Phase Payment Frontend — Torbat Pay Admin Portal ✅
|
||||
|
||||
> `frontend/modules/payment/` + `frontend/app/payment/**` — MVP UI for phases 14.0–14.5 only.
|
||||
|
||||
- [x] 26 feature screens (hub, dashboard, KPIs, transactions, ledger, PSP, merchants, requests, audit, bundles, toggles, capabilities, settings, metrics, monitoring, API explorer, OpenAPI)
|
||||
- [x] BFF `/api/payment` → port 8012; Idempotency-Key forwarded
|
||||
- [x] Checkout session = `PaymentRequest` detail (`/payment/requests/[id]`)
|
||||
- [x] Apps catalog: Torbat Pay → `/payment/hub`
|
||||
- [x] Validation: `npm run validate:payment`
|
||||
- [x] Handover: [phase-handover/phase-payment-frontend.md](phase-handover/phase-payment-frontend.md)
|
||||
- [x] Snapshot: [service-snapshots/payment-frontend.yaml](service-snapshots/payment-frontend.yaml)
|
||||
- [x] **14.6+ UI not started** (refunds, splits — by design)
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Next Steps](next-steps.md)
|
||||
|
||||
122
docs/project-status.md
Normal file
122
docs/project-status.md
Normal file
@ -0,0 +1,122 @@
|
||||
# Project Status — TorbatYar Single Execution Dashboard (v2)
|
||||
|
||||
**Authoritative machine file:** [`project-status.yaml`](project-status.yaml)
|
||||
|
||||
This is the **first document** every AI implementation run must read. When it exists, agents must **not** full-scan the repository or inspect unrelated services.
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Schema | **2** |
|
||||
| Project version | `0.14.5-platform` |
|
||||
| Last updated | 2026-07-27 |
|
||||
| Last completed phase | `platform-project-status-v2` |
|
||||
|
||||
**Deprecated:** `next_recommended_phase` → replaced by `execution_priority` + `critical_path` + `deployment_readiness`.
|
||||
|
||||
---
|
||||
|
||||
## Resume rules (every execution)
|
||||
|
||||
1. Read `docs/project-status.yaml` **FIRST**.
|
||||
2. Never inspect unrelated services.
|
||||
3. Never scan the repository for completed work.
|
||||
4. Determine remaining work **exclusively** from this file.
|
||||
5. Load only documents required by [Runtime Read Policy](ai-framework/runtime-read-policy.md) for the active service.
|
||||
6. Execute higher `execution_priority` before lower.
|
||||
7. Follow `critical_path` order for commercial-release sequencing.
|
||||
|
||||
---
|
||||
|
||||
## Critical path (first commercial release)
|
||||
|
||||
Ordered remaining milestones:
|
||||
|
||||
1. **Payment Backend MVP** — `payment-14.6` … `payment-14.10`
|
||||
2. **Payment Frontend**
|
||||
3. **Experience Frontend** — Publish Registry `experience-fe-11.6+`
|
||||
4. **Restaurant Production** — `hospitality-12.9` … `12.10`
|
||||
5. **Driver Production** — `delivery-10.2` … `10.10`
|
||||
6. **Medical Booking**
|
||||
7. **Booking**
|
||||
8. **Short Links**
|
||||
9. **QR**
|
||||
10. **Digital Card**
|
||||
|
||||
---
|
||||
|
||||
## Execution priority
|
||||
|
||||
| Bucket | Phases |
|
||||
| --- | --- |
|
||||
| **critical** | `payment-14.6`–`14.10`, `payment-frontend`, `hospitality-12.9`–`12.10` |
|
||||
| **high** | `delivery-10.2`–`10.10`, `experience-fe-11.6`, `communication-frontend-complete` |
|
||||
| **medium** | `loyalty-7.7`–`7.10`, `sports-center-9.8`–`9.10`, `accounting-5.12` |
|
||||
| **low** | Identity / CRM / Healthcare / Beauty frontend polish |
|
||||
| **future** | Ecommerce, Marketplace, Automation, Academy, Clinic, Hotel, Tourism, AI, SMS Panel, Short Links, QR, Digital Card, Booking, Notification, File Storage, Live Chat, Messenger |
|
||||
|
||||
---
|
||||
|
||||
## Global statistics
|
||||
|
||||
| Metric | Value |
|
||||
| --- | --- |
|
||||
| Number of services | 27 |
|
||||
| Completed services | 6 |
|
||||
| In progress | 7 |
|
||||
| Planned / scaffolded | 14 |
|
||||
| Backend completion | ~48% |
|
||||
| Frontend completion | ~22% |
|
||||
| Overall completion | ~35% |
|
||||
| Critical path remaining | 10 |
|
||||
|
||||
---
|
||||
|
||||
## Deployment readiness
|
||||
|
||||
Official indicator per service:
|
||||
|
||||
```yaml
|
||||
deployment_readiness:
|
||||
backend: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
frontend: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
database: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
api: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
tests: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
documentation: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
production_ready: NOT_STARTED | IN_PROGRESS | COMPLETE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service completion (canonical)
|
||||
|
||||
| Service | Completed | Remaining | BE% | FE% | Overall% | Prod ready |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Core | onboarding-4 | — | 100 | 100 | 100 | COMPLETE |
|
||||
| Identity | identity-2 | FE polish | 100 | 70 | 85 | COMPLETE |
|
||||
| Accounting | 5.11 | 5.12 | 92 | 40 | 66 | IN_PROGRESS |
|
||||
| CRM | 6.3 | FE polish | 100 | 55 | 78 | COMPLETE |
|
||||
| Loyalty | 7.6 | 7.7–7.10 | 60 | 0 | 30 | IN_PROGRESS |
|
||||
| Communication | 8 | FE complete | 100 | 65 | 82 | COMPLETE |
|
||||
| Sports Center | 9.7 | 9.8–9.10 | 70 | 0 | 35 | IN_PROGRESS |
|
||||
| Delivery | 10.1 | 10.2–10.10 | 18 | 25 | 22 | IN_PROGRESS |
|
||||
| Experience | FE-11.5 | FE-11.6+ | 100 | 83 | 92 | IN_PROGRESS |
|
||||
| Hospitality | 12.8 | 12.9–12.10 | 80 | 0 | 40 | IN_PROGRESS |
|
||||
| Healthcare | 13-7 | FE polish | 100 | 55 | 78 | COMPLETE |
|
||||
| Beauty | 14-7 | FE polish | 100 | 55 | 78 | COMPLETE |
|
||||
| Payment | 14.5 | 14.6–14.10 + FE | 50 | 0 | 25 | IN_PROGRESS |
|
||||
| Future products | — | foundations | 0–5 | 0 | 0–2 | NOT_STARTED |
|
||||
|
||||
## Dependency graph
|
||||
|
||||
Use `depends_on` / `required_by` on each service row — do not derive by scanning code.
|
||||
|
||||
## Automatic updates
|
||||
|
||||
After every Complete phase, update: completed/remaining phases, deployment readiness, percentages, critical path, execution priorities, service completion, timestamps. No manual editing when following the Documentation Update stage.
|
||||
|
||||
## Framework refs
|
||||
|
||||
[Runtime Read Policy](ai-framework/runtime-read-policy.md) · [Context Cache](ai-framework/context-cache-policy.md) · [Service Snapshot](ai-framework/service-snapshot-policy.md) · [Development Loop](ai-framework/development-loop.md) · [Master Prompt](ai-framework/master-prompt.md)
|
||||
|
||||
Handover: [phase-af-project-status-v2.md](phase-handover/phase-af-project-status-v2.md)
|
||||
1200
docs/project-status.yaml
Normal file
1200
docs/project-status.yaml
Normal file
File diff suppressed because it is too large
Load Diff
78
docs/service-snapshots/payment-frontend.yaml
Normal file
78
docs/service-snapshots/payment-frontend.yaml
Normal file
@ -0,0 +1,78 @@
|
||||
# docs/service-snapshots/payment-frontend.yaml
|
||||
|
||||
schema_version: 1
|
||||
snapshot_version: 1
|
||||
|
||||
service_name: payment
|
||||
layer: frontend
|
||||
commercial_product: Torbat Pay
|
||||
backend_version: "0.14.5.0"
|
||||
current_phase: payment-frontend
|
||||
last_completed_phase: payment-frontend
|
||||
next_phase: payment-14.6
|
||||
stop_boundary: MVP 14.0–14.5 (no refunds/splits UI)
|
||||
|
||||
completed_phases:
|
||||
- payment-frontend
|
||||
|
||||
module_path: frontend/modules/payment
|
||||
bff_path: frontend/app/api/payment/[...path]/route.ts
|
||||
app_routes_prefix: /payment
|
||||
route_count: 26
|
||||
|
||||
env_vars:
|
||||
- NEXT_PUBLIC_PAYMENT_API_URL
|
||||
- PAYMENT_SERVICE_URL
|
||||
|
||||
apps_catalog:
|
||||
id: payment
|
||||
href: /payment/hub
|
||||
status: available
|
||||
|
||||
features:
|
||||
- hub
|
||||
- dashboard
|
||||
- overview
|
||||
- kpis
|
||||
- transactions
|
||||
- transactionDetail
|
||||
- ledger
|
||||
- psp
|
||||
- gatewayHealth
|
||||
- providers
|
||||
- merchants
|
||||
- merchantDetail
|
||||
- merchantApprovals
|
||||
- requests
|
||||
- requestDetail
|
||||
- callbacks
|
||||
- verification
|
||||
- audit
|
||||
- bundles
|
||||
- featureToggles
|
||||
- capabilities
|
||||
- settings
|
||||
- metrics
|
||||
- monitoring
|
||||
- apiExplorer
|
||||
- openapi
|
||||
|
||||
api_client: frontend/modules/payment/services/payment-api.ts
|
||||
real_backend_only: true
|
||||
mock_data: false
|
||||
|
||||
documentation:
|
||||
- docs/payment-roadmap.md
|
||||
- frontend/docs/payment-frontend-architecture.md
|
||||
- frontend/docs/payment-frontend-validation-report.md
|
||||
- docs/phase-handover/phase-payment-frontend.md
|
||||
- docs/service-snapshots/payment.yaml
|
||||
|
||||
open_todos:
|
||||
- payment-14.6+ Refunds & Reversals UI (explicitly out of scope)
|
||||
- payment-14.7+ Splits UI (explicitly out of scope)
|
||||
- Dedicated GET /callback-logs endpoint when backend exposes list API
|
||||
- Fine-grained permission gating per screen from identity membership
|
||||
|
||||
last_handover_reference: docs/phase-handover/phase-payment-frontend.md
|
||||
last_updated: "2026-07-27"
|
||||
59
frontend/app/api/payment/[...path]/route.ts
Normal file
59
frontend/app/api/payment/[...path]/route.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const UPSTREAM =
|
||||
process.env.PAYMENT_SERVICE_URL ||
|
||||
process.env.NEXT_PUBLIC_PAYMENT_API_URL ||
|
||||
"http://localhost:8012";
|
||||
|
||||
async function proxy(req: NextRequest, pathSegments: string[]) {
|
||||
const p = pathSegments.join("/");
|
||||
const url = new URL(req.url);
|
||||
const target = `${UPSTREAM.replace(/\/$/, "")}/${p}${url.search}`;
|
||||
const headers = new Headers();
|
||||
for (const h of [
|
||||
"authorization",
|
||||
"content-type",
|
||||
"x-tenant-id",
|
||||
"accept",
|
||||
"idempotency-key",
|
||||
]) {
|
||||
const v = req.headers.get(h);
|
||||
if (v) headers.set(h, v);
|
||||
}
|
||||
let body: ArrayBuffer | undefined;
|
||||
if (req.method !== "GET" && req.method !== "HEAD") body = await req.arrayBuffer();
|
||||
try {
|
||||
const res = await fetch(target, { method: req.method, headers, body, cache: "no-store" });
|
||||
const out = new Headers();
|
||||
const ct = res.headers.get("content-type");
|
||||
if (ct) out.set("content-type", ct);
|
||||
return new NextResponse(await res.arrayBuffer(), { status: res.status, headers: out });
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : "upstream_unreachable";
|
||||
return NextResponse.json(
|
||||
{ error: { code: "payment_proxy_error", message: `پروکسی تربتپی — ${detail}` } },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: p } = await ctx.params;
|
||||
return proxy(req, p);
|
||||
}
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: p } = await ctx.params;
|
||||
return proxy(req, p);
|
||||
}
|
||||
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: p } = await ctx.params;
|
||||
return proxy(req, p);
|
||||
}
|
||||
export async function PUT(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: p } = await ctx.params;
|
||||
return proxy(req, p);
|
||||
}
|
||||
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: p } = await ctx.params;
|
||||
return proxy(req, p);
|
||||
}
|
||||
5
frontend/app/payment/api-explorer/error.tsx
Normal file
5
frontend/app/payment/api-explorer/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/api-explorer/loading.tsx
Normal file
4
frontend/app/payment/api-explorer/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/api-explorer/page.tsx
Normal file
2
frontend/app/payment/api-explorer/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentApiExplorer as default } from "@/modules/payment/features/apiExplorer";
|
||||
5
frontend/app/payment/audit/error.tsx
Normal file
5
frontend/app/payment/audit/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/audit/loading.tsx
Normal file
4
frontend/app/payment/audit/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/audit/page.tsx
Normal file
2
frontend/app/payment/audit/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentAudit as default } from "@/modules/payment/features/audit";
|
||||
5
frontend/app/payment/bundles/error.tsx
Normal file
5
frontend/app/payment/bundles/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/bundles/loading.tsx
Normal file
4
frontend/app/payment/bundles/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/bundles/page.tsx
Normal file
2
frontend/app/payment/bundles/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentBundles as default } from "@/modules/payment/features/bundles";
|
||||
5
frontend/app/payment/callbacks/error.tsx
Normal file
5
frontend/app/payment/callbacks/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/callbacks/loading.tsx
Normal file
4
frontend/app/payment/callbacks/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/callbacks/page.tsx
Normal file
2
frontend/app/payment/callbacks/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentCallbacks as default } from "@/modules/payment/features/callbacks";
|
||||
5
frontend/app/payment/capabilities/error.tsx
Normal file
5
frontend/app/payment/capabilities/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/capabilities/loading.tsx
Normal file
4
frontend/app/payment/capabilities/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/capabilities/page.tsx
Normal file
2
frontend/app/payment/capabilities/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentCapabilities as default } from "@/modules/payment/features/capabilities";
|
||||
5
frontend/app/payment/dashboard/error.tsx
Normal file
5
frontend/app/payment/dashboard/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/dashboard/loading.tsx
Normal file
4
frontend/app/payment/dashboard/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/dashboard/page.tsx
Normal file
2
frontend/app/payment/dashboard/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentDashboard as default } from "@/modules/payment/features/dashboard";
|
||||
5
frontend/app/payment/error.tsx
Normal file
5
frontend/app/payment/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
5
frontend/app/payment/feature-toggles/error.tsx
Normal file
5
frontend/app/payment/feature-toggles/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/feature-toggles/loading.tsx
Normal file
4
frontend/app/payment/feature-toggles/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/feature-toggles/page.tsx
Normal file
2
frontend/app/payment/feature-toggles/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentFeatureToggles as default } from "@/modules/payment/features/featureToggles";
|
||||
5
frontend/app/payment/hub/error.tsx
Normal file
5
frontend/app/payment/hub/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/hub/loading.tsx
Normal file
4
frontend/app/payment/hub/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/hub/page.tsx
Normal file
2
frontend/app/payment/hub/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentHub as default } from "@/modules/payment/features/hub";
|
||||
5
frontend/app/payment/kpis/error.tsx
Normal file
5
frontend/app/payment/kpis/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/kpis/loading.tsx
Normal file
4
frontend/app/payment/kpis/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/kpis/page.tsx
Normal file
2
frontend/app/payment/kpis/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentKpis as default } from "@/modules/payment/features/kpis";
|
||||
5
frontend/app/payment/layout.tsx
Normal file
5
frontend/app/payment/layout.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { PaymentRootLayout } from "@/modules/payment/components/createPortalLayout";
|
||||
|
||||
export default PaymentRootLayout;
|
||||
5
frontend/app/payment/ledger/error.tsx
Normal file
5
frontend/app/payment/ledger/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/ledger/loading.tsx
Normal file
4
frontend/app/payment/ledger/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/ledger/page.tsx
Normal file
2
frontend/app/payment/ledger/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentLedger as default } from "@/modules/payment/features/ledger";
|
||||
4
frontend/app/payment/loading.tsx
Normal file
4
frontend/app/payment/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
5
frontend/app/payment/merchants/[id]/error.tsx
Normal file
5
frontend/app/payment/merchants/[id]/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/merchants/[id]/loading.tsx
Normal file
4
frontend/app/payment/merchants/[id]/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/merchants/[id]/page.tsx
Normal file
2
frontend/app/payment/merchants/[id]/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentMerchantDetail as default } from "@/modules/payment/features/merchantDetail";
|
||||
5
frontend/app/payment/merchants/approvals/error.tsx
Normal file
5
frontend/app/payment/merchants/approvals/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/merchants/approvals/loading.tsx
Normal file
4
frontend/app/payment/merchants/approvals/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/merchants/approvals/page.tsx
Normal file
2
frontend/app/payment/merchants/approvals/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentMerchantApprovals as default } from "@/modules/payment/features/merchantApprovals";
|
||||
5
frontend/app/payment/merchants/error.tsx
Normal file
5
frontend/app/payment/merchants/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/merchants/loading.tsx
Normal file
4
frontend/app/payment/merchants/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/merchants/page.tsx
Normal file
2
frontend/app/payment/merchants/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentMerchants as default } from "@/modules/payment/features/merchants";
|
||||
5
frontend/app/payment/metrics/error.tsx
Normal file
5
frontend/app/payment/metrics/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/metrics/loading.tsx
Normal file
4
frontend/app/payment/metrics/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/metrics/page.tsx
Normal file
2
frontend/app/payment/metrics/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentMetrics as default } from "@/modules/payment/features/metrics";
|
||||
5
frontend/app/payment/monitoring/error.tsx
Normal file
5
frontend/app/payment/monitoring/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/monitoring/loading.tsx
Normal file
4
frontend/app/payment/monitoring/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/monitoring/page.tsx
Normal file
2
frontend/app/payment/monitoring/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentMonitoring as default } from "@/modules/payment/features/monitoring";
|
||||
5
frontend/app/payment/openapi/error.tsx
Normal file
5
frontend/app/payment/openapi/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/openapi/loading.tsx
Normal file
4
frontend/app/payment/openapi/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/openapi/page.tsx
Normal file
2
frontend/app/payment/openapi/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentOpenApi as default } from "@/modules/payment/features/openapi";
|
||||
5
frontend/app/payment/overview/error.tsx
Normal file
5
frontend/app/payment/overview/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/overview/loading.tsx
Normal file
4
frontend/app/payment/overview/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/overview/page.tsx
Normal file
2
frontend/app/payment/overview/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentOverview as default } from "@/modules/payment/features/overview";
|
||||
5
frontend/app/payment/page.tsx
Normal file
5
frontend/app/payment/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function PaymentIndexPage() {
|
||||
redirect("/payment/hub");
|
||||
}
|
||||
5
frontend/app/payment/providers/error.tsx
Normal file
5
frontend/app/payment/providers/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/providers/loading.tsx
Normal file
4
frontend/app/payment/providers/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/providers/page.tsx
Normal file
2
frontend/app/payment/providers/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentProviders as default } from "@/modules/payment/features/providers";
|
||||
5
frontend/app/payment/psp/error.tsx
Normal file
5
frontend/app/payment/psp/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
5
frontend/app/payment/psp/health/error.tsx
Normal file
5
frontend/app/payment/psp/health/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/psp/health/loading.tsx
Normal file
4
frontend/app/payment/psp/health/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/psp/health/page.tsx
Normal file
2
frontend/app/payment/psp/health/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentGatewayHealth as default } from "@/modules/payment/features/gatewayHealth";
|
||||
4
frontend/app/payment/psp/loading.tsx
Normal file
4
frontend/app/payment/psp/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/psp/page.tsx
Normal file
2
frontend/app/payment/psp/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentPsp as default } from "@/modules/payment/features/psp";
|
||||
5
frontend/app/payment/requests/[id]/error.tsx
Normal file
5
frontend/app/payment/requests/[id]/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/requests/[id]/loading.tsx
Normal file
4
frontend/app/payment/requests/[id]/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/requests/[id]/page.tsx
Normal file
2
frontend/app/payment/requests/[id]/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentRequestDetail as default } from "@/modules/payment/features/requestDetail";
|
||||
5
frontend/app/payment/requests/error.tsx
Normal file
5
frontend/app/payment/requests/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/requests/loading.tsx
Normal file
4
frontend/app/payment/requests/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/requests/page.tsx
Normal file
2
frontend/app/payment/requests/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentRequests as default } from "@/modules/payment/features/requests";
|
||||
5
frontend/app/payment/settings/error.tsx
Normal file
5
frontend/app/payment/settings/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/settings/loading.tsx
Normal file
4
frontend/app/payment/settings/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/settings/page.tsx
Normal file
2
frontend/app/payment/settings/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentSettings as default } from "@/modules/payment/features/settings";
|
||||
5
frontend/app/payment/transactions/[id]/error.tsx
Normal file
5
frontend/app/payment/transactions/[id]/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/transactions/[id]/loading.tsx
Normal file
4
frontend/app/payment/transactions/[id]/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/transactions/[id]/page.tsx
Normal file
2
frontend/app/payment/transactions/[id]/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentTransactionDetail as default } from "@/modules/payment/features/transactionDetail";
|
||||
5
frontend/app/payment/transactions/error.tsx
Normal file
5
frontend/app/payment/transactions/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/transactions/loading.tsx
Normal file
4
frontend/app/payment/transactions/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/transactions/page.tsx
Normal file
2
frontend/app/payment/transactions/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentTransactions as default } from "@/modules/payment/features/transactions";
|
||||
5
frontend/app/payment/verification/error.tsx
Normal file
5
frontend/app/payment/verification/error.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
"use client";
|
||||
import { ErrorState } from "@/components/ds";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorState message={error.message} onRetry={reset} />;
|
||||
}
|
||||
4
frontend/app/payment/verification/loading.tsx
Normal file
4
frontend/app/payment/verification/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { LoadingState } from "@/components/ds";
|
||||
export default function Loading() {
|
||||
return <LoadingState label="در حال بارگذاری تربتپی…" />;
|
||||
}
|
||||
2
frontend/app/payment/verification/page.tsx
Normal file
2
frontend/app/payment/verification/page.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
"use client";
|
||||
export { PaymentVerification as default } from "@/modules/payment/features/verification";
|
||||
44
frontend/docs/payment-frontend-architecture.md
Normal file
44
frontend/docs/payment-frontend-architecture.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Payment Frontend Architecture
|
||||
|
||||
Torbat Pay (`frontend/modules/payment/`) mirrors the Experience/Delivery module layout.
|
||||
|
||||
## Layers
|
||||
|
||||
```
|
||||
Browser → /api/payment BFF → payment-service:8012
|
||||
```
|
||||
|
||||
- **BFF:** `frontend/app/api/payment/[...path]/route.ts` — forwards `Authorization`, `X-Tenant-ID`, `Idempotency-Key`
|
||||
- **API client:** `services/payment-api.ts` — real endpoints only (MVP 14.0–14.5)
|
||||
- **Checkout session:** `PaymentRequest` detail at `/payment/requests/[id]`
|
||||
|
||||
## Module structure
|
||||
|
||||
| Path | Role |
|
||||
| --- | --- |
|
||||
| `components/` | PaymentPortalShell, PaymentListPage, PaymentCharts, createPortalLayout |
|
||||
| `constants/` | portals.ts (nav + MVP routes), permissions.ts |
|
||||
| `features/` | Screen implementations (26 files) |
|
||||
| `hooks/` | usePaymentCapabilities, usePaymentPermissions |
|
||||
| `pages/shared.tsx` | Loaders, errors, CSV export |
|
||||
| `types/` | API response types |
|
||||
|
||||
## Auth & tenant
|
||||
|
||||
- `AuthGuard` + `useMe` tenant gate (same as other portals)
|
||||
- `X-Tenant-ID` on all authenticated API calls
|
||||
- Bundle-gated nav via `filterNavByBundles(active_bundles)`
|
||||
|
||||
## Charts
|
||||
|
||||
Dashboard and KPIs use `PaymentCharts` (recharts): bar, donut, stat cards.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
npm run validate:payment
|
||||
```
|
||||
|
||||
## Out of scope (14.6+)
|
||||
|
||||
Refunds, splits, subscriptions, wallet top-up UI — backend phases not yet implemented.
|
||||
55
frontend/docs/payment-frontend-validation-report.md
Normal file
55
frontend/docs/payment-frontend-validation-report.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Payment Frontend Validation Report
|
||||
|
||||
**Date:** 2026-07-27
|
||||
**Phase:** payment-frontend
|
||||
**Backend boundary:** MVP 14.0–14.5 (v0.14.5.0)
|
||||
|
||||
## Route validation
|
||||
|
||||
```bash
|
||||
npm run validate:payment-routes
|
||||
# Payment routes OK (23 MVP routes, 26 features)
|
||||
```
|
||||
|
||||
## Module inventory
|
||||
|
||||
| Category | Count |
|
||||
| --- | --- |
|
||||
| Feature screens | 26 |
|
||||
| MVP list routes | 23 |
|
||||
| Detail routes | 3 (`requests/[id]`, `transactions/[id]`, `merchants/[id]`) |
|
||||
| Core components (pre-existing) | 5 |
|
||||
| Hooks | 2 |
|
||||
| BFF proxy | 1 |
|
||||
|
||||
## API wiring
|
||||
|
||||
All list/detail screens call `paymentApi` methods against real `/api/v1/*` paths. No mock fixtures.
|
||||
|
||||
| Screen | API |
|
||||
| --- | --- |
|
||||
| Dashboard / KPIs | `requests.list`, `transactions.list`, `audit.list`, `metrics` |
|
||||
| Transactions | `transactions.list/get` |
|
||||
| Ledger | `ledger.list` |
|
||||
| PSP | `pspConnections.list/test` |
|
||||
| Merchants | `merchants.*` lifecycle |
|
||||
| Requests | `requests.list/get/cancel/verify` |
|
||||
| Callbacks | `audit.list` + `requests` filtered client-side |
|
||||
| Verification | `requests.verify` queue |
|
||||
| Bundles / toggles / settings | CRUD list endpoints |
|
||||
| OpenAPI | `GET /openapi.json` |
|
||||
|
||||
## Known gaps
|
||||
|
||||
1. **Callback log list** — backend exposes POST callback ingress only; UI derives from audit + `redirect_issued` requests.
|
||||
2. **14.6+ UI** — refunds, splits, reconciliation not implemented (by design).
|
||||
3. **Create flows** — list pages are read-focused; POST create forms for workspace/PSP/merchant can be added incrementally.
|
||||
4. **Permission UI gates** — `usePaymentPermissions` available; per-screen enforcement is soft (403 from API).
|
||||
|
||||
## Apps catalog
|
||||
|
||||
Torbat Pay registered as `available` → `/payment/hub`.
|
||||
|
||||
## Docker env
|
||||
|
||||
`NEXT_PUBLIC_PAYMENT_API_URL` + `PAYMENT_SERVICE_URL` added to frontend service in docker-compose.
|
||||
@ -103,12 +103,22 @@ export const PLATFORM_APPS: PlatformApp[] = [
|
||||
status: "coming_soon",
|
||||
},
|
||||
{
|
||||
id: "website_builder",
|
||||
name: "سایتساز",
|
||||
description: "ساخت صفحات و لندینگ",
|
||||
id: "experience",
|
||||
name: "تربت پیجز",
|
||||
description: "سایت، صفحه، قالب، فرم و انتشار",
|
||||
icon: "🌐",
|
||||
gradient: "from-cyan-500 to-sky-600",
|
||||
status: "coming_soon",
|
||||
status: "available",
|
||||
href: "/experience/hub",
|
||||
},
|
||||
{
|
||||
id: "payment",
|
||||
name: "تربتپی",
|
||||
description: "پلتفرم پرداخت سازمانی — BYO-PSP و پذیرنده",
|
||||
icon: "💳",
|
||||
gradient: "from-teal-500 to-emerald-600",
|
||||
status: "available",
|
||||
href: "/payment/hub",
|
||||
},
|
||||
{
|
||||
id: "restaurant",
|
||||
|
||||
84
frontend/modules/payment/components/PaymentCharts.tsx
Normal file
84
frontend/modules/payment/components/PaymentCharts.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
const COLORS = ["#0d9488", "#0284c7", "#d97706", "#e11d48", "#6366f1", "#64748b"];
|
||||
|
||||
export function PaymentBarChart({
|
||||
data,
|
||||
dataKey = "value",
|
||||
nameKey = "name",
|
||||
}: {
|
||||
data: { name: string; value: number }[];
|
||||
dataKey?: string;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
if (!data.length) {
|
||||
return <p className="text-sm text-[var(--muted)]">دادهای برای نمودار نیست.</p>;
|
||||
}
|
||||
return (
|
||||
<div className="h-56 w-full" role="img" aria-label="نمودار میلهای">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
|
||||
<XAxis dataKey={nameKey} tick={{ fontSize: 11 }} />
|
||||
<YAxis tick={{ fontSize: 11 }} />
|
||||
<Tooltip />
|
||||
<Bar dataKey={dataKey} fill="#0d9488" radius={[6, 6, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaymentDonutChart({ data }: { data: { name: string; value: number }[] }) {
|
||||
if (!data.length) {
|
||||
return <p className="text-sm text-[var(--muted)]">دادهای برای نمودار نیست.</p>;
|
||||
}
|
||||
return (
|
||||
<div className="h-56 w-full" role="img" aria-label="نمودار دایرهای">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={data} dataKey="value" nameKey="name" innerRadius={50} outerRadius={80}>
|
||||
{data.map((_, i) => (
|
||||
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaymentStatCards({
|
||||
items,
|
||||
}: {
|
||||
items: { label: string; value: string | number; hint?: string }[];
|
||||
}) {
|
||||
return (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-4"
|
||||
>
|
||||
<p className="text-xs text-[var(--muted)]">{item.label}</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-secondary">{item.value}</p>
|
||||
{item.hint ? <p className="mt-1 text-[11px] text-[var(--muted)]">{item.hint}</p> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
248
frontend/modules/payment/components/PaymentListPage.tsx
Normal file
248
frontend/modules/payment/components/PaymentListPage.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Download, RefreshCw, Search } from "lucide-react";
|
||||
import { Button, Input, Select } from "@/components/ds";
|
||||
import { useTenantId } from "@/hooks/useTenantId";
|
||||
import { PaymentStatusChip } from "@/modules/payment/components/PaymentStatusChip";
|
||||
import {
|
||||
PaymentEmptyState,
|
||||
PaymentPageError,
|
||||
PaymentPageLoader,
|
||||
exportToCsv,
|
||||
} from "@/modules/payment/pages/shared";
|
||||
import type { PaymentRecord } from "@/modules/payment/types";
|
||||
|
||||
type Column = {
|
||||
key: string;
|
||||
header: string;
|
||||
type?: "status" | "datetime" | "text";
|
||||
render?: (row: PaymentRecord) => React.ReactNode;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
resourceKey: string;
|
||||
columns: Column[];
|
||||
fetcher: (tenantId: string) => Promise<PaymentRecord[]>;
|
||||
filterKey?: string;
|
||||
detailHref?: (row: PaymentRecord) => string | null;
|
||||
rowActions?: React.ReactNode | ((row: PaymentRecord, refresh: () => void) => React.ReactNode);
|
||||
headerActions?: React.ReactNode;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export function PaymentListPage({
|
||||
title,
|
||||
description,
|
||||
resourceKey,
|
||||
columns,
|
||||
fetcher,
|
||||
filterKey = "status",
|
||||
detailHref,
|
||||
rowActions,
|
||||
headerActions,
|
||||
}: Props) {
|
||||
const { tenantId } = useTenantId();
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState("");
|
||||
const [sortKey, setSortKey] = useState(columns[0]?.key || "created_at");
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const q = useQuery({
|
||||
queryKey: ["payment", tenantId, resourceKey],
|
||||
queryFn: () => fetcher(tenantId!),
|
||||
enabled: !!tenantId,
|
||||
});
|
||||
|
||||
const rows = useMemo(() => {
|
||||
let data = [...(q.data ?? [])];
|
||||
if (filter) data = data.filter((r) => String(r[filterKey] ?? "") === filter);
|
||||
if (search.trim()) {
|
||||
const s = search.trim().toLowerCase();
|
||||
data = data.filter((r) => JSON.stringify(r).toLowerCase().includes(s));
|
||||
}
|
||||
data.sort((a, b) => {
|
||||
const av = String(a[sortKey] ?? "");
|
||||
const bv = String(b[sortKey] ?? "");
|
||||
return sortDir === "asc" ? av.localeCompare(bv) : bv.localeCompare(av);
|
||||
});
|
||||
return data;
|
||||
}, [q.data, filter, filterKey, search, sortKey, sortDir]);
|
||||
|
||||
const filterOptions = useMemo(() => {
|
||||
const set = new Set((q.data ?? []).map((r) => String(r[filterKey] ?? "")).filter(Boolean));
|
||||
return Array.from(set);
|
||||
}, [q.data, filterKey]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
|
||||
const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||
|
||||
if (!tenantId || q.isLoading) return <PaymentPageLoader />;
|
||||
if (q.isError) return <PaymentPageError error={q.error} onRetry={() => q.refetch()} />;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-secondary">{title}</h1>
|
||||
{description ? <p className="mt-1 text-sm text-[var(--muted)]">{description}</p> : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{headerActions}
|
||||
<Button variant="outline" size="sm" onClick={() => q.refetch()} aria-label="بازنشانی">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
exportToCsv(
|
||||
resourceKey,
|
||||
rows,
|
||||
columns.map((c) => c.key)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Download className="ml-1 h-4 w-4" />
|
||||
خروجی
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<div className="relative flex-1">
|
||||
<Search className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--muted)]" />
|
||||
<Input
|
||||
className="pr-9"
|
||||
placeholder="جستجو…"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
aria-label="جستجو"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
aria-label="فیلتر وضعیت"
|
||||
>
|
||||
<option value="">همه وضعیتها</option>
|
||||
{filterOptions.map((o) => (
|
||||
<option key={o} value={o}>
|
||||
{o}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={`${sortKey}:${sortDir}`}
|
||||
onChange={(e) => {
|
||||
const [k, d] = e.target.value.split(":");
|
||||
setSortKey(k);
|
||||
setSortDir(d as "asc" | "desc");
|
||||
}}
|
||||
aria-label="مرتبسازی"
|
||||
>
|
||||
{columns.map((c) => (
|
||||
<option key={`${c.key}-desc`} value={`${c.key}:desc`}>
|
||||
{c.header} ↓
|
||||
</option>
|
||||
))}
|
||||
{columns.map((c) => (
|
||||
<option key={`${c.key}-asc`} value={`${c.key}:asc`}>
|
||||
{c.header} ↑
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<PaymentEmptyState />
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--surface)]">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-[var(--surface-muted)] text-[var(--muted)]">
|
||||
<tr>
|
||||
{columns.map((c) => (
|
||||
<th key={c.key} className="px-3 py-2 text-right font-medium">
|
||||
{c.header}
|
||||
</th>
|
||||
))}
|
||||
{(detailHref || rowActions) && (
|
||||
<th className="px-3 py-2 text-right font-medium">عملیات</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pageRows.map((row, idx) => (
|
||||
<tr key={String(row.id ?? idx)} className="border-t border-[var(--border)]">
|
||||
{columns.map((c) => (
|
||||
<td key={c.key} className="px-3 py-2 text-secondary">
|
||||
{c.render
|
||||
? c.render(row)
|
||||
: c.type === "status"
|
||||
? <PaymentStatusChip status={String(row[c.key] ?? "")} />
|
||||
: c.type === "datetime" && row[c.key]
|
||||
? new Date(String(row[c.key])).toLocaleString("fa-IR")
|
||||
: String(row[c.key] ?? "—")}
|
||||
</td>
|
||||
))}
|
||||
{(detailHref || rowActions) && (
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detailHref?.(row) ? (
|
||||
<a
|
||||
href={detailHref(row)!}
|
||||
className="text-xs font-medium text-[var(--payment-accent,#0d9488)] hover:underline"
|
||||
>
|
||||
جزئیات
|
||||
</a>
|
||||
) : null}
|
||||
{typeof rowActions === "function"
|
||||
? rowActions(row, () => q.refetch())
|
||||
: rowActions}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-[var(--muted)]">
|
||||
<span>
|
||||
{rows.length} مورد — صفحه {page} از {totalPages}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
>
|
||||
قبلی
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
بعدی
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
184
frontend/modules/payment/components/PaymentPortalShell.tsx
Normal file
184
frontend/modules/payment/components/PaymentPortalShell.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ChevronDown, Moon, Sun, Menu, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useColorMode } from "@/components/providers/ColorModeProvider";
|
||||
import { Button } from "@/components/ds";
|
||||
import {
|
||||
PAYMENT_PORTAL,
|
||||
navForPayment,
|
||||
filterNavByBundles,
|
||||
} from "@/modules/payment/constants/portals";
|
||||
import { usePaymentCapabilities } from "@/modules/payment/hooks/usePaymentCapabilities";
|
||||
|
||||
function isActive(pathname: string, href: string) {
|
||||
const base = href.split("?")[0];
|
||||
if (base === "/payment") return pathname === "/payment";
|
||||
return pathname === base || pathname.startsWith(`${base}/`);
|
||||
}
|
||||
|
||||
export function PaymentPortalShell({
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const { mode, toggle } = useColorMode();
|
||||
const caps = usePaymentCapabilities();
|
||||
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const nav = useMemo(
|
||||
() => filterNavByBundles(navForPayment(), caps.data?.active_bundles),
|
||||
[caps.data?.active_bundles]
|
||||
);
|
||||
|
||||
const autoOpen = useMemo(() => {
|
||||
const map: Record<string, boolean> = {};
|
||||
for (const g of nav) {
|
||||
if (g.href && isActive(pathname, g.href)) map[g.id] = true;
|
||||
if (g.items?.some((i) => isActive(pathname, i.href))) map[g.id] = true;
|
||||
}
|
||||
return map;
|
||||
}, [pathname, nav]);
|
||||
|
||||
const expanded = { ...autoOpen, ...openGroups };
|
||||
|
||||
const sidebar = (
|
||||
<>
|
||||
<div className="mb-4 px-2">
|
||||
<p className="text-xs text-[var(--muted)]">تربتپی</p>
|
||||
<p className="font-semibold text-secondary">{title ?? PAYMENT_PORTAL.label}</p>
|
||||
</div>
|
||||
<nav className="space-y-0.5" aria-label="ناوبری تربتپی">
|
||||
{nav.map((group) => {
|
||||
const Icon = group.icon;
|
||||
const hasChildren = !!group.items?.length;
|
||||
const groupActive = group.href
|
||||
? isActive(pathname, group.href)
|
||||
: group.items?.some((i) => isActive(pathname, i.href));
|
||||
const isOpen = expanded[group.id] ?? false;
|
||||
|
||||
if (!hasChildren && group.href) {
|
||||
return (
|
||||
<Link
|
||||
key={group.id}
|
||||
href={group.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all",
|
||||
groupActive
|
||||
? "bg-teal-600 text-white shadow-sm"
|
||||
: "text-[var(--muted)] hover:bg-[var(--surface-muted)] hover:text-secondary"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
{group.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={group.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenGroups((prev) => ({
|
||||
...prev,
|
||||
[group.id]: !(expanded[group.id] ?? false),
|
||||
}))
|
||||
}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors",
|
||||
groupActive
|
||||
? "bg-[var(--surface-muted)] text-secondary"
|
||||
: "text-[var(--muted)] hover:bg-[var(--surface-muted)]"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1 text-right">{group.label}</span>
|
||||
<ChevronDown
|
||||
className={cn("h-4 w-4 transition-transform", isOpen && "rotate-180")}
|
||||
/>
|
||||
</button>
|
||||
{isOpen ? (
|
||||
<div className="mb-1 mr-3 space-y-0.5 border-r border-[var(--border)] pr-2">
|
||||
{group.items?.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={cn(
|
||||
"block rounded-lg px-3 py-1.5 text-xs font-medium transition-colors",
|
||||
isActive(pathname, item.href)
|
||||
? "bg-teal-600 text-white"
|
||||
: "text-[var(--muted)] hover:bg-[var(--surface-muted)]"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="mt-auto space-y-2 border-t border-[var(--border)] pt-4">
|
||||
<Link
|
||||
href="/payment/hub"
|
||||
className="block rounded-lg px-3 py-2 text-xs text-[var(--muted)] hover:text-secondary"
|
||||
>
|
||||
هاب تربتپی
|
||||
</Link>
|
||||
<Button variant="ghost" size="sm" className="w-full justify-start gap-2" onClick={toggle}>
|
||||
{mode === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
{mode === "dark" ? "حالت روشن" : "حالت تاریک"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[var(--canvas)]">
|
||||
<aside className="hidden w-64 shrink-0 flex-col border-l border-[var(--border)] bg-[var(--surface)] p-4 lg:flex">
|
||||
{sidebar}
|
||||
</aside>
|
||||
{mobileOpen ? (
|
||||
<div className="fixed inset-0 z-50 lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 bg-black/40"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
aria-label="بستن"
|
||||
/>
|
||||
<aside className="absolute right-0 top-0 flex h-full w-72 flex-col border-l border-[var(--border)] bg-[var(--surface)] p-4 shadow-lg">
|
||||
<button
|
||||
type="button"
|
||||
className="mb-2 self-start rounded-lg p-2"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
aria-label="بستن منو"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
{sidebar}
|
||||
</aside>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="flex items-center gap-2 border-b border-[var(--border)] bg-[var(--surface)] px-4 py-3 lg:hidden">
|
||||
<Button variant="ghost" size="sm" onClick={() => setMobileOpen(true)} aria-label="منو">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
<span className="font-medium text-secondary">{PAYMENT_PORTAL.label}</span>
|
||||
</header>
|
||||
<main className="flex-1 p-4 md:p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
frontend/modules/payment/components/PaymentStatusChip.tsx
Normal file
36
frontend/modules/payment/components/PaymentStatusChip.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const COLORS: Record<string, string> = {
|
||||
paid: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||
recorded: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||
active: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||
enabled: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||
healthy: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
||||
pending: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
||||
pending_review: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
||||
redirect_issued: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
|
||||
processing: "bg-sky-500/15 text-sky-700 dark:text-sky-300",
|
||||
draft: "bg-slate-500/15 text-slate-700 dark:text-slate-300",
|
||||
failed: "bg-rose-500/15 text-rose-700 dark:text-rose-300",
|
||||
cancelled: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
|
||||
expired: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
|
||||
suspended: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
|
||||
closed: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
|
||||
disabled: "bg-slate-500/15 text-slate-600 dark:text-slate-400",
|
||||
};
|
||||
|
||||
export function PaymentStatusChip({ status }: { status?: string | null }) {
|
||||
const key = (status || "unknown").toLowerCase();
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex rounded-md px-2 py-0.5 text-xs font-medium",
|
||||
COLORS[key] || "bg-[var(--surface-muted)] text-[var(--muted)]"
|
||||
)}
|
||||
>
|
||||
{status || "—"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
41
frontend/modules/payment/components/createPortalLayout.tsx
Normal file
41
frontend/modules/payment/components/createPortalLayout.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { AuthGuard } from "@/components/AuthGuard";
|
||||
import { useMe } from "@/hooks/useMe";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { LoadingState, ErrorState } from "@/components/ds";
|
||||
import { PaymentPortalShell } from "@/modules/payment/components/PaymentPortalShell";
|
||||
|
||||
function PortalGate({ children }: { children: React.ReactNode }) {
|
||||
const { me, loading, error, reload } = useMe();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (me?.onboarding_required) router.replace("/onboarding");
|
||||
}, [me, loading, router]);
|
||||
|
||||
if (loading) return <LoadingState label="در حال آمادهسازی تربتپی…" />;
|
||||
if (error) return <ErrorState message={error} onRetry={reload} />;
|
||||
if (!me?.current_tenant_id) {
|
||||
return <ErrorState message="برای استفاده از تربتپی workspace فعال لازم است." />;
|
||||
}
|
||||
|
||||
return <PaymentPortalShell>{children}</PaymentPortalShell>;
|
||||
}
|
||||
|
||||
export function PaymentRootLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const isHub = pathname === "/payment/hub";
|
||||
|
||||
if (isHub) {
|
||||
return <AuthGuard>{children}</AuthGuard>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthGuard>
|
||||
<PortalGate>{children}</PortalGate>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
27
frontend/modules/payment/constants/permissions.ts
Normal file
27
frontend/modules/payment/constants/permissions.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export const PAYMENT_VIEW = "payment.view";
|
||||
export const PAYMENT_MANAGE = "payment.manage";
|
||||
export const PAYMENT_AUDIT_VIEW = "payment.audit.view";
|
||||
export const PAYMENT_SETTINGS_MANAGE = "payment.settings.manage";
|
||||
export const PAYMENT_PSP_VIEW = "payment.psp_connections.view";
|
||||
export const PAYMENT_PSP_MANAGE = "payment.psp_connections.manage";
|
||||
export const PAYMENT_MERCHANT_VIEW = "payment.merchant_accounts.view";
|
||||
export const PAYMENT_MERCHANT_MANAGE = "payment.merchant_accounts.manage";
|
||||
export const PAYMENT_REQUEST_VIEW = "payment.requests.view";
|
||||
export const PAYMENT_REQUEST_CREATE = "payment.requests.create";
|
||||
export const PAYMENT_TX_VIEW = "payment.transactions.view";
|
||||
export const PAYMENT_LEDGER_VIEW = "payment.ledger.view";
|
||||
|
||||
export const ALL_PAYMENT_PERMISSIONS = [
|
||||
PAYMENT_VIEW,
|
||||
PAYMENT_MANAGE,
|
||||
PAYMENT_AUDIT_VIEW,
|
||||
PAYMENT_SETTINGS_MANAGE,
|
||||
PAYMENT_PSP_VIEW,
|
||||
PAYMENT_PSP_MANAGE,
|
||||
PAYMENT_MERCHANT_VIEW,
|
||||
PAYMENT_MERCHANT_MANAGE,
|
||||
PAYMENT_REQUEST_VIEW,
|
||||
PAYMENT_REQUEST_CREATE,
|
||||
PAYMENT_TX_VIEW,
|
||||
PAYMENT_LEDGER_VIEW,
|
||||
] as const;
|
||||
158
frontend/modules/payment/constants/portals.ts
Normal file
158
frontend/modules/payment/constants/portals.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
CreditCard,
|
||||
Building2,
|
||||
Settings,
|
||||
Shield,
|
||||
Activity,
|
||||
BarChart3,
|
||||
Wallet,
|
||||
Plug,
|
||||
FileText,
|
||||
BookOpen,
|
||||
KeyRound,
|
||||
Gauge,
|
||||
ScrollText,
|
||||
Network,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
|
||||
export type PortalNavItem = { href: string; label: string; bundle?: string | null };
|
||||
export type PortalNavGroup = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
href?: string;
|
||||
bundle?: string | null;
|
||||
items?: PortalNavItem[];
|
||||
};
|
||||
|
||||
export const PAYMENT_PORTAL = {
|
||||
id: "payment" as const,
|
||||
label: "تربتپی",
|
||||
description: "پلتفرم پرداخت سازمانی — BYO-PSP و پذیرنده",
|
||||
basePath: "/payment",
|
||||
icon: CreditCard,
|
||||
};
|
||||
|
||||
export const PAYMENT_NAV: PortalNavGroup[] = [
|
||||
{
|
||||
id: "dashboards",
|
||||
label: "داشبوردها",
|
||||
icon: LayoutDashboard,
|
||||
items: [
|
||||
{ href: "/payment/dashboard", label: "داشبورد اجرایی" },
|
||||
{ href: "/payment/overview", label: "نمای کلی" },
|
||||
{ href: "/payment/kpis", label: "شاخصهای کلیدی" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "operations",
|
||||
label: "عملیات پرداخت",
|
||||
icon: Wallet,
|
||||
items: [
|
||||
{ href: "/payment/requests", label: "درخواستهای پرداخت" },
|
||||
{ href: "/payment/transactions", label: "تراکنشها" },
|
||||
{ href: "/payment/ledger", label: "دفتر کل" },
|
||||
{ href: "/payment/callbacks", label: "کالبکها" },
|
||||
{ href: "/payment/verification", label: "تأیید پرداخت" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "psp",
|
||||
label: "درگاهها (PSP)",
|
||||
icon: Plug,
|
||||
bundle: "payment.byo_psp.basic",
|
||||
items: [
|
||||
{ href: "/payment/psp", label: "مدیریت درگاه", bundle: "payment.byo_psp.basic" },
|
||||
{ href: "/payment/psp/health", label: "سلامت درگاه", bundle: "payment.byo_psp.basic" },
|
||||
{ href: "/payment/providers", label: "تخصیص ارائهدهنده" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "merchants",
|
||||
label: "پذیرندگان",
|
||||
icon: Building2,
|
||||
bundle: "payment.torbat_pay.merchant",
|
||||
items: [
|
||||
{ href: "/payment/merchants", label: "حسابها", bundle: "payment.torbat_pay.merchant" },
|
||||
{ href: "/payment/merchants/approvals", label: "تأییدها", bundle: "payment.torbat_pay.merchant" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "licensing",
|
||||
label: "مجوز و باندل",
|
||||
icon: KeyRound,
|
||||
items: [
|
||||
{ href: "/payment/bundles", label: "باندلها" },
|
||||
{ href: "/payment/feature-toggles", label: "کلیدهای ویژگی" },
|
||||
{ href: "/payment/capabilities", label: "قابلیتها" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "ops",
|
||||
label: "عملیات پلتفرم",
|
||||
icon: Activity,
|
||||
items: [
|
||||
{ href: "/payment/monitoring", label: "مانیتورینگ" },
|
||||
{ href: "/payment/metrics", label: "متریکها" },
|
||||
{ href: "/payment/audit", label: "حسابرسی" },
|
||||
{ href: "/payment/settings", label: "تنظیمات" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "dev",
|
||||
label: "توسعهدهنده",
|
||||
icon: Network,
|
||||
items: [
|
||||
{ href: "/payment/api-explorer", label: "کاوشگر API" },
|
||||
{ href: "/payment/openapi", label: "OpenAPI" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function navForPayment() {
|
||||
return PAYMENT_NAV;
|
||||
}
|
||||
|
||||
export function filterNavByBundles(
|
||||
nav: PortalNavGroup[],
|
||||
activeBundles: string[] | undefined
|
||||
) {
|
||||
const bundles = new Set(activeBundles ?? []);
|
||||
return nav
|
||||
.map((g) => {
|
||||
if (g.bundle && !bundles.has(g.bundle)) return null;
|
||||
const items = g.items?.filter((i) => !i.bundle || bundles.has(i.bundle));
|
||||
if (g.items && (!items || items.length === 0)) return null;
|
||||
return { ...g, items };
|
||||
})
|
||||
.filter(Boolean) as PortalNavGroup[];
|
||||
}
|
||||
|
||||
export const MVP_ROUTE_HREFS = [
|
||||
"/payment/hub",
|
||||
"/payment/dashboard",
|
||||
"/payment/overview",
|
||||
"/payment/kpis",
|
||||
"/payment/transactions",
|
||||
"/payment/ledger",
|
||||
"/payment/psp",
|
||||
"/payment/psp/health",
|
||||
"/payment/providers",
|
||||
"/payment/merchants",
|
||||
"/payment/merchants/approvals",
|
||||
"/payment/requests",
|
||||
"/payment/callbacks",
|
||||
"/payment/verification",
|
||||
"/payment/audit",
|
||||
"/payment/bundles",
|
||||
"/payment/feature-toggles",
|
||||
"/payment/capabilities",
|
||||
"/payment/settings",
|
||||
"/payment/metrics",
|
||||
"/payment/monitoring",
|
||||
"/payment/api-explorer",
|
||||
"/payment/openapi",
|
||||
] as const;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user