diff --git a/.gitignore b/.gitignore index bed8176..ad19ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules/ !.env.example playwright-report/ test-results/ +.impeccable/review/ *.log .idea/ .vscode/ diff --git a/AGENT.md b/AGENT.md index e1154f0..70fecbd 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,4 +1,3 @@ # Compatibility Pointer The canonical agent instructions are in [`AGENTS.md`](AGENTS.md). This file exists for compatibility only. - diff --git a/DESIGN.md b/DESIGN.md index 5d13295..5c86dd3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -15,22 +15,22 @@ The authenticated panel screenshots are the primary behavioral reference: fixed ### Color tokens -| Token | Value | Use | -|---|---:|---| -| `--color-brand-600` | `#0A33FF` | Primary actions, active navigation, links, selected chart series | -| `--color-brand-700` | `#0828CC` | Hover/pressed brand state | -| `--color-accent-400` | `#FFD400` | Small ZarinPal-like accent, highlights; never body text on white | -| `--color-ink-950` | `#19191A` | Primary text, dark panels | -| `--color-ink-700` | `#3D3D42` | Secondary strong text | -| `--color-ink-500` | `#6B6B73` | Supporting text | -| `--color-canvas` | `#F1F1F3` | Application background | -| `--color-surface` | `#FFFFFF` | Cards, drawer, dialogs | -| `--color-surface-muted` | `#F7F7F8` | Nested panels, table headers | -| `--color-border` | `#E3E3E7` | Dividers and neutral outlines | -| `--color-success-600` | `#248061` | Success states and positive semantic data | -| `--color-warning-600` | `#A86700` | Warnings; paired with text/icon, never color alone | -| `--color-danger-500` | `#FF5C5D` | Errors and destructive actions | -| `--color-info-surface` | `#E7F2FF` | Informational callouts | +| Token | Value | Use | +| ----------------------- | --------: | ---------------------------------------------------------------- | +| `--color-brand-600` | `#0A33FF` | Primary actions, active navigation, links, selected chart series | +| `--color-brand-700` | `#0828CC` | Hover/pressed brand state | +| `--color-accent-400` | `#FFD400` | Small ZarinPal-like accent, highlights; never body text on white | +| `--color-ink-950` | `#19191A` | Primary text, dark panels | +| `--color-ink-700` | `#3D3D42` | Secondary strong text | +| `--color-ink-500` | `#6B6B73` | Supporting text | +| `--color-canvas` | `#F1F1F3` | Application background | +| `--color-surface` | `#FFFFFF` | Cards, drawer, dialogs | +| `--color-surface-muted` | `#F7F7F8` | Nested panels, table headers | +| `--color-border` | `#E3E3E7` | Dividers and neutral outlines | +| `--color-success-600` | `#248061` | Success states and positive semantic data | +| `--color-warning-600` | `#A86700` | Warnings; paired with text/icon, never color alone | +| `--color-danger-500` | `#FF5C5D` | Errors and destructive actions | +| `--color-info-surface` | `#E7F2FF` | Informational callouts | All text/background pairs must meet WCAG 2.2 AA contrast. Chart palettes require patterns, labels, or shapes in addition to hue. @@ -38,22 +38,22 @@ All text/background pairs must meet WCAG 2.2 AA contrast. Chart palettes require Light and dark modes are first-class variants of the same semantic token system. Components consume role tokens rather than raw palette values; dark mode is not a color inversion or an after-launch skin. -| Semantic role | Light | Dark | Notes | -|---|---:|---:|---| -| `--color-canvas` | `#F1F1F3` | `#0F0F10` | Application workspace | -| `--color-surface` | `#FFFFFF` | `#19191A` | Cards, drawers, dialogs | -| `--color-surface-muted` | `#F7F7F8` | `#28282A` | Nested panels and table headers | -| `--color-text-primary` | `#19191A` | `#FFFFFF` | Primary content | -| `--color-text-secondary` | `#3D3D42` | `#D1D1D6` | Supporting content | -| `--color-text-muted` | `#6B6B73` | `#A7A7AF` | Metadata; contrast must be verified at actual size | -| `--color-border` | `#E3E3E7` | `#3A3A3D` | Dividers and neutral outlines | -| `--color-brand-action` | `#0A33FF` | `#90AEF4` | Primary action and active state | -| `--color-on-brand` | `#FFFFFF` | `#0A0A0A` | Content on brand action | -| `--color-focus-ring` | `#0A33FF` | `#90AEF4` | Keyboard focus | -| `--color-semantic-success` | `#248061` | `#6FD6AE` | Positive values with text/icon support | -| `--color-semantic-warning` | `#8A5300` | `#F3BE5B` | Caution and incomplete verification | -| `--color-semantic-danger` | `#C93637` | `#FF8A8B` | Error and destructive meaning | -| `--color-semantic-info` | `#0A33FF` | `#90AEF4` | Informational emphasis | +| Semantic role | Light | Dark | Notes | +| -------------------------- | --------: | --------: | -------------------------------------------------- | +| `--color-canvas` | `#F1F1F3` | `#0F0F10` | Application workspace | +| `--color-surface` | `#FFFFFF` | `#19191A` | Cards, drawers, dialogs | +| `--color-surface-muted` | `#F7F7F8` | `#28282A` | Nested panels and table headers | +| `--color-text-primary` | `#19191A` | `#FFFFFF` | Primary content | +| `--color-text-secondary` | `#3D3D42` | `#D1D1D6` | Supporting content | +| `--color-text-muted` | `#6B6B73` | `#A7A7AF` | Metadata; contrast must be verified at actual size | +| `--color-border` | `#E3E3E7` | `#3A3A3D` | Dividers and neutral outlines | +| `--color-brand-action` | `#0A33FF` | `#90AEF4` | Primary action and active state | +| `--color-on-brand` | `#FFFFFF` | `#0A0A0A` | Content on brand action | +| `--color-focus-ring` | `#0A33FF` | `#90AEF4` | Keyboard focus | +| `--color-semantic-success` | `#248061` | `#6FD6AE` | Positive values with text/icon support | +| `--color-semantic-warning` | `#8A5300` | `#F3BE5B` | Caution and incomplete verification | +| `--color-semantic-danger` | `#C93637` | `#FF8A8B` | Error and destructive meaning | +| `--color-semantic-info` | `#0A33FF` | `#90AEF4` | Informational emphasis | - The initial theme follows `prefers-color-scheme` unless the user has explicitly selected light or dark. The explicit choice is persisted locally because it is presentation-only and non-sensitive. - Theme selection is applied before first paint to avoid a light/dark flash. The toggle remains keyboard and screen-reader operable and exposes its current state. @@ -183,26 +183,26 @@ Navigation labels should describe merchant tasks, not internal database concepts ## Current decisions and change log -| Version | Date | Decision | -|---|---|---| -| 0.4.0 | 2026-08-20 | Adopted the supplied current ZarinPal panel as the closer interaction reference: added the top wordmark bar, 256/72 px collapsible right sidebar, less-rounded 8 px data surfaces, routed sections, URL-backed period/terminal filters, payment-history chart, filtered transaction table, routed attempt history, formula help, and supportive human-readable insights. | -| 0.3.3 | 2026-08-20 | Replaced merchant/cohort revision freshness language with the approved immutable completed-import dataset snapshot. This changes trace labels only; cache age still does not imply stale data. | -| 0.3.2 | 2026-08-20 | Made the licensed Iran Yekan X FaNum variable WOFF2 and its required license file Git-tracked frontend assets, eliminating a separate private build-injection path. | -| 0.3.1 | 2026-08-20 | Verified the purchased Iran Yekan X Professional FaNum variable assets and required a private, reproducible injection path for public repositories. Added merchant-owned detail CSV export as an MVP component contract. | -| 0.3.0 | 2026-08-20 | Approved Iran Yekan X Professional FaNum variable typography, Persian digits, Rial-only values, and first-class light/dark semantic themes. Added the deterministic insight eligibility threshold. | -| 0.2.4 | 2026-08-20 | Fixed the static-demo default period, comparison fallback, overview KPI priority, and safe read-only drill-down scope; added accurate `NoAttempt`/`InBank` stage language from the CTO data guide. | -| 0.2.3 | 2026-08-20 | Kept authentication UX intentionally demo-sized: generic and throttled username/password login now, with a clean replacement boundary for ZarinPal SSO after acceptance. | -| 0.2.2 | 2026-08-20 | Defined backend-owned session UX: authentication tokens remain inaccessible to the frontend, and expiry/revocation returns the user to login with a generic, accessible message. | -| 0.2.1 | 2026-08-20 | Defined same-category peer cards as an equal-per-merchant median plus direction-aware percentile. This prevents the dataset's largest merchants from dominating what should represent a typical peer. | -| 0.2.0 | 2026-08-20 | Corrected payment lifecycle semantics from CTO guidance: `Paid` and `Verified` are bank-payment successes, `Paid` remains unverified, and session-level fees are real Rial amounts. | -| 0.1.9 | 2026-08-20 | Added rate-limit UX, 15-second SSE heartbeat/recovery, one-hour replay, and failed-section-only manual retry. | -| 0.1.8 | 2026-08-20 | Fixed the four parallel section ids, 30-day cache, idempotent start/SSE flow, and section timeout/retry behavior. | -| 0.1.7 | 2026-08-20 | Moved analytics results to high-TTL Redis and defined partial, parallel section delivery through authenticated SSE. | -| 0.1.6 | 2026-08-20 | Replaced time-based analytics freshness with lazy first-access calculation and revision-based merchant/cohort cache states; defined Saturday–Friday weeks. | -| 0.1.5 | 2026-08-20 | Removed holiday/event reporting; limited period controls to daily, weekly, Jalali-month, and Persian-season views. | -| 0.1.2 | 2026-08-20 | Added the insufficient-data state and visibility threshold for anonymized peer comparisons. | -| 0.1.1 | 2026-08-20 | Fixed merchant-facing time presentation to the Jalali calendar in `Asia/Tehran`, with explicit UTC API instants. | -| 0.1.0 | 2026-08-20 | Established ZarinPal-inspired RTL foundation from supplied public/panel screenshots; fixed SvelteKit/Svelte/TypeScript/Tailwind stack; defined responsive shell, core components, accessibility, chart, and motion rules. | +| Version | Date | Decision | +| ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 0.4.0 | 2026-08-20 | Adopted the supplied current ZarinPal panel as the closer interaction reference: added the top wordmark bar, 256/72 px collapsible right sidebar, less-rounded 8 px data surfaces, routed sections, URL-backed period/terminal filters, payment-history chart, filtered transaction table, routed attempt history, formula help, and supportive human-readable insights. | +| 0.3.3 | 2026-08-20 | Replaced merchant/cohort revision freshness language with the approved immutable completed-import dataset snapshot. This changes trace labels only; cache age still does not imply stale data. | +| 0.3.2 | 2026-08-20 | Made the licensed Iran Yekan X FaNum variable WOFF2 and its required license file Git-tracked frontend assets, eliminating a separate private build-injection path. | +| 0.3.1 | 2026-08-20 | Verified the purchased Iran Yekan X Professional FaNum variable assets and required a private, reproducible injection path for public repositories. Added merchant-owned detail CSV export as an MVP component contract. | +| 0.3.0 | 2026-08-20 | Approved Iran Yekan X Professional FaNum variable typography, Persian digits, Rial-only values, and first-class light/dark semantic themes. Added the deterministic insight eligibility threshold. | +| 0.2.4 | 2026-08-20 | Fixed the static-demo default period, comparison fallback, overview KPI priority, and safe read-only drill-down scope; added accurate `NoAttempt`/`InBank` stage language from the CTO data guide. | +| 0.2.3 | 2026-08-20 | Kept authentication UX intentionally demo-sized: generic and throttled username/password login now, with a clean replacement boundary for ZarinPal SSO after acceptance. | +| 0.2.2 | 2026-08-20 | Defined backend-owned session UX: authentication tokens remain inaccessible to the frontend, and expiry/revocation returns the user to login with a generic, accessible message. | +| 0.2.1 | 2026-08-20 | Defined same-category peer cards as an equal-per-merchant median plus direction-aware percentile. This prevents the dataset's largest merchants from dominating what should represent a typical peer. | +| 0.2.0 | 2026-08-20 | Corrected payment lifecycle semantics from CTO guidance: `Paid` and `Verified` are bank-payment successes, `Paid` remains unverified, and session-level fees are real Rial amounts. | +| 0.1.9 | 2026-08-20 | Added rate-limit UX, 15-second SSE heartbeat/recovery, one-hour replay, and failed-section-only manual retry. | +| 0.1.8 | 2026-08-20 | Fixed the four parallel section ids, 30-day cache, idempotent start/SSE flow, and section timeout/retry behavior. | +| 0.1.7 | 2026-08-20 | Moved analytics results to high-TTL Redis and defined partial, parallel section delivery through authenticated SSE. | +| 0.1.6 | 2026-08-20 | Replaced time-based analytics freshness with lazy first-access calculation and revision-based merchant/cohort cache states; defined Saturday–Friday weeks. | +| 0.1.5 | 2026-08-20 | Removed holiday/event reporting; limited period controls to daily, weekly, Jalali-month, and Persian-season views. | +| 0.1.2 | 2026-08-20 | Added the insufficient-data state and visibility threshold for anonymized peer comparisons. | +| 0.1.1 | 2026-08-20 | Fixed merchant-facing time presentation to the Jalali calendar in `Asia/Tehran`, with explicit UTC API instants. | +| 0.1.0 | 2026-08-20 | Established ZarinPal-inspired RTL foundation from supplied public/panel screenshots; fixed SvelteKit/Svelte/TypeScript/Tailwind stack; defined responsive shell, core components, accessibility, chart, and motion rules. | Any material token/component/interaction change increments this version and adds a row describing why it changed. A surprising or difficult-to-reverse change also receives an ADR. diff --git a/components.json b/components.json new file mode 100644 index 0000000..3644a57 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "neutral" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry", + "style": "nova", + "iconLibrary": "lucide", + "menuColor": "default", + "menuAccent": "subtle" +} diff --git a/docs/DASHBOARD-UX-SPEC.md b/docs/DASHBOARD-UX-SPEC.md index 883472c..bae1270 100644 --- a/docs/DASHBOARD-UX-SPEC.md +++ b/docs/DASHBOARD-UX-SPEC.md @@ -56,12 +56,12 @@ If an implementation-facing API description later conflicts with an approved sou ## 4. Actors and session model -| Actor | Entry | Visible merchant scope | Switching behavior | -|---|---|---|---| -| Demo merchant user | Assigned username and password | Exactly the merchant bound to the authenticated server session | Log out, then sign in with another seeded merchant account | -| Merchant user | Same MVP login adapter | Exactly one merchant | Same as demo user until ZarinPal SSO replaces the adapter | -| Judge/operator | Uses separately distributed seeded credentials | No hidden operator or cross-tenant browser privilege | Uses explicit logout/login between demonstrations | -| Finance/legal recipient | Receives an authorized CSV outside the dashboard workflow | Only fields in the merchant-owned exported detail view | No dashboard access is implied by receiving a file | +| Actor | Entry | Visible merchant scope | Switching behavior | +| ----------------------- | --------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | +| Demo merchant user | Assigned username and password | Exactly the merchant bound to the authenticated server session | Log out, then sign in with another seeded merchant account | +| Merchant user | Same MVP login adapter | Exactly one merchant | Same as demo user until ZarinPal SSO replaces the adapter | +| Judge/operator | Uses separately distributed seeded credentials | No hidden operator or cross-tenant browser privilege | Uses explicit logout/login between demonstrations | +| Finance/legal recipient | Receives an authorized CSV outside the dashboard workflow | Only fields in the merchant-owned exported detail view | No dashboard access is implied by receiving a file | The active merchant appears as identity context, not as a selector. The UI must not present a merchant dropdown that suggests the current session can change tenant scope. @@ -69,18 +69,18 @@ The active merchant appears as identity context, not as a selector. The UI must Technical route names are proposed for planning only; final route contracts remain subject to the approved interface and static-host fallback. -| Screen | Persian label | Primary task | Entry and exits | -|---|---|---|---| -| Login | ورود | Establish the demo merchant session | Entry from unauthenticated routes/session expiry; exit to overview | -| Overview | پیشخوان | Understand current outcomes and the most important qualifying changes | Default authenticated destination; links to focused analysis/detail | -| Payment performance | عملکرد پرداخت | Diagnose success, verification, failure, retry, and timing | From navigation/KPIs/insights; links to three approved detail views | -| Unverified-paid detail | پرداخت‌های تأییدنشده | Reconcile bank-paid sessions awaiting merchant verification | From verification card; CSV eligible | -| Failed-session detail | پرداخت‌های ناموفق | Review observed failure stages without invented causes | From failure card; CSV eligible | -| Retried-session detail | پرداخت‌های تکرارشده | Review attempt history and eventual recovery | From retry card; CSV eligible | -| Fees | کارمزد و درآمد | Understand real merchant fee amount and paid fee rate in Rial | From navigation/overview; no attempt-grain fee multiplication | -| Peer comparison | مقایسه با هم‌گروه‌ها | Compare approved metrics with eligible same-category merchants | Aggregate-only; never links to peer rows or peer export | -| Metric quality | کیفیت داده و شاخص‌ها | Understand definitions, periods, freshness, coverage, suppression, and limitations | From navigation and definition/provenance links | -| Account/session | حساب کاربری | Confirm current merchant identity, switch theme, or log out | No roles/profile editing in MVP | +| Screen | Persian label | Primary task | Entry and exits | +| ---------------------- | -------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| Login | ورود | Establish the demo merchant session | Entry from unauthenticated routes/session expiry; exit to overview | +| Overview | پیشخوان | Understand current outcomes and the most important qualifying changes | Default authenticated destination; links to focused analysis/detail | +| Payment performance | عملکرد پرداخت | Diagnose success, verification, failure, retry, and timing | From navigation/KPIs/insights; links to three approved detail views | +| Unverified-paid detail | پرداخت‌های تأییدنشده | Reconcile bank-paid sessions awaiting merchant verification | From verification card; CSV eligible | +| Failed-session detail | پرداخت‌های ناموفق | Review observed failure stages without invented causes | From failure card; CSV eligible | +| Retried-session detail | پرداخت‌های تکرارشده | Review attempt history and eventual recovery | From retry card; CSV eligible | +| Fees | کارمزد و درآمد | Understand real merchant fee amount and paid fee rate in Rial | From navigation/overview; no attempt-grain fee multiplication | +| Peer comparison | مقایسه با هم‌گروه‌ها | Compare approved metrics with eligible same-category merchants | Aggregate-only; never links to peer rows or peer export | +| Metric quality | کیفیت داده و شاخص‌ها | Understand definitions, periods, freshness, coverage, suppression, and limitations | From navigation and definition/provenance links | +| Account/session | حساب کاربری | Confirm current merchant identity, switch theme, or log out | No roles/profile editing in MVP | ### 5.1 Navigation hierarchy @@ -304,12 +304,12 @@ Example qualifying pattern: «نرخ تکمیل تأیید ۴٫۲ واحد در #### Recommended visual forms -| Question | Primary form | Exact alternative | -|---|---|---| +| Question | Primary form | Exact alternative | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | How are final session outcomes distributed? | Direct-labeled stacked bar; `Paid` and `Verified` visually distinct but grouped under bank-paid meaning | Semantic table of count, share, and definition | -| Is retry recovery changing? | Period trend line/columns with exact current rate and denominator | Period/value table | -| What are p50 and p95 times? | Paired dot/range plot per timing metric; no dual-axis chart | Table with p50, p95, sample, coverage, unit | -| Where did failed sessions observably reach? | Ordered horizontal bars for zero-attempt, reached-bank, and other observed stages | Stage/count table with caveat | +| Is retry recovery changing? | Period trend line/columns with exact current rate and denominator | Period/value table | +| What are p50 and p95 times? | Paired dot/range plot per timing metric; no dual-axis chart | Table with p50, p95, sample, coverage, unit | +| Where did failed sessions observably reach? | Ordered horizontal bars for zero-attempt, reached-bank, and other observed stages | Stage/count table with caveat | `NoAttempt` means zero normalized attempts, not a failed attempt. `InBank` means an observed gateway stage, not bank rejection. Timing nulls are excluded, never displayed as zero. Reversed is outside the success numerator and appears separately when non-zero. @@ -430,24 +430,24 @@ Overall progress is written as a contextual message such as «۲ از ۴ بخش ### 10.2 Section state matrix -| Contract/UI state | Visible content | Available actions | Accessibility behavior | -|---|---|---|---| -| Not requested | Section title and neutral placeholder only when the screen has not initiated that section | Apply valid filters/start access | No live announcement | -| Starting | Stable section frame; «در حال آماده‌سازی تحلیل…» | Controls that would duplicate start are busy/disabled | One overall polite status; `aria-busy` on affected region | -| Queued | Section title, period/unit context, layout-matched skeleton, «در صف محاسبه» | None | Status text remains available; skeleton is hidden from assistive tech | -| Calculating | Same stable frame, «در حال محاسبه…»; automatic backend retries remain in this state | None; no manual retry yet | Overall progress announces meaningful transitions, not animation frames | -| Ready | Exact KPI/takeaway/chart/table/trace metadata | Definition, approved drill-down/export | Focus does not move; one concise completion announcement when useful | -| Ready with no rows | Purpose-specific empty copy and period | Change period; no retry unless backend says failure | Announced as successful empty result, not error | -| Ready with unavailable comparison | Current exact metric plus «مبنای مقایسه در دسترس نیست» | Definition/change period | No trend icon, relative change, or insight claim | -| Ready with undefined rate | Numerator/denominator evidence and «نرخ قابل محاسبه نیست» | Definition/change period | Never spoken/displayed as zero | -| Suppressed | Peer-specific privacy/insufficient-data explanation; no peer values | Definition/change period | Status text explains that merchant data is unaffected | -| Failed after exhaustion | Section-local safe error; error/reference id only if approved and non-sensitive | «تلاش دوباره برای این بخش» | Error is associated with section heading; retry receives focus only if user initiated it | -| Manual retry active | Ready siblings unchanged; failed section returns to calculating with retry label | Retry disabled until terminal response | Polite status; no repeated countdown announcements | -| Rate-limited | Ready content retained; affected action explains wait and honors `Retry-After` | Retry becomes available only when permitted; no loop | Remaining time is not announced every second; announce initial limit and availability | -| Connection recovering | Ready content retained; unfinished sections show «در حال بازیابی اتصال…» | Optional explicit reconnect only after automatic strategy stops | One overall polite status; heartbeat events are silent | -| Replay expired | Ready content retained; «در حال همگام‌سازی آخرین وضعیت…» while current run snapshot loads | None during snapshot; safe retry on failure | Announce snapshot recovery outcome once | -| Unauthorized/session ended | Merchant data removed from active view and navigation returns to login | «ورود دوباره» | Focus lands on login heading; generic session-ended message | -| Forbidden/not found | No other-merchant existence detail; safe boundary message | Return to authorized overview | Page title and error are announced; no leaked identifier | +| Contract/UI state | Visible content | Available actions | Accessibility behavior | +| --------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Not requested | Section title and neutral placeholder only when the screen has not initiated that section | Apply valid filters/start access | No live announcement | +| Starting | Stable section frame; «در حال آماده‌سازی تحلیل…» | Controls that would duplicate start are busy/disabled | One overall polite status; `aria-busy` on affected region | +| Queued | Section title, period/unit context, layout-matched skeleton, «در صف محاسبه» | None | Status text remains available; skeleton is hidden from assistive tech | +| Calculating | Same stable frame, «در حال محاسبه…»; automatic backend retries remain in this state | None; no manual retry yet | Overall progress announces meaningful transitions, not animation frames | +| Ready | Exact KPI/takeaway/chart/table/trace metadata | Definition, approved drill-down/export | Focus does not move; one concise completion announcement when useful | +| Ready with no rows | Purpose-specific empty copy and period | Change period; no retry unless backend says failure | Announced as successful empty result, not error | +| Ready with unavailable comparison | Current exact metric plus «مبنای مقایسه در دسترس نیست» | Definition/change period | No trend icon, relative change, or insight claim | +| Ready with undefined rate | Numerator/denominator evidence and «نرخ قابل محاسبه نیست» | Definition/change period | Never spoken/displayed as zero | +| Suppressed | Peer-specific privacy/insufficient-data explanation; no peer values | Definition/change period | Status text explains that merchant data is unaffected | +| Failed after exhaustion | Section-local safe error; error/reference id only if approved and non-sensitive | «تلاش دوباره برای این بخش» | Error is associated with section heading; retry receives focus only if user initiated it | +| Manual retry active | Ready siblings unchanged; failed section returns to calculating with retry label | Retry disabled until terminal response | Polite status; no repeated countdown announcements | +| Rate-limited | Ready content retained; affected action explains wait and honors `Retry-After` | Retry becomes available only when permitted; no loop | Remaining time is not announced every second; announce initial limit and availability | +| Connection recovering | Ready content retained; unfinished sections show «در حال بازیابی اتصال…» | Optional explicit reconnect only after automatic strategy stops | One overall polite status; heartbeat events are silent | +| Replay expired | Ready content retained; «در حال همگام‌سازی آخرین وضعیت…» while current run snapshot loads | None during snapshot; safe retry on failure | Announce snapshot recovery outcome once | +| Unauthorized/session ended | Merchant data removed from active view and navigation returns to login | «ورود دوباره» | Focus lands on login heading; generic session-ended message | +| Forbidden/not found | No other-merchant existence detail; safe boundary message | Return to authorized overview | Page title and error are announced; no leaked identifier | ### 10.3 Stale/freshness language @@ -499,17 +499,17 @@ Chart rules: ### 11.3 Status vocabulary -| Data meaning | Persian display | Prohibited interpretation | -|---|---|---| -| `Verified` | تأییدشده | — | -| `Paid` | پرداخت‌شده، در انتظار تأیید پذیرنده | ناموفق | -| `Failed` | ناموفق | A specific cause unless the source proves it | -| `Reversed` | برگشت‌خورده | Successful/finalized | -| `NoAttempt` | بدون تلاش پرداخت | A failed attempt | -| `InBank` | رسیده به درگاه بانکی | ردشده توسط بانک | -| Undefined rate | نرخ قابل محاسبه نیست | `۰٪` | -| No rows | داده‌ای در این بازه وجود ندارد | System failure | -| Peer suppression | داده هم‌گروه کافی نیست | Merchant underperformance | +| Data meaning | Persian display | Prohibited interpretation | +| ---------------- | ----------------------------------- | -------------------------------------------- | +| `Verified` | تأییدشده | — | +| `Paid` | پرداخت‌شده، در انتظار تأیید پذیرنده | ناموفق | +| `Failed` | ناموفق | A specific cause unless the source proves it | +| `Reversed` | برگشت‌خورده | Successful/finalized | +| `NoAttempt` | بدون تلاش پرداخت | A failed attempt | +| `InBank` | رسیده به درگاه بانکی | ردشده توسط بانک | +| Undefined rate | نرخ قابل محاسبه نیست | `۰٪` | +| No rows | داده‌ای در این بازه وجود ندارد | System failure | +| Peer suppression | داده هم‌گروه کافی نیست | Merchant underperformance | ## 12. Persian RTL content and formatting @@ -587,18 +587,18 @@ This is a scope confirmation, not a destructive confirmation. Focus returns to t ### 14.3 Generation and download states -| State | UX behavior | -|---|---| -| Ready to request | Show scope summary; generation action enabled | -| Requesting | «در حال ثبت درخواست…»; prevent duplicate request | -| Generating | Keep detail table usable; show «فایل در حال آماده‌سازی است…» with period/unit; do not imply browser-side row assembly | -| Ready | Show «فایل آماده است» plus explicit «دانلود CSV»; download is tied to the authorized artifact/response | -| Empty | «برای فیلترهای انتخاب‌شده ردیفی برای خروجی وجود ندارد.»; no blank misleading file unless interface explicitly returns one | -| Rate-limited | Preserve table and generation context; honor `Retry-After`; no retry loop | -| Failed/recoverable | Safe reason category and «تلاش دوباره» for the same approved scope | -| Session ended | Cancel access to artifact, remove ready download action, return to login; re-login does not silently resume old download | -| Forbidden/not found | Do not confirm another merchant/artifact exists; return to authorized detail context | -| Filters changed | Invalidate the prior preflight scope; require a new explicit generation request for current filters | +| State | UX behavior | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Ready to request | Show scope summary; generation action enabled | +| Requesting | «در حال ثبت درخواست…»; prevent duplicate request | +| Generating | Keep detail table usable; show «فایل در حال آماده‌سازی است…» with period/unit; do not imply browser-side row assembly | +| Ready | Show «فایل آماده است» plus explicit «دانلود CSV»; download is tied to the authorized artifact/response | +| Empty | «برای فیلترهای انتخاب‌شده ردیفی برای خروجی وجود ندارد.»; no blank misleading file unless interface explicitly returns one | +| Rate-limited | Preserve table and generation context; honor `Retry-After`; no retry loop | +| Failed/recoverable | Safe reason category and «تلاش دوباره» for the same approved scope | +| Session ended | Cancel access to artifact, remove ready download action, return to login; re-login does not silently resume old download | +| Forbidden/not found | Do not confirm another merchant/artifact exists; return to authorized detail context | +| Filters changed | Invalidate the prior preflight scope; require a new explicit generation request for current filters | ### 14.4 Export content boundary @@ -668,18 +668,18 @@ Normative basis: [WCAG 2.2](https://www.w3.org/TR/WCAG22/). The focus-obscuring ## 16. Responsive behavior matrix -| Concern | Mobile <768 px | Tablet 768–1023 px | Desktop ≥1024 px | -|---|---|---|---| -| Navigation | Right drawer; compact header | Right drawer | Fixed right sidebar | -| Main gutter | 16 px | 20–24 px | 24–32 px | -| KPI layout | One column | One or two by content fit | Four headline cards in one row when space permits | -| Filter bar | Disclosure/full-width controls; explicit Apply | Two rows | One quiet row where content fits | -| Charts | ≥240 px; exact value first; table toggle | Full width or two-column by fit | 8/4 or full 12-column | -| Detail table | Priority row summary + disclosure; optional labeled horizontal table | Priority columns; controlled horizontal scroll | Full semantic table | -| Export preflight | Full-screen dialog/sheet respecting safe areas | Modal/inline panel | Modal/inline panel near action | -| Insights | Stack after relevant KPI | Stack or companion column | Companion 4-column panel where useful | -| Merchant identity | Two-line compact context | Compact context | Full sidebar block | -| Theme control | Accessible icon+name in drawer/header | Header/drawer | Utility area | +| Concern | Mobile <768 px | Tablet 768–1023 px | Desktop ≥1024 px | +| ----------------- | -------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------- | +| Navigation | Right drawer; compact header | Right drawer | Fixed right sidebar | +| Main gutter | 16 px | 20–24 px | 24–32 px | +| KPI layout | One column | One or two by content fit | Four headline cards in one row when space permits | +| Filter bar | Disclosure/full-width controls; explicit Apply | Two rows | One quiet row where content fits | +| Charts | ≥240 px; exact value first; table toggle | Full width or two-column by fit | 8/4 or full 12-column | +| Detail table | Priority row summary + disclosure; optional labeled horizontal table | Priority columns; controlled horizontal scroll | Full semantic table | +| Export preflight | Full-screen dialog/sheet respecting safe areas | Modal/inline panel | Modal/inline panel near action | +| Insights | Stack after relevant KPI | Stack or companion column | Companion 4-column panel where useful | +| Merchant identity | Two-line compact context | Compact context | Full sidebar block | +| Theme control | Accessible icon+name in drawer/header | Header/drawer | Utility area | Acceptance widths: 360 px, 768 px, 1024 px, and 1440 px; include 320 CSS px reflow/zoom verification. @@ -687,21 +687,21 @@ Acceptance widths: 360 px, 768 px, 1024 px, and 1440 px; include 320 CSS px refl All strings are complete messages; variable insertion must preserve Persian grammar and bidi isolation. -| Situation | Approved pattern | -|---|---| -| Overall progress | «{آماده} از ۴ بخش تحلیل آماده است.» | -| Calculating | «این بخش در حال محاسبه است…» | -| Recovering SSE | «ارتباط با تحلیل قطع شد؛ در حال بازیابی اتصال…» | -| Snapshot fallback | «در حال همگام‌سازی آخرین وضعیت تحلیل…» | -| Section failure | «محاسبه این بخش انجام نشد. می‌توانید فقط همین بخش را دوباره اجرا کنید.» | -| Rate limited | «درخواست‌های زیادی ثبت شده است. پس از {زمان} دوباره تلاش کنید.» | -| Baseline unavailable | «مبنای مقایسه برای این بازه در دسترس نیست.» | -| Undefined rate | «به‌دلیل نبود مخرج معتبر، این نرخ قابل محاسبه نیست.» | -| Empty period | «در بازه انتخاب‌شده داده‌ای برای این شاخص وجود ندارد.» | -| Session ended | «نشست شما پایان یافته است. برای ادامه دوباره وارد شوید.» | -| Safe forbidden | «به این اطلاعات دسترسی ندارید. به پیشخوان حساب خود برگردید.» | -| Export ready | «فایل CSV برای بازه انتخاب‌شده آماده است.» | -| Export failed | «ساخت فایل انجام نشد. می‌توانید همین درخواست را دوباره ثبت کنید.» | +| Situation | Approved pattern | +| -------------------- | ----------------------------------------------------------------------- | +| Overall progress | «{آماده} از ۴ بخش تحلیل آماده است.» | +| Calculating | «این بخش در حال محاسبه است…» | +| Recovering SSE | «ارتباط با تحلیل قطع شد؛ در حال بازیابی اتصال…» | +| Snapshot fallback | «در حال همگام‌سازی آخرین وضعیت تحلیل…» | +| Section failure | «محاسبه این بخش انجام نشد. می‌توانید فقط همین بخش را دوباره اجرا کنید.» | +| Rate limited | «درخواست‌های زیادی ثبت شده است. پس از {زمان} دوباره تلاش کنید.» | +| Baseline unavailable | «مبنای مقایسه برای این بازه در دسترس نیست.» | +| Undefined rate | «به‌دلیل نبود مخرج معتبر، این نرخ قابل محاسبه نیست.» | +| Empty period | «در بازه انتخاب‌شده داده‌ای برای این شاخص وجود ندارد.» | +| Session ended | «نشست شما پایان یافته است. برای ادامه دوباره وارد شوید.» | +| Safe forbidden | «به این اطلاعات دسترسی ندارید. به پیشخوان حساب خود برگردید.» | +| Export ready | «فایل CSV برای بازه انتخاب‌شده آماده است.» | +| Export failed | «ساخت فایل انجام نشد. می‌توانید همین درخواست را دوباره ثبت کنید.» | Persian digits are applied to rendered variable values. Technical identifiers inside an error/support reference remain Latin and isolated LTR. diff --git a/package-lock.json b/package-lock.json index 52ec8db..66b2c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,20 +8,30 @@ "name": "zarinpal-analytics-frontend", "version": "0.1.0", "devDependencies": { + "@fontsource-variable/geist": "^5.3.0", + "@internationalized/date": "^3.12.3", + "@lucide/svelte": "^1.33.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.37.0", "@sveltejs/vite-plugin-svelte": "^6.1.0", "@tailwindcss/vite": "^4.1.12", "@testing-library/jest-dom": "^6.8.0", "@testing-library/svelte": "^5.2.8", + "bits-ui": "^2.19.0", + "clsx": "^2.1.1", "jsdom": "^26.1.0", + "layerchart": "^2.2.0", "openapi-typescript": "^7.9.1", "prettier": "3.6.2", "prettier-plugin-svelte": "3.4.0", "prettier-plugin-tailwindcss": "0.6.14", + "shadcn-svelte": "^1.5.0", "svelte": "^5.38.7", "svelte-check": "^4.3.1", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.1", "tailwindcss": "^4.1.12", + "tw-animate-css": "^1.4.0", "typescript": "^5.9.2", "vite": "^7.1.3", "vitest": "^3.2.4" @@ -198,6 +208,23 @@ "node": ">=18" } }, + "node_modules/@dagrejs/dagre": { + "version": "2.0.4", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@dagrejs/dagre/-/dagre-2.0.4.tgz", + "integrity": "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "3.0.4" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "3.0.4", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@dagrejs/graphlib/-/graphlib-3.0.4.tgz", + "integrity": "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -640,6 +667,54 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@fontsource-variable/geist": { + "version": "5.3.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@fontsource-variable/geist/-/geist-5.3.0.tgz", + "integrity": "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.3", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@internationalized/date/-/date-3.12.3.tgz", + "integrity": "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -690,6 +765,87 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@layerstack/svelte-actions": { + "version": "1.0.1-next.18", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@layerstack/svelte-actions/-/svelte-actions-1.0.1-next.18.tgz", + "integrity": "sha512-gxPzCnJ1c9LTfWtRqLUzefCx+k59ZpxDUQ2XB+LokveZQPe7IDSOwHaBOEMlaGoGrtwc3Ft8dSZq+2WT2o9u/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.0", + "@layerstack/utils": "2.0.0-next.18", + "d3-scale": "^4.0.2" + } + }, + "node_modules/@layerstack/svelte-actions/node_modules/@layerstack/utils": { + "version": "2.0.0-next.18", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@layerstack/utils/-/utils-2.0.0-next.18.tgz", + "integrity": "sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "d3-time-format": "^4.1.0" + } + }, + "node_modules/@layerstack/svelte-state": { + "version": "0.1.0-next.23", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@layerstack/svelte-state/-/svelte-state-0.1.0-next.23.tgz", + "integrity": "sha512-7O4umv+gXwFfs3/vjzFWYHNXGwYnnjBapWJ5Y+9u99F4eVk6rh4ocNwqkqQNkpMZ5tUJBlRTWjPE1So6+hEzIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@layerstack/utils": "2.0.0-next.18" + } + }, + "node_modules/@layerstack/svelte-state/node_modules/@layerstack/utils": { + "version": "2.0.0-next.18", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@layerstack/utils/-/utils-2.0.0-next.18.tgz", + "integrity": "sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "d3-time-format": "^4.1.0" + } + }, + "node_modules/@layerstack/tailwind": { + "version": "2.0.0-next.21", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@layerstack/tailwind/-/tailwind-2.0.0-next.21.tgz", + "integrity": "sha512-Qgp2EpmEHmjtura8MQzWicR6ztBRSsRvddakFtx9ShrLMz6jWzd6bCMVVRu44Q3ZOrtXmSu4QxjCZWu1ytvuPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@layerstack/utils": "^2.0.0-next.18", + "clsx": "^2.1.1", + "d3-array": "^3.2.4", + "tailwind-merge": "^3.2.0" + } + }, + "node_modules/@layerstack/utils": { + "version": "2.0.0-next.19", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@layerstack/utils/-/utils-2.0.0-next.19.tgz", + "integrity": "sha512-HyXCEz9pge4rFWklrSgFr3YIwdE1gP8WM6GPetmnKPDfYLF1/oetRFKI9FqiE+dw4brnULGPKJsVhjM3vQfD4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "d3-time-format": "^4.1.0" + } + }, + "node_modules/@lucide/svelte": { + "version": "1.33.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@lucide/svelte/-/svelte-1.33.0.tgz", + "integrity": "sha512-b+osTYG2V4dge5Lr7tnaTaahhy/4vH7Xb8zcqVjmctjwgkpXS0NNOJPkGzHHZoxtw/OO+2YAU28omeMfYCaawg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1228,6 +1384,16 @@ "vite": "^6.3.0 || ^7.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@tailwindcss/node/-/node-4.3.3.tgz", @@ -1622,6 +1788,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1636,6 +1820,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1858,6 +2049,31 @@ "dev": true, "license": "MIT" }, + "node_modules/bits-ui": { + "version": "2.19.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/bits-ui/-/bits-ui-2.19.0.tgz", + "integrity": "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, "node_modules/brace-expansion": { "version": "2.1.4", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/brace-expansion/-/brace-expansion-2.1.4.tgz", @@ -1945,6 +2161,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/cookie/-/cookie-0.6.0.tgz", @@ -1976,6 +2202,384 @@ "node": ">=18" } }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-voronoi": { + "version": "2.1.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-geo-voronoi/-/d3-geo-voronoi-2.1.0.tgz", + "integrity": "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-delaunay": "6", + "d3-geo": "3", + "d3-tricontour": "1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate-path": { + "version": "2.3.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-interpolate-path/-/d3-interpolate-path-2.3.0.tgz", + "integrity": "sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://package-mirror.liara.ir/repository/npm/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "dev": true, + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-tile": { + "version": "1.0.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-tile/-/d3-tile-1.0.0.tgz", + "integrity": "sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-tricontour": { + "version": "1.1.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/d3-tricontour/-/d3-tricontour-1.1.0.tgz", + "integrity": "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-delaunay": "6", + "d3-scale": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/data-urls/-/data-urls-5.0.0.tgz", @@ -2035,6 +2639,16 @@ "node": ">=0.10.0" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://package-mirror.liara.ir/repository/npm/dequal/-/dequal-2.0.3.tgz", @@ -2314,6 +2928,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://package-mirror.liara.ir/repository/npm/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://package-mirror.liara.ir/repository/npm/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -2438,6 +3069,77 @@ "node": ">=6" } }, + "node_modules/layerchart": { + "version": "2.2.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/layerchart/-/layerchart-2.2.0.tgz", + "integrity": "sha512-IarOANUPAqv+P9UESpQhzlkiM36oQhUACZuE3T9wbFHoD8FtSBdr/UNRC4Aks2UBN/OEy/dE+xBypZ6RFoFLMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dagrejs/dagre": "^2.0.4", + "@layerstack/svelte-actions": "1.0.1-next.18", + "@layerstack/svelte-state": "0.1.0-next.23", + "@layerstack/tailwind": "2.0.0-next.21", + "@layerstack/utils": "2.0.0-next.19", + "@types/d3-contour": "^3.0.6", + "d3-array": "^3.2.4", + "d3-chord": "^3.0.1", + "d3-color": "^3.1.0", + "d3-contour": "^4.0.2", + "d3-delaunay": "^6.0.4", + "d3-dsv": "^3.0.1", + "d3-force": "^3.0.0", + "d3-geo": "^3.1.1", + "d3-geo-voronoi": "^2.1.0", + "d3-hierarchy": "^3.1.2", + "d3-interpolate": "^3.0.1", + "d3-interpolate-path": "^2.3.0", + "d3-path": "^3.1.0", + "d3-polygon": "^3.0.1", + "d3-quadtree": "^3.0.1", + "d3-random": "^3.0.1", + "d3-sankey": "^0.12.3", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "d3-shape": "^3.2.0", + "d3-tile": "^1.0.0", + "d3-time": "^3.1.0", + "memoize": "^10.2.0", + "runed": "^0.37.1" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/layerchart/node_modules/runed": { + "version": "0.37.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/runed/-/runed-0.37.1.tgz", + "integrity": "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0", + "zod": "^4.1.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2740,6 +3442,35 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/memoize": { + "version": "10.2.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/memoize/-/memoize-10.2.0.tgz", + "integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://package-mirror.liara.ir/repository/npm/min-indent/-/min-indent-1.0.1.tgz", @@ -2809,6 +3540,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, "node_modules/nwsapi": { "version": "2.2.24", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/nwsapi/-/nwsapi-2.2.24.tgz", @@ -3142,6 +3880,13 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/rollup/-/rollup-4.62.4.tgz", @@ -3195,6 +3940,38 @@ "dev": true, "license": "MIT" }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/sade/-/sade-1.8.1.tgz", @@ -3235,6 +4012,25 @@ "dev": true, "license": "MIT" }, + "node_modules/shadcn-svelte": { + "version": "1.5.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/shadcn-svelte/-/shadcn-svelte-1.5.0.tgz", + "integrity": "sha512-fcxTeUwEIvELkL/h0Vg2i/9Sf6exAmLG5u3GXc0aN8CA+3fNffKQnfrTdMgkjUEmx9XP619I+wcJOvlr5aDWYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.0", + "node-fetch-native": "^1.6.4", + "postcss": "^8.5.10", + "tailwind-merge": "^3.6.0" + }, + "bin": { + "shadcn-svelte": "dist/index.mjs" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://package-mirror.liara.ir/repository/npm/siginfo/-/siginfo-2.0.0.tgz", @@ -3314,6 +4110,16 @@ "dev": true, "license": "MIT" }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/supports-color/-/supports-color-10.2.2.tgz", @@ -3380,6 +4186,27 @@ "typescript": "^5.0.0 || ^6.0.0" } }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, "node_modules/svelte/node_modules/aria-query": { "version": "5.3.1", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/aria-query/-/aria-query-5.3.1.tgz", @@ -3397,6 +4224,47 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.3.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/tailwind-variants/-/tailwind-variants-3.3.1.tgz", + "integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -3535,6 +4403,23 @@ "node": ">=18" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/type-fest/-/type-fest-4.41.0.tgz", diff --git a/package.json b/package.json index d7da922..5f0d43e 100644 --- a/package.json +++ b/package.json @@ -15,20 +15,30 @@ "generate:api": "openapi-typescript ../zarinpal-analytics-backend/docs/openapi.yaml -o src/lib/api/schema.d.ts" }, "devDependencies": { + "@fontsource-variable/geist": "^5.3.0", + "@internationalized/date": "^3.12.3", + "@lucide/svelte": "^1.33.0", "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/kit": "^2.37.0", "@sveltejs/vite-plugin-svelte": "^6.1.0", "@tailwindcss/vite": "^4.1.12", "@testing-library/jest-dom": "^6.8.0", "@testing-library/svelte": "^5.2.8", + "bits-ui": "^2.19.0", + "clsx": "^2.1.1", "jsdom": "^26.1.0", + "layerchart": "^2.2.0", "openapi-typescript": "^7.9.1", "prettier": "3.6.2", "prettier-plugin-svelte": "3.4.0", "prettier-plugin-tailwindcss": "0.6.14", + "shadcn-svelte": "^1.5.0", "svelte": "^5.38.7", "svelte-check": "^4.3.1", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.1", "tailwindcss": "^4.1.12", + "tw-animate-css": "^1.4.0", "typescript": "^5.9.2", "vite": "^7.1.3", "vitest": "^3.2.4" diff --git a/src/app.css b/src/app.css index 34178ba..639c2f0 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,8 @@ @import 'tailwindcss'; +@import 'tw-animate-css'; +@import 'shadcn-svelte/tailwind.css'; + +@custom-variant dark (&:is(.dark *)); @font-face { font-family: 'IRANYekanX'; @@ -14,8 +18,8 @@ --surface-muted: #f7f7f8; --text: #19191a; --text-secondary: #3d3d42; - --muted: #6b6b73; - --border: #e3e3e7; + --muted: #f3f4f6; + --border: #e4e4e7; --brand: #0a33ff; --brand-hover: #0828cc; --on-brand: #fff; @@ -23,9 +27,40 @@ --success: #248061; --warning: #8a5300; --danger: #c93637; + --background: #f5f5f6; + --foreground: #18181b; + --card: #ffffff; + --card-foreground: #18181b; + --popover: #ffffff; + --popover-foreground: #18181b; + --primary: #0a33ff; + --primary-foreground: #ffffff; + --secondary: #f3f4f6; + --secondary-foreground: #27272a; + --muted-foreground: #71717a; + --accent: #eef1ff; + --accent-foreground: #0a33ff; + --destructive: oklch(0.577 0.245 27.325); + --input: #e4e4e7; + --ring: #0a33ff; + --chart-1: #0a33ff; + --chart-2: #18a779; + --chart-3: #f5b800; + --chart-4: #7657e8; + --chart-5: #f36b7f; + --radius: 0.5rem; + --sidebar: #ffffff; + --sidebar-foreground: #27272a; + --sidebar-primary: #0a33ff; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #eef1ff; + --sidebar-accent-foreground: #0a33ff; + --sidebar-border: #e4e4e7; + --sidebar-ring: #0a33ff; } -:root[data-theme='dark'] { +:root[data-theme='dark'], +.dark { color-scheme: dark; --canvas: #0f0f10; --surface: #19191a; @@ -41,6 +76,30 @@ --success: #6fd6ae; --warning: #f3be5b; --danger: #ff8a8b; + --background: #101012; + --foreground: #fafafa; + --card: #19191c; + --card-foreground: #fafafa; + --popover: #19191c; + --popover-foreground: #fafafa; + --primary: #6f8dff; + --primary-foreground: #080b18; + --secondary: #27272a; + --secondary-foreground: #fafafa; + --muted: #27272a; + --muted-foreground: #a1a1aa; + --accent: #202950; + --accent-foreground: #a9baff; + --input: #3f3f46; + --ring: #90aef4; + --sidebar: #18181b; + --sidebar-foreground: #f4f4f5; + --sidebar-primary: #6f8dff; + --sidebar-primary-foreground: #080b18; + --sidebar-accent: #202950; + --sidebar-accent-foreground: #a9baff; + --sidebar-border: #303036; + --sidebar-ring: #90aef4; } * { @@ -93,7 +152,7 @@ select { .button-primary { min-height: 44px; border: 0; - border-radius: 10px; + border-radius: 7px; padding: 0 20px; background: var(--brand); color: var(--on-brand); @@ -116,7 +175,7 @@ select { .button-secondary { min-height: 44px; border: 1px solid var(--border); - border-radius: 10px; + border-radius: 7px; padding: 0 16px; background: var(--surface); color: var(--text); @@ -126,7 +185,7 @@ select { .card { background: var(--surface); border: 1px solid var(--border); - border-radius: 14px; + border-radius: 8px; } .skeleton { overflow: hidden; @@ -161,3 +220,57 @@ select { transition-duration: 0.01ms !important; } } + +@theme inline { + --font-sans: 'IRANYekanX', Tahoma, sans-serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/src/app.html b/src/app.html index 8c479d7..fb472a3 100644 --- a/src/app.html +++ b/src/app.html @@ -11,6 +11,7 @@ const saved = localStorage.getItem('zpa-theme'); const dark = saved ? saved === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.dataset.theme = dark ? 'dark' : 'light'; + document.documentElement.classList.toggle('dark', dark); } catch (_) {} })(); diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index bfe2065..a2399a2 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -4,6 +4,10 @@ export type AuthSession = components['schemas']['AuthSession']; export type ReportingPeriod = components['schemas']['ReportingPeriod']; export type AnalyticsRun = components['schemas']['AnalyticsRun']; export type SessionDetailPage = components['schemas']['SessionDetailPage']; +export type SessionDetail = components['schemas']['SessionDetail']; +export type Terminal = components['schemas']['Terminal']; +export type TerminalList = components['schemas']['TerminalList']; +export type SessionStatus = components['schemas']['SessionStatus']; export type DetailView = components['schemas']['DetailView']; export type FailureStage = components['schemas']['FailureStage']; export type Problem = components['schemas']['Problem']; @@ -61,14 +65,17 @@ export const api = { periods() { return request('/analytics/periods'); }, - startRun(periodId: string, csrfToken: string) { + terminals() { + return request('/analytics/terminals'); + }, + startRun(periodId: string, csrfToken: string, terminalId?: string) { return request('/analytics/runs', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken, 'Idempotency-Key': crypto.randomUUID(), }, - body: JSON.stringify({ periodId }), + body: JSON.stringify({ periodId, ...(terminalId ? { terminalId } : {}) }), }); }, run(runId: string) { @@ -86,15 +93,49 @@ export const api = { }, ); }, - details(periodId: string, view: DetailView, cursor?: string, failureStage?: FailureStage) { + details( + periodId: string, + view: DetailView, + options: { + cursor?: string; + failureStage?: FailureStage; + terminalId?: string; + status?: SessionStatus; + minAmountRial?: number; + maxAmountRial?: number; + limit?: number; + } = {}, + ) { const query = new URLSearchParams({ periodId, view, limit: '25' }); - if (cursor) query.set('cursor', cursor); - if (failureStage) query.set('failureStage', failureStage); + if (options.cursor) query.set('cursor', options.cursor); + if (options.failureStage) query.set('failureStage', options.failureStage); + if (options.terminalId) query.set('terminalId', options.terminalId); + if (options.status) query.set('status', options.status); + if (options.minAmountRial !== undefined) query.set('minAmountRial', String(options.minAmountRial)); + if (options.maxAmountRial !== undefined) query.set('maxAmountRial', String(options.maxAmountRial)); + if (options.limit) query.set('limit', String(options.limit)); return request(`/analytics/session-details?${query}`); }, - exportUrl(periodId: string, view: DetailView, failureStage?: FailureStage) { + sessionDetail(sessionId: string) { + return request(`/analytics/sessions/${encodeURIComponent(sessionId)}`); + }, + exportUrl( + periodId: string, + view: DetailView, + options: { + failureStage?: FailureStage; + terminalId?: string; + status?: SessionStatus; + minAmountRial?: number; + maxAmountRial?: number; + } = {}, + ) { const query = new URLSearchParams({ periodId, view }); - if (failureStage) query.set('failureStage', failureStage); + if (options.failureStage) query.set('failureStage', options.failureStage); + if (options.terminalId) query.set('terminalId', options.terminalId); + if (options.status) query.set('status', options.status); + if (options.minAmountRial !== undefined) query.set('minAmountRial', String(options.minAmountRial)); + if (options.maxAmountRial !== undefined) query.set('maxAmountRial', String(options.maxAmountRial)); return `${apiBase}/analytics/session-details.csv?${query}`; }, }; diff --git a/src/lib/api/schema.d.ts b/src/lib/api/schema.d.ts index e44cbfb..bc24835 100644 --- a/src/lib/api/schema.d.ts +++ b/src/lib/api/schema.d.ts @@ -4,1352 +4,1447 @@ */ export interface paths { - '/auth/sessions': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/auth/sessions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Authenticate a demo merchant user + * @description Creates a revocable server-side session. Expected credential failures are + * externally indistinguishable. The response sets the host-only + * `__Host-zpa_session` cookie; the raw token is never present in JSON. + */ + post: operations["createAuthSession"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** - * Authenticate a demo merchant user - * @description Creates a revocable server-side session. Expected credential failures are - * externally indistinguishable. The response sets the host-only - * `__Host-zpa_session` cookie; the raw token is never present in JSON. - */ - post: operations['createAuthSession']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/auth/session': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/auth/session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get the current session summary */ + get: operations["getAuthSession"]; + put?: never; + post?: never; + /** + * Revoke the current session + * @description Revokes server-side state before expiring the browser cookie. + */ + delete: operations["deleteAuthSession"]; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get the current session summary */ - get: operations['getAuthSession']; - put?: never; - post?: never; - /** - * Revoke the current session - * @description Revokes server-side state before expiring the browser cookie. - */ - delete: operations['deleteAuthSession']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/periods': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/analytics/periods": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List approved reporting periods + * @description Returns allowlisted periods with server-calculated UTC and Tehran-local + * half-open boundaries. Clients start runs with a returned `periodId` and + * never submit arbitrary timestamps. + */ + get: operations["listAnalyticsPeriods"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * List approved reporting periods - * @description Returns allowlisted periods with server-calculated UTC and Tehran-local - * half-open boundaries. Clients start runs with a returned `periodId` and - * never submit arbitrary timestamps. - */ - get: operations['listAnalyticsPeriods']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/runs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/analytics/terminals": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List terminals owned by the authenticated merchant + * @description Returns display-safe terminal choices scoped exclusively from the authenticated server session. + */ + get: operations["listMerchantTerminals"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** - * Create or return the current analytics run - * @description Idempotently resolves the current merchant/dataset/metric version - * and starts only missing sections. Equivalent concurrent starts deduplicate. - */ - post: operations['createAnalyticsRun']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/runs/{runId}': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components['parameters']['RunIdPath']; - }; - cookie?: never; + "/analytics/runs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create or return the current analytics run + * @description Idempotently resolves the current merchant/dataset/metric version + * and starts only missing sections. Equivalent concurrent starts deduplicate. + */ + post: operations["createAnalyticsRun"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get the current run snapshot */ - get: operations['getAnalyticsRun']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/runs/{runId}/events': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components['parameters']['RunIdPath']; - }; - cookie?: never; + "/analytics/runs/{runId}": { + parameters: { + query?: never; + header?: never; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components["parameters"]["RunIdPath"]; + }; + cookie?: never; + }; + /** Get the current run snapshot */ + get: operations["getAnalyticsRun"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * Stream partial analytics results - * @description Authenticated SSE stream. Events are retained for one hour. A retained - * `Last-Event-ID` replays only later events. Missing, invalid, unauthorized, - * or expired ids reset safely by emitting `run.snapshot` with - * `recovery=RESET_TO_SNAPSHOT`, then continuing from new events. - * - * Event types are `run.snapshot`, `section.started`, `section.ready`, - * `section.failed`, `section.suppressed`, `run.completed`, and `heartbeat`. - * Each event has an opaque `id` and one-line JSON `data` matching one of the - * schemas named by `AnalyticsSseEvent`. - */ - get: operations['streamAnalyticsRunEvents']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/runs/{runId}/sections/{sectionId}/retry-attempts': { - parameters: { - query?: never; - header?: never; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components['parameters']['RunIdPath']; - sectionId: components['parameters']['SectionIdPath']; - }; - cookie?: never; + "/analytics/runs/{runId}/events": { + parameters: { + query?: never; + header?: never; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components["parameters"]["RunIdPath"]; + }; + cookie?: never; + }; + /** + * Stream partial analytics results + * @description Authenticated SSE stream. Events are retained for one hour. A retained + * `Last-Event-ID` replays only later events. Missing, invalid, unauthorized, + * or expired ids reset safely by emitting `run.snapshot` with + * `recovery=RESET_TO_SNAPSHOT`, then continuing from new events. + * + * Event types are `run.snapshot`, `section.started`, `section.ready`, + * `section.failed`, `section.suppressed`, `run.completed`, and `heartbeat`. + * Each event has an opaque `id` and one-line JSON `data` matching one of the + * schemas named by `AnalyticsSseEvent`. + */ + get: operations["streamAnalyticsRunEvents"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** - * Retry one exhausted failed section - * @description Does not clear or restart ready sibling sections. - */ - post: operations['createAnalyticsSectionRetryAttempt']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/session-details': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/analytics/runs/{runId}/sections/{sectionId}/retry-attempts": { + parameters: { + query?: never; + header?: never; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components["parameters"]["RunIdPath"]; + sectionId: components["parameters"]["SectionIdPath"]; + }; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Retry one exhausted failed section + * @description Does not clear or restart ready sibling sections. + */ + post: operations["createAnalyticsSectionRetryAttempt"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * List authorized merchant-owned session evidence - * @description Cursor-paginated detail rows for one approved period and view. Stable - * ordering is `createdAt DESC, sessionId DESC`. The server binds cursors to - * merchant, filters, and ordering. Cross-tenant and absent ids are not - * distinguishable. - */ - get: operations['listAnalyticsSessionDetails']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/analytics/session-details.csv': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/analytics/session-details": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List authorized merchant-owned session evidence + * @description Cursor-paginated detail rows for one approved period and view. Stable + * ordering is `createdAt DESC, sessionId DESC`. The server binds cursors to + * merchant, filters, and ordering. Cross-tenant and absent ids are not + * distinguishable. + */ + get: operations["listAnalyticsSessionDetails"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analytics/sessions/{sessionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one merchant-owned payment session and attempt history */ + get: operations["getAnalyticsSession"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analytics/session-details.csv": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export the authorized detail view as CSV + * @description Uses the same tenant scope, approved period, filters, and allowlisted + * projection as `listAnalyticsSessionDetails`. Cursor and page limit do not + * apply. Output is UTF-8 with a BOM, a Persian header row whose labels use + * normal spaces, comma separators, CRLF records, and RFC 4180 quoting. + * + * Stable columns are: + * `شناسه نشست پرداخت`, `شناسه پایانه`, `وضعیت پرداخت`, `مبلغ ریال`, + * `کارمزد ریال`, `زمان ایجاد`, `زمان انقضا`, `تعداد تلاش`, + * `وضعیت آخرین تلاش`, `زمان آخرین تلاش`, `زمان تأیید`, `زمان تسویه`, + * `مرحله مشاهده شده شکست`. + * + * CSV status labels are Persian: `Paid` is `پرداخت شده و تأیید نشده`, + * `Verified` is `تأیید شده`, `Failed` is `ناموفق`, `Reversed` is + * `برگشت خورده`, and `InBank` is `در بانک`. Numeric cells remain plain + * calculation-safe numbers and timestamps remain UTC ISO values. + * + * Columns unavailable to the chosen authorized view are empty. Payer tokens, + * credentials, peer data, internal ids, and any field unavailable in the JSON + * detail view are forbidden. Potential spreadsheet formulas in textual cells + * are prefixed with an apostrophe before CSV quoting. + */ + get: operations["exportAnalyticsSessionDetailsCsv"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * Export the authorized detail view as CSV - * @description Uses the same tenant scope, approved period, filters, and allowlisted - * projection as `listAnalyticsSessionDetails`. Cursor and page limit do not - * apply. Output is UTF-8 with a BOM, a Persian header row whose labels use - * normal spaces, comma separators, CRLF records, and RFC 4180 quoting. - * - * Stable columns are: - * `شناسه نشست پرداخت`, `شناسه پایانه`, `وضعیت پرداخت`, `مبلغ ریال`, - * `کارمزد ریال`, `زمان ایجاد`, `زمان انقضا`, `تعداد تلاش`, - * `وضعیت آخرین تلاش`, `زمان آخرین تلاش`, `زمان تأیید`, `زمان تسویه`, - * `مرحله مشاهده شده شکست`. - * - * CSV status labels are Persian: `Paid` is `پرداخت شده و تأیید نشده`, - * `Verified` is `تأیید شده`, `Failed` is `ناموفق`, `Reversed` is - * `برگشت خورده`, and `InBank` is `در بانک`. Numeric cells remain plain - * calculation-safe numbers and timestamps remain UTC ISO values. - * - * Columns unavailable to the chosen authorized view are empty. Payer tokens, - * credentials, peer data, internal ids, and any field unavailable in the JSON - * detail view are forbidden. Potential spreadsheet formulas in textual cells - * are prefixed with an apostrophe before CSV quoting. - */ - get: operations['exportAnalyticsSessionDetailsCsv']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - LoginRequest: { - /** @example merchant-0001 */ - username: string; - password: string; + schemas: { + LoginRequest: { + /** @example merchant-0001 */ + username: string; + password: string; + }; + AuthSession: { + user: components["schemas"]["MerchantUserSummary"]; + merchant: components["schemas"]["MerchantSummary"]; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + idleExpiresAt: string; + /** Format: date-time */ + absoluteExpiresAt: string; + /** @description Session-bound CSRF token. It is not an authentication token and must not be logged. */ + csrfToken: string; + }; + MerchantUserSummary: { + userId: string; + /** @example merchant-0001 */ + username: string; + }; + MerchantSummary: { + /** @description Opaque API identifier, never accepted as authorization input. */ + merchantId: string; + displayName: string; + profileIsSynthetic: boolean; + }; + /** @enum {string} */ + PeriodType: "DAY" | "WEEK" | "JALALI_MONTH" | "PERSIAN_SEASON"; + /** @example jm-1405-05 */ + PeriodId: string; + ReportingPeriod: { + periodId: components["schemas"]["PeriodId"]; + periodType: components["schemas"]["PeriodType"]; + /** + * Format: date-time + * @description Inclusive UTC boundary. + * @example 2026-07-23T20:30:00Z + */ + startAt: string; + /** + * Format: date-time + * @description Exclusive UTC boundary. + * @example 2026-08-22T20:30:00Z + */ + endAt: string; + /** + * Format: date-time + * @description Inclusive Asia/Tehran civil boundary with explicit offset. + * @example 2026-07-24T00:00:00+03:30 + */ + localStartAt: string; + /** + * Format: date-time + * @description Exclusive Asia/Tehran civil boundary with explicit offset. + * @example 2026-08-23T00:00:00+03:30 + */ + localEndAt: string; + /** @constant */ + timezone: "Asia/Tehran"; + /** @example مرداد ۱۴۰۵ */ + jalaliLabelFa: string; + isComplete: boolean; + isDefault: boolean; + }; + PeriodList: { + defaultPeriodId: components["schemas"]["PeriodId"]; + periods: components["schemas"]["ReportingPeriod"][]; + }; + CreateAnalyticsRunRequest: { + periodId: components["schemas"]["PeriodId"]; + terminalId?: string; + }; + Terminal: { + terminalId: string; + displayName: string; + /** Format: int64 */ + sessionCount: number; + isDefault: boolean; + }; + TerminalList: { + terminals: components["schemas"]["Terminal"][]; + }; + AnalyticsScope: { + terminalId: string | null; + }; + /** @example run_01K2M7YYYYYYYYYYYYYYYYYYYY */ + RunId: string; + /** @enum {string} */ + SectionId: "overview" | "payment_performance" | "fees" | "peer_comparison"; + /** @enum {string} */ + AnalyticsRunState: "IN_PROGRESS" | "COMPLETE" | "COMPLETE_WITH_FAILURES"; + /** @enum {string} */ + SectionState: "QUEUED" | "CALCULATING" | "READY" | "FAILED" | "SUPPRESSED"; + AnalyticsRun: { + runId: components["schemas"]["RunId"]; + state: components["schemas"]["AnalyticsRunState"]; + period: components["schemas"]["ReportingPeriod"]; + /** @description Opaque identifier for the immutable completed-import snapshot. */ + datasetSnapshotId: string; + /** @constant */ + metricDefinitionVersion: "0.4.0"; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + /** Format: uri-reference */ + eventsUrl: string; + scope: components["schemas"]["AnalyticsScope"]; + sections: { + overview: components["schemas"]["OverviewSection"]; + paymentPerformance: components["schemas"]["PaymentPerformanceSection"]; + fees: components["schemas"]["FeesSection"]; + peerComparison: components["schemas"]["PeerComparisonSection"]; + }; + }; + QueuedSection: { + /** @constant */ + state: "QUEUED"; + /** Format: date-time */ + queuedAt?: string | null; + }; + CalculatingSection: { + /** @constant */ + state: "CALCULATING"; + /** Format: date-time */ + startedAt: string; + }; + FailedSection: { + /** @constant */ + state: "FAILED"; + /** Format: date-time */ + failedAt: string; + /** @enum {string} */ + errorCode: "CALCULATION_FAILED" | "CALCULATION_TIMEOUT" | "SOURCE_UNAVAILABLE" | "UNSUPPORTED_METRIC_VERSION"; + canRetry: boolean; + /** Format: uri-reference */ + retryAttemptsUrl?: string | null; + }; + ReadySectionMetadata: { + /** Format: date-time */ + calculatedAt: string; + datasetSnapshotId: string; + /** @constant */ + metricDefinitionVersion: "0.4.0"; + }; + IntegerMetric: { + metricId: string; + value: number; + /** @enum {string} */ + unit: "SESSIONS" | "RIAL"; + }; + DecimalMetric: { + metricId: string; + /** @description Null only when the denominator is zero or source is unavailable. */ + value: string | null; + /** @enum {string} */ + unit: "PERCENT" | "RATIO"; + numerator: number; + denominator: number; + }; + TimingDistribution: { + /** @enum {string} */ + metricId: "init_time_ms" | "verify_time_ms"; + p50Ms: number | null; + p95Ms: number | null; + sampleSize: number; + eligibleSampleSize: number; + coveragePercent: string | null; + }; + PreviousPeriodComparison: { + /** @enum {string} */ + state: "AVAILABLE" | "UNAVAILABLE"; + baselinePeriod: components["schemas"]["ReportingPeriod"]; + absoluteChange?: string | null; + relativeChangePercent?: string | null; + /** @enum {string|null} */ + unavailableReason?: "EMPTY_BASELINE" | "ZERO_BASELINE" | "SOURCE_UNAVAILABLE" | null; + isActionableInsightEligible?: boolean; + }; + OverviewData: { + /** + * @example { + * "metricId": "bank_paid_sessions", + * "value": 420, + * "unit": "SESSIONS" + * } + */ + bankPaidSessions: components["schemas"]["IntegerMetric"]; + /** + * @example { + * "metricId": "bank_paid_amount_rial", + * "value": 680000000, + * "unit": "RIAL" + * } + */ + bankPaidAmountRial: components["schemas"]["IntegerMetric"]; + paymentSuccessRate: components["schemas"]["DecimalMetric"]; + verificationCompletionRate: components["schemas"]["DecimalMetric"]; + comparisons: { + [key: string]: components["schemas"]["PreviousPeriodComparison"]; + }; + paymentHistory: components["schemas"]["PaymentHistoryPoint"][]; + }; + PaymentHistoryPoint: { + /** Format: date */ + localDate: string; + /** Format: int64 */ + totalSessions: number; + /** Format: int64 */ + bankPaidSessions: number; + /** Format: int64 */ + bankPaidAmountRial: number; + }; + PaymentPerformanceData: { + bankPaidSessions: components["schemas"]["IntegerMetric"]; + verifiedSessions: components["schemas"]["IntegerMetric"]; + unverifiedPaidSessions: components["schemas"]["IntegerMetric"]; + failedSessions: components["schemas"]["IntegerMetric"]; + reversedSessions: components["schemas"]["IntegerMetric"]; + noAttemptSessions: components["schemas"]["IntegerMetric"]; + reachedBankSessions: components["schemas"]["IntegerMetric"]; + retriedSessions: components["schemas"]["IntegerMetric"]; + retryRecoveryRate: components["schemas"]["DecimalMetric"]; + initTiming: components["schemas"]["TimingDistribution"]; + verifyTiming: components["schemas"]["TimingDistribution"]; + }; + FeesData: { + paidFeeAmountRial: components["schemas"]["IntegerMetric"]; + bankPaidAmountRial: components["schemas"]["IntegerMetric"]; + paidFeeRate: components["schemas"]["DecimalMetric"]; + comparison: components["schemas"]["PreviousPeriodComparison"]; + }; + PeerMetric: { + /** @enum {string} */ + metricId: "payment_success_rate" | "verification_completion_rate" | "retry_recovery_rate" | "init_time_ms_p50" | "init_time_ms_p95" | "verify_time_ms_p50" | "verify_time_ms_p95" | "paid_fee_rate"; + cohortMedian: string; + /** @description Higher always means better; lower-is-better metrics are direction-adjusted. */ + merchantPerformancePercentile: string; + /** @enum {string} */ + direction: "HIGHER_IS_BETTER" | "LOWER_IS_BETTER"; + }; + PeerComparisonData: { + eligiblePeerCount: number; + combinedPeerSessionCount: number; + metrics: components["schemas"]["PeerMetric"][]; + }; + OverviewReadySection: { + /** @constant */ + sectionId: "overview"; + /** @constant */ + state: "READY"; + metadata: components["schemas"]["ReadySectionMetadata"]; + data: components["schemas"]["OverviewData"]; + }; + PaymentPerformanceReadySection: { + /** @constant */ + sectionId: "payment_performance"; + /** @constant */ + state: "READY"; + metadata: components["schemas"]["ReadySectionMetadata"]; + data: components["schemas"]["PaymentPerformanceData"]; + }; + FeesReadySection: { + /** @constant */ + sectionId: "fees"; + /** @constant */ + state: "READY"; + metadata: components["schemas"]["ReadySectionMetadata"]; + data: components["schemas"]["FeesData"]; + }; + PeerComparisonReadySection: { + /** @constant */ + sectionId: "peer_comparison"; + /** @constant */ + state: "READY"; + metadata: components["schemas"]["ReadySectionMetadata"]; + data: components["schemas"]["PeerComparisonData"]; + }; + PeerComparisonSuppressedSection: { + /** @constant */ + sectionId: "peer_comparison"; + /** @constant */ + state: "SUPPRESSED"; + metadata: components["schemas"]["ReadySectionMetadata"]; + /** + * @description `TERMINAL_SCOPE_UNSUPPORTED` is returned for terminal-scoped runs because + * the approved peer cohort is merchant-grain and must not be presented as + * if it benchmarked one terminal. + * @enum {string} + */ + reason: "INSUFFICIENT_COHORT" | "TERMINAL_SCOPE_UNSUPPORTED"; + eligiblePeerCount: number; + combinedPeerSessionCount: number; + /** @constant */ + requiredPeerCount: 10; + /** @constant */ + requiredPeerSessionCount: 1000; + }; + OverviewSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["OverviewReadySection"]; + PaymentPerformanceSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["PaymentPerformanceReadySection"]; + FeesSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["FeesReadySection"]; + PeerComparisonSection: components["schemas"]["QueuedSection"] | components["schemas"]["CalculatingSection"] | components["schemas"]["FailedSection"] | components["schemas"]["PeerComparisonReadySection"] | components["schemas"]["PeerComparisonSuppressedSection"]; + /** @enum {string} */ + DetailView: "ALL" | "UNVERIFIED_PAID" | "FAILED" | "RETRIED"; + /** @enum {string} */ + FailureStage: "NO_ATTEMPT" | "REACHED_BANK" | "OTHER_OBSERVED"; + /** @enum {string} */ + SessionStatus: "Paid" | "Verified" | "Failed" | "Reversed"; + /** @enum {string} */ + AttemptStatus: "InBank" | "Paid" | "Verified" | "Failed" | "Reversed"; + AttemptSummary: { + sequence: number; + status: components["schemas"]["AttemptStatus"]; + /** Format: date-time */ + createdAt?: string | null; + initTimeMs?: number | null; + verifyTimeMs?: number | null; + }; + SessionDetail: { + /** @description Opaque merchant-local API id, not the internal database key. */ + sessionId: string; + /** @description Existing opaque source terminal reference shown as `شناسه پایانه`; tenant-scoped display/trace data only. */ + terminalId: string; + sessionStatus: components["schemas"]["SessionStatus"]; + amountRial: number; + feeAmountRial: number; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + expiresAt?: string | null; + /** Format: date-time */ + verifiedAt?: string | null; + /** Format: date-time */ + settledAt?: string | null; + attemptCount: number; + failureStage?: components["schemas"]["FailureStage"] | null; + /** @description Bounded ordered attempt summary; payer tokens and response codes are absent. */ + attempts: components["schemas"]["AttemptSummary"][]; + }; + PageInfo: { + limit: number; + hasNextPage: boolean; + nextCursor: string | null; + }; + SessionDetailPage: { + period: components["schemas"]["ReportingPeriod"]; + view: components["schemas"]["DetailView"]; + appliedFilters: { + terminalId?: string | null; + failureStage?: components["schemas"]["FailureStage"] | null; + status?: components["schemas"]["SessionStatus"] | null; + /** Format: int64 */ + minAmountRial?: number | null; + /** Format: int64 */ + maxAmountRial?: number | null; + }; + items: components["schemas"]["SessionDetail"][]; + pageInfo: components["schemas"]["PageInfo"]; + }; + EventId: string; + SseEventBase: { + eventId: components["schemas"]["EventId"]; + runId: components["schemas"]["RunId"]; + /** Format: date-time */ + occurredAt: string; + }; + RunSnapshotEvent: components["schemas"]["SseEventBase"] & { + /** @enum {string} */ + recovery: "INITIAL" | "REPLAYED" | "RESET_TO_SNAPSHOT"; + run: components["schemas"]["AnalyticsRun"]; + }; + SectionStartedEvent: components["schemas"]["SseEventBase"] & { + sectionId: components["schemas"]["SectionId"]; + /** Format: date-time */ + startedAt: string; + }; + SectionReadyEvent: components["schemas"]["SseEventBase"] & { + sectionId: components["schemas"]["SectionId"]; + section: components["schemas"]["OverviewReadySection"] | components["schemas"]["PaymentPerformanceReadySection"] | components["schemas"]["FeesReadySection"] | components["schemas"]["PeerComparisonReadySection"]; + }; + SectionFailedEvent: components["schemas"]["SseEventBase"] & { + sectionId: components["schemas"]["SectionId"]; + section: components["schemas"]["FailedSection"]; + }; + SectionSuppressedEvent: components["schemas"]["SseEventBase"] & { + /** @constant */ + sectionId: "peer_comparison"; + section: components["schemas"]["PeerComparisonSuppressedSection"]; + }; + RunCompletedEvent: components["schemas"]["SseEventBase"] & { + run: components["schemas"]["AnalyticsRun"]; + }; + HeartbeatEvent: components["schemas"]["SseEventBase"] & { + /** Format: date-time */ + serverTime: string; + }; + /** @description JSON payload carried in the SSE `data` field; event name selects the schema. */ + AnalyticsSseEvent: components["schemas"]["RunSnapshotEvent"] | components["schemas"]["SectionStartedEvent"] | components["schemas"]["SectionReadyEvent"] | components["schemas"]["SectionFailedEvent"] | components["schemas"]["SectionSuppressedEvent"] | components["schemas"]["RunCompletedEvent"] | components["schemas"]["HeartbeatEvent"]; + FieldError: { + /** @description Allowlisted public field name; never a database path. */ + field: string; + reason: string; + }; + /** @description RFC 9457 Problem Details with stable application extensions. */ + Problem: { + /** + * Format: uri + * @description Stable problem type URI under `https://zarinpal.amiirkhl.ir/docs/errors/{error-slug}`. + */ + type: string; + title: string; + /** @description Must equal the actual HTTP response status. */ + status: number; + /** @description Safe occurrence-specific guidance; clients must not parse it. */ + detail: string; + /** Format: uri-reference */ + instance: string; + /** @description Stable application error code used by clients. */ + code: string; + requestId: string; + errors?: components["schemas"]["FieldError"][]; + /** + * Format: uri-reference + * @description Optional safe recovery link for run-related problems. + */ + snapshotUrl?: string; + }; }; - AuthSession: { - user: components['schemas']['MerchantUserSummary']; - merchant: components['schemas']['MerchantSummary']; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - idleExpiresAt: string; - /** Format: date-time */ - absoluteExpiresAt: string; - /** @description Session-bound CSRF token. It is not an authentication token and must not be logged. */ - csrfToken: string; + responses: { + /** @description Request syntax, cursor, event id, or parameter combination is invalid. */ + BadRequest: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description The current session is missing, malformed, expired, or revoked. */ + Unauthenticated: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/unauthenticated", + * "title": "Authentication required", + * "status": 401, + * "detail": "Sign in to continue.", + * "instance": "/api/v1/analytics/runs", + * "code": "UNAUTHENTICATED", + * "requestId": "req_01K2M8A0000000000000000000" + * } + */ + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description The authenticated request is not permitted or failed CSRF/origin checks. */ + Forbidden: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Resource is absent or is outside the authenticated merchant boundary. */ + ResourceNotFound: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/resource-not-found", + * "title": "Resource not found", + * "status": 404, + * "detail": "The requested resource is unavailable.", + * "instance": "/api/v1/analytics/runs/run_unknown", + * "code": "RESOURCE_NOT_FOUND", + * "requestId": "req_01K2M8A0000000000000000000" + * } + */ + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Current resource state does not permit the requested operation. */ + Conflict: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Request exceeds the 16 KiB JSON body limit. */ + RequestTooLarge: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Export exceeds 100,000 rows or 50 MiB estimated uncompressed output. */ + ExportTooLarge: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/export-too-large", + * "title": "Export is too large", + * "status": 413, + * "detail": "Narrow the approved filters and try again.", + * "instance": "/api/v1/analytics/session-details.csv", + * "code": "EXPORT_TOO_LARGE", + * "requestId": "req_01K2M8A0000000000000000000" + * } + */ + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Request media type is not supported. */ + UnsupportedMediaType: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Request is well-formed but violates the interface contract. */ + ValidationFailed: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "type": "https://zarinpal.amiirkhl.ir/docs/errors/validation-error", + * "title": "Request validation failed", + * "status": 422, + * "detail": "One or more request values are invalid.", + * "instance": "/api/v1/analytics/runs", + * "code": "VALIDATION_ERROR", + * "requestId": "req_01K2M8A0000000000000000000", + * "errors": [ + * { + * "field": "periodId", + * "reason": "UNKNOWN_PERIOD" + * } + * ] + * } + */ + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Authenticated-user or merchant rate limit reached before work began. */ + RateLimited: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "Retry-After": components["headers"]["RetryAfter"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description SSE concurrency limit reached for the user or merchant. */ + ConnectionLimited: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "Retry-After": components["headers"]["RetryAfter"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Unexpected server failure; implementation details are not exposed. */ + InternalError: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Required authoritative dependency is unavailable; protected requests fail closed. */ + DependencyUnavailable: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "Retry-After": components["headers"]["RetryAfter"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Analytics dependency is temporarily unavailable. */ + AnalyticsUnavailable: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "Retry-After": components["headers"]["RetryAfter"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + /** @description Export did not complete within the 60-second deadline before streaming began. */ + ExportTimeout: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "Retry-After": components["headers"]["RetryAfter"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; }; - MerchantUserSummary: { - userId: string; - /** @example merchant-0001 */ - username: string; - }; - MerchantSummary: { - /** @description Opaque API identifier, never accepted as authorization input. */ - merchantId: string; - displayName: string; - profileIsSynthetic: boolean; - }; - /** @enum {string} */ - PeriodType: 'DAY' | 'WEEK' | 'JALALI_MONTH' | 'PERSIAN_SEASON'; - /** @example jm-1405-05 */ - PeriodId: string; - ReportingPeriod: { - periodId: components['schemas']['PeriodId']; - periodType: components['schemas']['PeriodType']; - /** - * Format: date-time - * @description Inclusive UTC boundary. - * @example 2026-07-23T20:30:00Z - */ - startAt: string; - /** - * Format: date-time - * @description Exclusive UTC boundary. - * @example 2026-08-22T20:30:00Z - */ - endAt: string; - /** - * Format: date-time - * @description Inclusive Asia/Tehran civil boundary with explicit offset. - * @example 2026-07-24T00:00:00+03:30 - */ - localStartAt: string; - /** - * Format: date-time - * @description Exclusive Asia/Tehran civil boundary with explicit offset. - * @example 2026-08-23T00:00:00+03:30 - */ - localEndAt: string; - /** @constant */ - timezone: 'Asia/Tehran'; - /** @example مرداد ۱۴۰۵ */ - jalaliLabelFa: string; - isComplete: boolean; - isDefault: boolean; - }; - PeriodList: { - defaultPeriodId: components['schemas']['PeriodId']; - periods: components['schemas']['ReportingPeriod'][]; - }; - CreateAnalyticsRunRequest: { - periodId: components['schemas']['PeriodId']; - }; - /** @example run_01K2M7YYYYYYYYYYYYYYYYYYYY */ - RunId: string; - /** @enum {string} */ - SectionId: 'overview' | 'payment_performance' | 'fees' | 'peer_comparison'; - /** @enum {string} */ - AnalyticsRunState: 'IN_PROGRESS' | 'COMPLETE' | 'COMPLETE_WITH_FAILURES'; - /** @enum {string} */ - SectionState: 'QUEUED' | 'CALCULATING' | 'READY' | 'FAILED' | 'SUPPRESSED'; - AnalyticsRun: { - runId: components['schemas']['RunId']; - state: components['schemas']['AnalyticsRunState']; - period: components['schemas']['ReportingPeriod']; - /** @description Opaque identifier for the immutable completed-import snapshot. */ - datasetSnapshotId: string; - /** @constant */ - metricDefinitionVersion: '0.4.0'; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - /** Format: uri-reference */ - eventsUrl: string; - sections: { - overview: components['schemas']['OverviewSection']; - paymentPerformance: components['schemas']['PaymentPerformanceSection']; - fees: components['schemas']['FeesSection']; - peerComparison: components['schemas']['PeerComparisonSection']; - }; - }; - QueuedSection: { - /** @constant */ - state: 'QUEUED'; - /** Format: date-time */ - queuedAt?: string | null; - }; - CalculatingSection: { - /** @constant */ - state: 'CALCULATING'; - /** Format: date-time */ - startedAt: string; - }; - FailedSection: { - /** @constant */ - state: 'FAILED'; - /** Format: date-time */ - failedAt: string; - /** @enum {string} */ - errorCode: - | 'CALCULATION_FAILED' - | 'CALCULATION_TIMEOUT' - | 'SOURCE_UNAVAILABLE' - | 'UNSUPPORTED_METRIC_VERSION'; - canRetry: boolean; - /** Format: uri-reference */ - retryAttemptsUrl?: string | null; - }; - ReadySectionMetadata: { - /** Format: date-time */ - calculatedAt: string; - datasetSnapshotId: string; - /** @constant */ - metricDefinitionVersion: '0.4.0'; - }; - IntegerMetric: { - metricId: string; - value: number; - /** @enum {string} */ - unit: 'SESSIONS' | 'RIAL'; - }; - DecimalMetric: { - metricId: string; - /** @description Null only when the denominator is zero or source is unavailable. */ - value: string | null; - /** @enum {string} */ - unit: 'PERCENT' | 'RATIO'; - numerator: number; - denominator: number; - }; - TimingDistribution: { - /** @enum {string} */ - metricId: 'init_time_ms' | 'verify_time_ms'; - p50Ms: number | null; - p95Ms: number | null; - sampleSize: number; - eligibleSampleSize: number; - coveragePercent: string | null; - }; - PreviousPeriodComparison: { - /** @enum {string} */ - state: 'AVAILABLE' | 'UNAVAILABLE'; - baselinePeriod: components['schemas']['ReportingPeriod']; - absoluteChange?: string | null; - relativeChangePercent?: string | null; - /** @enum {string|null} */ - unavailableReason?: 'EMPTY_BASELINE' | 'ZERO_BASELINE' | 'SOURCE_UNAVAILABLE' | null; - isActionableInsightEligible?: boolean; - }; - OverviewData: { - /** - * @example { - * "metricId": "bank_paid_sessions", - * "value": 420, - * "unit": "SESSIONS" - * } - */ - bankPaidSessions: components['schemas']['IntegerMetric']; - /** - * @example { - * "metricId": "bank_paid_amount_rial", - * "value": 680000000, - * "unit": "RIAL" - * } - */ - bankPaidAmountRial: components['schemas']['IntegerMetric']; - paymentSuccessRate: components['schemas']['DecimalMetric']; - verificationCompletionRate: components['schemas']['DecimalMetric']; - comparisons: { - [key: string]: components['schemas']['PreviousPeriodComparison']; - }; - }; - PaymentPerformanceData: { - bankPaidSessions: components['schemas']['IntegerMetric']; - verifiedSessions: components['schemas']['IntegerMetric']; - unverifiedPaidSessions: components['schemas']['IntegerMetric']; - failedSessions: components['schemas']['IntegerMetric']; - reversedSessions: components['schemas']['IntegerMetric']; - noAttemptSessions: components['schemas']['IntegerMetric']; - reachedBankSessions: components['schemas']['IntegerMetric']; - retriedSessions: components['schemas']['IntegerMetric']; - retryRecoveryRate: components['schemas']['DecimalMetric']; - initTiming: components['schemas']['TimingDistribution']; - verifyTiming: components['schemas']['TimingDistribution']; - }; - FeesData: { - paidFeeAmountRial: components['schemas']['IntegerMetric']; - bankPaidAmountRial: components['schemas']['IntegerMetric']; - paidFeeRate: components['schemas']['DecimalMetric']; - comparison: components['schemas']['PreviousPeriodComparison']; - }; - PeerMetric: { - /** @enum {string} */ - metricId: - | 'payment_success_rate' - | 'verification_completion_rate' - | 'retry_recovery_rate' - | 'init_time_ms_p50' - | 'init_time_ms_p95' - | 'verify_time_ms_p50' - | 'verify_time_ms_p95' - | 'paid_fee_rate'; - cohortMedian: string; - /** @description Higher always means better; lower-is-better metrics are direction-adjusted. */ - merchantPerformancePercentile: string; - /** @enum {string} */ - direction: 'HIGHER_IS_BETTER' | 'LOWER_IS_BETTER'; - }; - PeerComparisonData: { - eligiblePeerCount: number; - combinedPeerSessionCount: number; - metrics: components['schemas']['PeerMetric'][]; - }; - OverviewReadySection: { - /** @constant */ - sectionId: 'overview'; - /** @constant */ - state: 'READY'; - metadata: components['schemas']['ReadySectionMetadata']; - data: components['schemas']['OverviewData']; - }; - PaymentPerformanceReadySection: { - /** @constant */ - sectionId: 'payment_performance'; - /** @constant */ - state: 'READY'; - metadata: components['schemas']['ReadySectionMetadata']; - data: components['schemas']['PaymentPerformanceData']; - }; - FeesReadySection: { - /** @constant */ - sectionId: 'fees'; - /** @constant */ - state: 'READY'; - metadata: components['schemas']['ReadySectionMetadata']; - data: components['schemas']['FeesData']; - }; - PeerComparisonReadySection: { - /** @constant */ - sectionId: 'peer_comparison'; - /** @constant */ - state: 'READY'; - metadata: components['schemas']['ReadySectionMetadata']; - data: components['schemas']['PeerComparisonData']; - }; - PeerComparisonSuppressedSection: { - /** @constant */ - sectionId: 'peer_comparison'; - /** @constant */ - state: 'SUPPRESSED'; - metadata: components['schemas']['ReadySectionMetadata']; - /** @constant */ - reason: 'INSUFFICIENT_COHORT'; - eligiblePeerCount: number; - combinedPeerSessionCount: number; - /** @constant */ - requiredPeerCount: 10; - /** @constant */ - requiredPeerSessionCount: 1000; - }; - OverviewSection: - | components['schemas']['QueuedSection'] - | components['schemas']['CalculatingSection'] - | components['schemas']['FailedSection'] - | components['schemas']['OverviewReadySection']; - PaymentPerformanceSection: - | components['schemas']['QueuedSection'] - | components['schemas']['CalculatingSection'] - | components['schemas']['FailedSection'] - | components['schemas']['PaymentPerformanceReadySection']; - FeesSection: - | components['schemas']['QueuedSection'] - | components['schemas']['CalculatingSection'] - | components['schemas']['FailedSection'] - | components['schemas']['FeesReadySection']; - PeerComparisonSection: - | components['schemas']['QueuedSection'] - | components['schemas']['CalculatingSection'] - | components['schemas']['FailedSection'] - | components['schemas']['PeerComparisonReadySection'] - | components['schemas']['PeerComparisonSuppressedSection']; - /** @enum {string} */ - DetailView: 'UNVERIFIED_PAID' | 'FAILED' | 'RETRIED'; - /** @enum {string} */ - FailureStage: 'NO_ATTEMPT' | 'REACHED_BANK' | 'OTHER_OBSERVED'; - /** @enum {string} */ - SessionStatus: 'Paid' | 'Verified' | 'Failed' | 'Reversed'; - /** @enum {string} */ - AttemptStatus: 'InBank' | 'Paid' | 'Verified' | 'Failed' | 'Reversed'; - AttemptSummary: { - sequence: number; - status: components['schemas']['AttemptStatus']; - /** Format: date-time */ - createdAt?: string | null; - initTimeMs?: number | null; - verifyTimeMs?: number | null; - }; - SessionDetail: { - /** @description Opaque merchant-local API id, not the internal database key. */ - sessionId: string; - /** @description Existing opaque source terminal reference shown as `شناسه پایانه`; tenant-scoped display/trace data only. */ - terminalId: string; - sessionStatus: components['schemas']['SessionStatus']; - amountRial: number; - feeAmountRial: number; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - expiresAt?: string | null; - /** Format: date-time */ - verifiedAt?: string | null; - /** Format: date-time */ - settledAt?: string | null; - attemptCount: number; - failureStage?: components['schemas']['FailureStage'] | null; - /** @description Bounded ordered attempt summary; payer tokens and response codes are absent. */ - attempts: components['schemas']['AttemptSummary'][]; - }; - PageInfo: { - limit: number; - hasNextPage: boolean; - nextCursor: string | null; - }; - SessionDetailPage: { - period: components['schemas']['ReportingPeriod']; - view: components['schemas']['DetailView']; - appliedFilters: { - terminalId?: string | null; - failureStage?: components['schemas']['FailureStage'] | null; - }; - items: components['schemas']['SessionDetail'][]; - pageInfo: components['schemas']['PageInfo']; - }; - EventId: string; - SseEventBase: { - eventId: components['schemas']['EventId']; - runId: components['schemas']['RunId']; - /** Format: date-time */ - occurredAt: string; - }; - RunSnapshotEvent: components['schemas']['SseEventBase'] & { - /** @enum {string} */ - recovery: 'INITIAL' | 'REPLAYED' | 'RESET_TO_SNAPSHOT'; - run: components['schemas']['AnalyticsRun']; - }; - SectionStartedEvent: components['schemas']['SseEventBase'] & { - sectionId: components['schemas']['SectionId']; - /** Format: date-time */ - startedAt: string; - }; - SectionReadyEvent: components['schemas']['SseEventBase'] & { - sectionId: components['schemas']['SectionId']; - section: - | components['schemas']['OverviewReadySection'] - | components['schemas']['PaymentPerformanceReadySection'] - | components['schemas']['FeesReadySection'] - | components['schemas']['PeerComparisonReadySection']; - }; - SectionFailedEvent: components['schemas']['SseEventBase'] & { - sectionId: components['schemas']['SectionId']; - section: components['schemas']['FailedSection']; - }; - SectionSuppressedEvent: components['schemas']['SseEventBase'] & { - /** @constant */ - sectionId: 'peer_comparison'; - section: components['schemas']['PeerComparisonSuppressedSection']; - }; - RunCompletedEvent: components['schemas']['SseEventBase'] & { - run: components['schemas']['AnalyticsRun']; - }; - HeartbeatEvent: components['schemas']['SseEventBase'] & { - /** Format: date-time */ - serverTime: string; - }; - /** @description JSON payload carried in the SSE `data` field; event name selects the schema. */ - AnalyticsSseEvent: - | components['schemas']['RunSnapshotEvent'] - | components['schemas']['SectionStartedEvent'] - | components['schemas']['SectionReadyEvent'] - | components['schemas']['SectionFailedEvent'] - | components['schemas']['SectionSuppressedEvent'] - | components['schemas']['RunCompletedEvent'] - | components['schemas']['HeartbeatEvent']; - FieldError: { - /** @description Allowlisted public field name; never a database path. */ - field: string; - reason: string; - }; - /** @description RFC 9457 Problem Details with stable application extensions. */ - Problem: { - /** - * Format: uri - * @description Stable problem type URI under `https://zarinpal.amiirkhl.ir/docs/errors/{error-slug}`. - */ - type: string; - title: string; - /** @description Must equal the actual HTTP response status. */ - status: number; - /** @description Safe occurrence-specific guidance; clients must not parse it. */ - detail: string; - /** Format: uri-reference */ - instance: string; - /** @description Stable application error code used by clients. */ - code: string; - requestId: string; - errors?: components['schemas']['FieldError'][]; - /** - * Format: uri-reference - * @description Optional safe recovery link for run-related problems. - */ - snapshotUrl?: string; - }; - }; - responses: { - /** @description Request syntax, cursor, event id, or parameter combination is invalid. */ - BadRequest: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description The current session is missing, malformed, expired, or revoked. */ - Unauthenticated: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { + parameters: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + RunIdPath: components["schemas"]["RunId"]; + SectionIdPath: components["schemas"]["SectionId"]; /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/unauthenticated", - * "title": "Authentication required", - * "status": 401, - * "detail": "Sign in to continue.", - * "instance": "/api/v1/analytics/runs", - * "code": "UNAUTHENTICATED", - * "requestId": "req_01K2M8A0000000000000000000" - * } + * @description Client-generated intent key reused unchanged for retries of one intent. + * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b */ - 'application/problem+json': components['schemas']['Problem']; - }; + IdempotencyKeyHeader: string; + /** @description CSRF token for the current authenticated session; never log this value. */ + CsrfTokenHeader: string; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + OriginHeader: string; + /** @description Optional safe correlation id. The server may replace invalid values. */ + RequestIdHeader: string; + /** @description Stable id returned by `listAnalyticsPeriods`. */ + PeriodIdQuery: components["schemas"]["PeriodId"]; + DetailViewQuery: components["schemas"]["DetailView"]; + /** @description Valid only when `view=FAILED`. */ + FailureStageQuery: components["schemas"]["FailureStage"]; + /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ + TerminalIdQuery: string; + SessionStatusQuery: components["schemas"]["SessionStatus"]; + MinAmountRialQuery: number; + MaxAmountRialQuery: number; + /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ + CursorQuery: string; + /** @description Page size; defaults to 25 and cannot exceed 100. */ + LimitQuery: number; }; - /** @description The authenticated request is not permitted or failed CSRF/origin checks. */ - Forbidden: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; + requestBodies: never; + headers: { + /** @description Server correlation id for this request. */ + RequestId: string; + /** @description Prevents storage of authenticated merchant data. */ + PrivateNoStore: "private, no-store"; + /** @description Seconds until retry is permitted. */ + RetryAfter: number; }; - /** @description Resource is absent or is outside the authenticated merchant boundary. */ - ResourceNotFound: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/resource-not-found", - * "title": "Resource not found", - * "status": 404, - * "detail": "The requested resource is unavailable.", - * "instance": "/api/v1/analytics/runs/run_unknown", - * "code": "RESOURCE_NOT_FOUND", - * "requestId": "req_01K2M8A0000000000000000000" - * } - */ - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Current resource state does not permit the requested operation. */ - Conflict: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Request exceeds the 16 KiB JSON body limit. */ - RequestTooLarge: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Export exceeds 100,000 rows or 50 MiB estimated uncompressed output. */ - ExportTooLarge: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/export-too-large", - * "title": "Export is too large", - * "status": 413, - * "detail": "Narrow the approved filters and try again.", - * "instance": "/api/v1/analytics/session-details.csv", - * "code": "EXPORT_TOO_LARGE", - * "requestId": "req_01K2M8A0000000000000000000" - * } - */ - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Request media type is not supported. */ - UnsupportedMediaType: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Request is well-formed but violates the interface contract. */ - ValidationFailed: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - /** - * @example { - * "type": "https://zarinpal.amiirkhl.ir/docs/errors/validation-error", - * "title": "Request validation failed", - * "status": 422, - * "detail": "One or more request values are invalid.", - * "instance": "/api/v1/analytics/runs", - * "code": "VALIDATION_ERROR", - * "requestId": "req_01K2M8A0000000000000000000", - * "errors": [ - * { - * "field": "periodId", - * "reason": "UNKNOWN_PERIOD" - * } - * ] - * } - */ - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Authenticated-user or merchant rate limit reached before work began. */ - RateLimited: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'Retry-After': components['headers']['RetryAfter']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description SSE concurrency limit reached for the user or merchant. */ - ConnectionLimited: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'Retry-After': components['headers']['RetryAfter']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Unexpected server failure; implementation details are not exposed. */ - InternalError: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Required authoritative dependency is unavailable; protected requests fail closed. */ - DependencyUnavailable: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'Retry-After': components['headers']['RetryAfter']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Analytics dependency is temporarily unavailable. */ - AnalyticsUnavailable: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'Retry-After': components['headers']['RetryAfter']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - /** @description Export did not complete within the 60-second deadline before streaming began. */ - ExportTimeout: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'Retry-After': components['headers']['RetryAfter']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/problem+json': components['schemas']['Problem']; - }; - }; - }; - parameters: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - RunIdPath: components['schemas']['RunId']; - SectionIdPath: components['schemas']['SectionId']; - /** - * @description Client-generated intent key reused unchanged for retries of one intent. - * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b - */ - IdempotencyKeyHeader: string; - /** @description CSRF token for the current authenticated session; never log this value. */ - CsrfTokenHeader: string; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - OriginHeader: string; - /** @description Optional safe correlation id. The server may replace invalid values. */ - RequestIdHeader: string; - /** @description Stable id returned by `listAnalyticsPeriods`. */ - PeriodIdQuery: components['schemas']['PeriodId']; - DetailViewQuery: components['schemas']['DetailView']; - /** @description Valid only when `view=FAILED`. */ - FailureStageQuery: components['schemas']['FailureStage']; - /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ - TerminalIdQuery: string; - /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ - CursorQuery: string; - /** @description Page size; defaults to 25 and cannot exceed 100. */ - LimitQuery: number; - }; - requestBodies: never; - headers: { - /** @description Server correlation id for this request. */ - RequestId: string; - /** @description Prevents storage of authenticated merchant data. */ - PrivateNoStore: 'private, no-store'; - /** @description Seconds until retry is permitted. */ - RetryAfter: number; - }; - pathItems: never; + pathItems: never; } export type $defs = Record; export interface operations { - createAuthSession: { - parameters: { - query?: never; - header: { - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components['parameters']['OriginHeader']; - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['LoginRequest']; - }; - }; - responses: { - /** @description Authenticated session created. */ - 201: { - headers: { - /** - * @description Secure opaque session cookie; value is intentionally omitted from examples. - * @example __Host-zpa_session=; Path=/; Secure; HttpOnly; SameSite=Lax - */ - 'Set-Cookie'?: string; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + createAuthSession: { + parameters: { + query?: never; + header: { + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components["parameters"]["OriginHeader"]; + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; }; - content: { - 'application/json': components['schemas']['AuthSession']; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 413: components['responses']['RequestTooLarge']; - 415: components['responses']['UnsupportedMediaType']; - 422: components['responses']['ValidationFailed']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['DependencyUnavailable']; - }; - }; - getAuthSession: { - parameters: { - query?: never; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current authenticated merchant-user session. */ - 200: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + responses: { + /** @description Authenticated session created. */ + 201: { + headers: { + /** + * @description Secure opaque session cookie; value is intentionally omitted from examples. + * @example __Host-zpa_session=; Path=/; Secure; HttpOnly; SameSite=Lax + */ + "Set-Cookie"?: string; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthSession"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 413: components["responses"]["RequestTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 422: components["responses"]["ValidationFailed"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["DependencyUnavailable"]; }; - content: { - 'application/json': components['schemas']['AuthSession']; - }; - }; - 401: components['responses']['Unauthenticated']; - 500: components['responses']['InternalError']; - 503: components['responses']['DependencyUnavailable']; }; - }; - deleteAuthSession: { - parameters: { - query?: never; - header: { - /** @description CSRF token for the current authenticated session; never log this value. */ - 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components['parameters']['OriginHeader']; - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current authenticated session revoked. */ - 204: { - headers: { - /** - * @description Expired session cookie. - * @example __Host-zpa_session=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax - */ - 'Set-Cookie'?: string; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + getAuthSession: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; }; - content?: never; - }; - 401: components['responses']['Unauthenticated']; - 403: components['responses']['Forbidden']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['DependencyUnavailable']; - }; - }; - listAnalyticsPeriods: { - parameters: { - query?: { - /** @description Optional allowlisted period type. */ - periodType?: components['schemas']['PeriodType']; - }; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Approved period choices and default selection. */ - 200: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + requestBody?: never; + responses: { + /** @description Current authenticated merchant-user session. */ + 200: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthSession"]; + }; + }; + 401: components["responses"]["Unauthenticated"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["DependencyUnavailable"]; }; - content: { - 'application/json': components['schemas']['PeriodList']; - }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 422: components['responses']['ValidationFailed']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['DependencyUnavailable']; }; - }; - createAnalyticsRun: { - parameters: { - query?: never; - header: { - /** - * @description Client-generated intent key reused unchanged for retries of one intent. - * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b - */ - 'Idempotency-Key': components['parameters']['IdempotencyKeyHeader']; - /** @description CSRF token for the current authenticated session; never log this value. */ - 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components['parameters']['OriginHeader']; - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; + deleteAuthSession: { + parameters: { + query?: never; + header: { + /** @description CSRF token for the current authenticated session; never log this value. */ + "X-CSRF-Token": components["parameters"]["CsrfTokenHeader"]; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components["parameters"]["OriginHeader"]; + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current authenticated session revoked. */ + 204: { + headers: { + /** + * @description Expired session cookie. + * @example __Host-zpa_session=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax + */ + "Set-Cookie"?: string; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content?: never; + }; + 401: components["responses"]["Unauthenticated"]; + 403: components["responses"]["Forbidden"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["DependencyUnavailable"]; + }; }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateAnalyticsRunRequest']; - }; + listAnalyticsPeriods: { + parameters: { + query?: { + /** @description Optional allowlisted period type. */ + periodType?: components["schemas"]["PeriodType"]; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Approved period choices and default selection. */ + 200: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PeriodList"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 422: components["responses"]["ValidationFailed"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["DependencyUnavailable"]; + }; }; - responses: { - /** @description Existing current run returned with no new work required. */ - 200: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - /** @description Whether this response replayed a prior intent result. */ - 'Idempotency-Replayed'?: boolean; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + listMerchantTerminals: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; }; - content: { - 'application/json': components['schemas']['AnalyticsRun']; + requestBody?: never; + responses: { + /** @description Merchant-owned terminal choices ordered by session volume. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TerminalList"]; + }; + }; + 401: components["responses"]["Unauthenticated"]; + 503: components["responses"]["AnalyticsUnavailable"]; }; - }; - /** @description Current run accepted and one or more missing sections are queued/calculating. */ - 202: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - /** @description Whether this response replayed a prior intent result. */ - 'Idempotency-Replayed'?: boolean; - /** @description Relative URL of the run snapshot. */ - Location?: string; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AnalyticsRun']; - }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 403: components['responses']['Forbidden']; - 413: components['responses']['RequestTooLarge']; - 415: components['responses']['UnsupportedMediaType']; - 422: components['responses']['ValidationFailed']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['AnalyticsUnavailable']; }; - }; - getAnalyticsRun: { - parameters: { - query?: never; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components['parameters']['RunIdPath']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current state of every run section. */ - 200: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + createAnalyticsRun: { + parameters: { + query?: never; + header: { + /** + * @description Client-generated intent key reused unchanged for retries of one intent. + * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b + */ + "Idempotency-Key": components["parameters"]["IdempotencyKeyHeader"]; + /** @description CSRF token for the current authenticated session; never log this value. */ + "X-CSRF-Token": components["parameters"]["CsrfTokenHeader"]; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components["parameters"]["OriginHeader"]; + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; }; - content: { - 'application/json': components['schemas']['AnalyticsRun']; + requestBody: { + content: { + "application/json": components["schemas"]["CreateAnalyticsRunRequest"]; + }; }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 404: components['responses']['ResourceNotFound']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['AnalyticsUnavailable']; - }; - }; - streamAnalyticsRunEvents: { - parameters: { - query?: never; - header?: { - /** @description Opaque last processed event id; must not contain NUL, LF, or CR. */ - 'Last-Event-ID'?: string; - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components['parameters']['RunIdPath']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description UTF-8 event stream. A named heartbeat is emitted every 15 seconds. */ - 200: { - headers: { - /** @description Prevents transformation or storage of the live event stream. */ - 'Cache-Control'?: 'no-cache, no-transform'; - /** @description Deployment hint disabling Nginx response buffering. */ - 'X-Accel-Buffering'?: 'no'; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + responses: { + /** @description Existing current run returned with no new work required. */ + 200: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + /** @description Whether this response replayed a prior intent result. */ + "Idempotency-Replayed"?: boolean; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalyticsRun"]; + }; + }; + /** @description Current run accepted and one or more missing sections are queued/calculating. */ + 202: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + /** @description Whether this response replayed a prior intent result. */ + "Idempotency-Replayed"?: boolean; + /** @description Relative URL of the run snapshot. */ + Location?: string; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalyticsRun"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 403: components["responses"]["Forbidden"]; + 413: components["responses"]["RequestTooLarge"]; + 415: components["responses"]["UnsupportedMediaType"]; + 422: components["responses"]["ValidationFailed"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["AnalyticsUnavailable"]; }; - content: { - 'text/event-stream': string; - }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 404: components['responses']['ResourceNotFound']; - 429: components['responses']['ConnectionLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['AnalyticsUnavailable']; }; - }; - createAnalyticsSectionRetryAttempt: { - parameters: { - query?: never; - header: { - /** - * @description Client-generated intent key reused unchanged for retries of one intent. - * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b - */ - 'Idempotency-Key': components['parameters']['IdempotencyKeyHeader']; - /** @description CSRF token for the current authenticated session; never log this value. */ - 'X-CSRF-Token': components['parameters']['CsrfTokenHeader']; - /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ - Origin: components['parameters']['OriginHeader']; - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path: { - /** @description Opaque analytics run identifier; possession does not authorize access. */ - runId: components['parameters']['RunIdPath']; - sectionId: components['parameters']['SectionIdPath']; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Retry accepted or equivalent current retry returned. */ - 202: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - /** @description Relative run snapshot URL. */ - Location?: string; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + getAnalyticsRun: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components["parameters"]["RunIdPath"]; + }; + cookie?: never; }; - content: { - 'application/json': components['schemas']['AnalyticsRun']; + requestBody?: never; + responses: { + /** @description Current state of every run section. */ + 200: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalyticsRun"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 404: components["responses"]["ResourceNotFound"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["AnalyticsUnavailable"]; }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 403: components['responses']['Forbidden']; - 404: components['responses']['ResourceNotFound']; - 409: components['responses']['Conflict']; - 422: components['responses']['ValidationFailed']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['AnalyticsUnavailable']; }; - }; - listAnalyticsSessionDetails: { - parameters: { - query: { - /** @description Stable id returned by `listAnalyticsPeriods`. */ - periodId: components['parameters']['PeriodIdQuery']; - view: components['parameters']['DetailViewQuery']; - /** @description Valid only when `view=FAILED`. */ - failureStage?: components['parameters']['FailureStageQuery']; - /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ - terminalId?: components['parameters']['TerminalIdQuery']; - /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ - cursor?: components['parameters']['CursorQuery']; - /** @description Page size; defaults to 25 and cannot exceed 100. */ - limit?: components['parameters']['LimitQuery']; - }; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Authorized detail page. */ - 200: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + streamAnalyticsRunEvents: { + parameters: { + query?: never; + header?: { + /** @description Opaque last processed event id; must not contain NUL, LF, or CR. */ + "Last-Event-ID"?: string; + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components["parameters"]["RunIdPath"]; + }; + cookie?: never; }; - content: { - 'application/json': components['schemas']['SessionDetailPage']; + requestBody?: never; + responses: { + /** @description UTF-8 event stream. A named heartbeat is emitted every 15 seconds. */ + 200: { + headers: { + /** @description Prevents transformation or storage of the live event stream. */ + "Cache-Control"?: "no-cache, no-transform"; + /** @description Deployment hint disabling Nginx response buffering. */ + "X-Accel-Buffering"?: "no"; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "text/event-stream": string; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 404: components["responses"]["ResourceNotFound"]; + 429: components["responses"]["ConnectionLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["AnalyticsUnavailable"]; }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 404: components['responses']['ResourceNotFound']; - 422: components['responses']['ValidationFailed']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['AnalyticsUnavailable']; }; - }; - exportAnalyticsSessionDetailsCsv: { - parameters: { - query: { - /** @description Stable id returned by `listAnalyticsPeriods`. */ - periodId: components['parameters']['PeriodIdQuery']; - view: components['parameters']['DetailViewQuery']; - /** @description Valid only when `view=FAILED`. */ - failureStage?: components['parameters']['FailureStageQuery']; - /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ - terminalId?: components['parameters']['TerminalIdQuery']; - }; - header?: { - /** @description Optional safe correlation id. The server may replace invalid values. */ - 'X-Request-ID'?: components['parameters']['RequestIdHeader']; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Complete authorized CSV export. */ - 200: { - headers: { - 'Cache-Control': components['headers']['PrivateNoStore']; - /** - * @description Attachment with ASCII fallback first and UTF-8 `filename*` second. - * Filename contains only server-generated view, period id, and time. - * @example attachment; filename="unverified_paid-jm-1405-05-20260820T093000Z.csv"; filename*=UTF-8''unverified_paid-jm-1405-05-20260820T093000Z.csv - */ - 'Content-Disposition'?: string; - /** @description Prevents content sniffing. */ - 'X-Content-Type-Options'?: 'nosniff'; - 'X-Request-ID': components['headers']['RequestId']; - [name: string]: unknown; + createAnalyticsSectionRetryAttempt: { + parameters: { + query?: never; + header: { + /** + * @description Client-generated intent key reused unchanged for retries of one intent. + * @example 95f6ec2d-26e8-4983-afd4-8a6d4434726b + */ + "Idempotency-Key": components["parameters"]["IdempotencyKeyHeader"]; + /** @description CSRF token for the current authenticated session; never log this value. */ + "X-CSRF-Token": components["parameters"]["CsrfTokenHeader"]; + /** @description Strict same-origin evidence validated with Fetch Metadata where available. */ + Origin: components["parameters"]["OriginHeader"]; + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path: { + /** @description Opaque analytics run identifier; possession does not authorize access. */ + runId: components["parameters"]["RunIdPath"]; + sectionId: components["parameters"]["SectionIdPath"]; + }; + cookie?: never; }; - content: { - /** - * @example "شناسه نشست پرداخت","شناسه پایانه","وضعیت پرداخت","مبلغ ریال","کارمزد ریال","زمان ایجاد","زمان انقضا","تعداد تلاش","وضعیت آخرین تلاش","زمان آخرین تلاش","زمان تأیید","زمان تسویه","مرحله مشاهده شده شکست" - * ses_01K2M8,term_0042,پرداخت شده و تأیید نشده,1500000,2000,2026-06-01T08:00:00Z,2026-06-01T08:15:00Z,1,پرداخت شده و تأیید نشده,2026-06-01T08:02:00Z,,, - */ - 'text/csv; charset=utf-8': string; + requestBody?: never; + responses: { + /** @description Retry accepted or equivalent current retry returned. */ + 202: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + /** @description Relative run snapshot URL. */ + Location?: string; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AnalyticsRun"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["ResourceNotFound"]; + 409: components["responses"]["Conflict"]; + 422: components["responses"]["ValidationFailed"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["AnalyticsUnavailable"]; + }; + }; + listAnalyticsSessionDetails: { + parameters: { + query: { + /** @description Stable id returned by `listAnalyticsPeriods`. */ + periodId: components["parameters"]["PeriodIdQuery"]; + view: components["parameters"]["DetailViewQuery"]; + /** @description Valid only when `view=FAILED`. */ + failureStage?: components["parameters"]["FailureStageQuery"]; + /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ + terminalId?: components["parameters"]["TerminalIdQuery"]; + status?: components["parameters"]["SessionStatusQuery"]; + minAmountRial?: components["parameters"]["MinAmountRialQuery"]; + maxAmountRial?: components["parameters"]["MaxAmountRialQuery"]; + /** @description Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. */ + cursor?: components["parameters"]["CursorQuery"]; + /** @description Page size; defaults to 25 and cannot exceed 100. */ + limit?: components["parameters"]["LimitQuery"]; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorized detail page. */ + 200: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionDetailPage"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 404: components["responses"]["ResourceNotFound"]; + 422: components["responses"]["ValidationFailed"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["AnalyticsUnavailable"]; + }; + }; + getAnalyticsSession: { + parameters: { + query?: never; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path: { + sessionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorized merchant-owned session. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionDetail"]; + }; + }; + 401: components["responses"]["Unauthenticated"]; + 404: components["responses"]["ResourceNotFound"]; + 503: components["responses"]["AnalyticsUnavailable"]; + }; + }; + exportAnalyticsSessionDetailsCsv: { + parameters: { + query: { + /** @description Stable id returned by `listAnalyticsPeriods`. */ + periodId: components["parameters"]["PeriodIdQuery"]; + view: components["parameters"]["DetailViewQuery"]; + /** @description Valid only when `view=FAILED`. */ + failureStage?: components["parameters"]["FailureStageQuery"]; + /** @description Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. */ + terminalId?: components["parameters"]["TerminalIdQuery"]; + status?: components["parameters"]["SessionStatusQuery"]; + minAmountRial?: components["parameters"]["MinAmountRialQuery"]; + maxAmountRial?: components["parameters"]["MaxAmountRialQuery"]; + }; + header?: { + /** @description Optional safe correlation id. The server may replace invalid values. */ + "X-Request-ID"?: components["parameters"]["RequestIdHeader"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Complete authorized CSV export. */ + 200: { + headers: { + "Cache-Control": components["headers"]["PrivateNoStore"]; + /** + * @description Attachment with ASCII fallback first and UTF-8 `filename*` second. + * Filename contains only server-generated view, period id, and time. + * @example attachment; filename="unverified_paid-jm-1405-05-20260820T093000Z.csv"; filename*=UTF-8''unverified_paid-jm-1405-05-20260820T093000Z.csv + */ + "Content-Disposition"?: string; + /** @description Prevents content sniffing. */ + "X-Content-Type-Options"?: "nosniff"; + "X-Request-ID": components["headers"]["RequestId"]; + [name: string]: unknown; + }; + content: { + /** + * @example "شناسه نشست پرداخت","شناسه پایانه","وضعیت پرداخت","مبلغ ریال","کارمزد ریال","زمان ایجاد","زمان انقضا","تعداد تلاش","وضعیت آخرین تلاش","زمان آخرین تلاش","زمان تأیید","زمان تسویه","مرحله مشاهده شده شکست" + * ses_01K2M8,term_0042,پرداخت شده و تأیید نشده,1500000,2000,2026-06-01T08:00:00Z,2026-06-01T08:15:00Z,1,پرداخت شده و تأیید نشده,2026-06-01T08:02:00Z,,, + */ + "text/csv; charset=utf-8": string; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthenticated"]; + 404: components["responses"]["ResourceNotFound"]; + 413: components["responses"]["ExportTooLarge"]; + 422: components["responses"]["ValidationFailed"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 503: components["responses"]["AnalyticsUnavailable"]; + 504: components["responses"]["ExportTimeout"]; }; - }; - 400: components['responses']['BadRequest']; - 401: components['responses']['Unauthenticated']; - 404: components['responses']['ResourceNotFound']; - 413: components['responses']['ExportTooLarge']; - 422: components['responses']['ValidationFailed']; - 429: components['responses']['RateLimited']; - 500: components['responses']['InternalError']; - 503: components['responses']['AnalyticsUnavailable']; - 504: components['responses']['ExportTimeout']; }; - }; } diff --git a/src/lib/components/AnalyticsPage.svelte b/src/lib/components/AnalyticsPage.svelte new file mode 100644 index 0000000..6f2d798 --- /dev/null +++ b/src/lib/components/AnalyticsPage.svelte @@ -0,0 +1,320 @@ + + +{titles[kind][0]} | تحلیل زرین‌پال + + + +{#if loading} +
+ {#each Array(4) as _}{/each} +
+ +{:else if error} +

{error}

+{:else if run} + {@const overview = run.sections.overview} + {@const performance = run.sections.paymentPerformance} + {@const fees = run.sections.fees} + {@const peers = run.sections.peerComparison} + + {#if kind === 'overview' && overview.state === 'READY'} +
+ + + + +
+ + + +
+
+ روند پرداخت‌های موفق +

+ برای دیدن مبلغ و تعداد هر روز، روی نقطه‌های نمودار بروید. +

+
+ {run.period.jalaliLabelFa} +
+
+ +
+ + + + +
+ خلاصه این بازه +

+ {formatValue(overview.data.bankPaidSessions.value)} پرداخت موفق به ارزش {formatMoney( + overview.data.bankPaidAmountRial.value, + )} ثبت کرده‌اید. + {#if overview.data.paymentSuccessRate.value !== null} + در حال حاضر از هر ۱۰۰ شروع پرداخت، حدود {formatValue( + Math.round(Number(overview.data.paymentSuccessRate.value)), + )} مورد با موفقیت بانکی همراه می‌شود. + {/if} +

+
+
+
+ {:else if kind === 'payments' && performance.state === 'READY'} +
+ + + + +
+ +
+ + جریان مسیر پرداخت + + {#each [['همه شروع‌های پرداخت', performance.data.bankPaidSessions.value + performance.data.failedSessions.value + performance.data.noAttemptSessions.value, 'bg-primary'], ['رسیده به بانک', performance.data.reachedBankSessions.value, 'bg-blue-500'], ['موفق در بانک', performance.data.bankPaidSessions.value, 'bg-emerald-500'], ['تأیید نهایی', performance.data.verifiedSessions.value, 'bg-emerald-600']] as step, index} +
+
+ {step[0]}{formatValue(Number(step[1]))} +
+
+
+
+
+ {/each} +
+
+ + + +

یک فرصت روشن

+

+ {formatValue(performance.data.retriedSessions.value)} مشتری دوباره تلاش کرده‌اند و نرخ بازیابی آن‌ها + {formatPercent(performance.data.retryRecoveryRate.value)} بوده است. جزئیات تراکنش‌های ناموفق می‌تواند + نقاط بهبود مسیر را نشان دهد. +

+
+
+
+ {:else if kind === 'fees' && fees.state === 'READY'} +
+ + + +
+ +
+

کارمزد در یک نگاه

+

+ از مجموع {formatMoney(fees.data.bankPaidAmountRial.value)} پرداخت موفق، {formatMoney( + fees.data.paidFeeAmountRial.value, + )} به‌عنوان کارمزد ثبت شده است. این نمایش برای شفافیت روند است و درباره علت تغییرها ادعایی نمی‌کند. +

+
+ {:else if kind === 'peers' && peers.state === 'READY'} + +

+ {formatValue(peers.data.eligiblePeerCount)} کسب‌وکار مشابه با مجموع {formatValue( + peers.data.combinedPeerSessionCount, + )} نشست، مبنای مقایسه ناشناس این گزارش هستند. داده هیچ کسب‌وکاری جداگانه نمایش داده نمی‌شود. +

+
+ {#each peers.data.metrics as metric} + + +
+

{peerMetricLabel(metric.metricId)}

+ = 75 ? 'default' : 'secondary'} + class="rounded-md" + >صدک {formatValue(Math.round(percentile(metric.merchantPerformancePercentile)))} +
+
+
+
+
+ فضای رشدپیشرو +
+

+ {describePercentile(metric.metricId, percentile(metric.merchantPerformancePercentile))} +

+

+ میانه هم‌گروه: {formatPeerMetric(metric.metricId, metric.cohortMedian)} +

+
+
+ {/each} +
+ {#if filters.terminal !== 'all'}

+ مقایسه هم‌گروه در سطح کل پذیرنده محاسبه می‌شود؛ انتخاب درگاه فقط بخش‌های عملیاتی را محدود می‌کند. +

{/if} + {:else if kind === 'peers' && peers.state === 'SUPPRESSED'} + +

گروه مقایسه هنوز به‌اندازه کافی بزرگ نیست

+

+ برای حفظ محرمانگی کسب‌وکارها، این بخش تا رسیدن گروه به حد امن نمایش داده نمی‌شود. +

+ {:else} + این بخش هنوز در حال آماده‌سازی است. + {/if} +{/if} diff --git a/src/lib/components/AppShell.svelte b/src/lib/components/AppShell.svelte new file mode 100644 index 0000000..8962c3b --- /dev/null +++ b/src/lib/components/AppShell.svelte @@ -0,0 +1,207 @@ + + + + + +
+ +
+ {dashboard.session?.merchant.displayName} + حساب تحلیل پذیرنده +
+
+
+ + +
+
+ + + + + {#each items as item} + + goto(navUrl(item.href))} + > + + {item.label} + + + {/each} + + + + + + + + + goto(navUrl('/methodology'))} + > + + روش محاسبه + + + + + + + + + + + + خروج از حساب + + + + + +
+ +
+
+
+ + +
+
+ + + + +
+ {dashboard.session?.user.username.slice(0, 1).toUpperCase()} +
+
+
+
+ + +
+ {@render children()} +
+
+
+ + diff --git a/src/lib/components/BrandLogo.svelte b/src/lib/components/BrandLogo.svelte new file mode 100644 index 0000000..14f7354 --- /dev/null +++ b/src/lib/components/BrandLogo.svelte @@ -0,0 +1,37 @@ + + +{#if compact} + +{:else} + +{/if} + + diff --git a/src/lib/components/DashboardControls.svelte b/src/lib/components/DashboardControls.svelte new file mode 100644 index 0000000..3e1c8e4 --- /dev/null +++ b/src/lib/components/DashboardControls.svelte @@ -0,0 +1,38 @@ + + +
+
+
+
diff --git a/src/lib/components/DetailsPanel.svelte b/src/lib/components/DetailsPanel.svelte index ff4b025..49b7895 100644 --- a/src/lib/components/DetailsPanel.svelte +++ b/src/lib/components/DetailsPanel.svelte @@ -23,7 +23,7 @@ loading = true; error = ''; try { - page = await api.details(period.periodId, view, cursor, failureStage); + page = await api.details(period.periodId, view, { cursor, failureStage }); } catch { error = 'جزئیات در حال حاضر در دسترس نیست.'; } finally { @@ -107,7 +107,7 @@
دریافت CSVدریافت CSV {#if page.pageInfo.hasNextPage && page.pageInfo.nextCursor} +
+

جزئیات تراکنش

+

مسیر پرداخت و تلاش‌های ثبت‌شده برای این نشست

+
+
+ +{#if loading} +
در حال دریافت جزئیات…
+{:else if error || !detail} + {error} +{:else} + + +
+ +
+

{formatMoney(detail.amountRial)}

+

{detail.sessionId}

+
+
+ {statusLabels[detail.sessionStatus]} +
+
+ +
+ + اطلاعات پرداخت + + {#each [['شناسه پایانه', detail.terminalId], ['زمان ایجاد', formatDate(detail.createdAt)], ['مبلغ تراکنش', formatMoney(detail.amountRial)], ['کارمزد', formatMoney(detail.feeAmountRial)], ['زمان تأیید', detail.verifiedAt ? formatDate(detail.verifiedAt) : 'ثبت نشده'], ['زمان تسویه', detail.settledAt ? formatDate(detail.settledAt) : 'ثبت نشده']] as field} +
+ {field[0]}{field[1]} +
+ {/each} +
+
+ + + تاریخچه تلاش‌ها + +
    + {#each detail.attempts as attempt} +
  1. + +
    + تلاش {attempt.sequence}{statusLabels[attempt.status]} +
    + {#if attempt.createdAt}

    + {formatDate(attempt.createdAt)} +

    {/if} +
    + شروع: {formatMilliseconds(attempt.initTimeMs ?? null)}تأیید: {formatMilliseconds(attempt.verifyTimeMs ?? null)} +
    +
  2. + {/each} + {#if detail.attempts.length === 0}
  3. + تلاشی برای این نشست ثبت نشده است. +
  4. {/if} +
+
+
+
+ + +

+ این تاریخچه فقط رویدادهای عملیاتی همین پذیرنده را نشان می‌دهد. اطلاعات حساس پرداخت‌کننده و پاسخ خام + بانک در پنل نمایش داده نمی‌شود. +

+{/if} diff --git a/src/lib/components/TransactionsPage.svelte b/src/lib/components/TransactionsPage.svelte new file mode 100644 index 0000000..d289a9d --- /dev/null +++ b/src/lib/components/TransactionsPage.svelte @@ -0,0 +1,220 @@ + + +تراکنش‌ها | تحلیل زرین‌پال + + + + +
+
+ فیلترها +
+ +
+ + + + {#if filters.status !== 'all' || filters.minAmount !== null || filters.maxAmount !== null} + + {/if} +
+
+ + {#if loading} +
+ در حال دریافت تراکنش‌ها… +
+ {:else if error} + + {:else if result && result.items.length} +
+ + شناسه پرداختدرگاهزمانمبلغتلاش‌هاوضعیتجزئیات + + {#each result.items as item} + openRow(item.sessionId)} + onkeydown={(event) => { + if (event.key === 'Enter') openRow(item.sessionId); + }} + > + {item.sessionId} + {item.terminalId} + {formatDate(item.createdAt)} + {formatMoney(item.amountRial)} + {formatValue(item.attemptCount)} + {statusLabels[item.sessionStatus]} + + + {/each} + + +
+
+ {formatValue(result.items.length)} مورد در این صفحه +
+ {:else} +
+
+ +

تراکنشی با این فیلتر پیدا نشد

+

بازه یا فیلتر مبلغ و وضعیت را تغییر دهید.

+
+
+ {/if} +
+
diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..e286875 --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..f05fb87 --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from './badge.svelte'; +export { badgeVariants, type BadgeVariant } from './badge.svelte'; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..b7e9453 --- /dev/null +++ b/src/lib/components/ui/button/button.svelte @@ -0,0 +1,87 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..7ceb12a --- /dev/null +++ b/src/lib/components/ui/button/index.ts @@ -0,0 +1,12 @@ +import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, buttonVariants } from './button.svelte'; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/src/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..0469131 --- /dev/null +++ b/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..28a80d4 --- /dev/null +++ b/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..d8fc5e2 --- /dev/null +++ b/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..7cf4812 --- /dev/null +++ b/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..8dcf265 --- /dev/null +++ b/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7701531 --- /dev/null +++ b/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..03f9e92 --- /dev/null +++ b/src/lib/components/ui/card/card.svelte @@ -0,0 +1,25 @@ + + +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl', + className, + )} + {...restProps} +> + {@render children?.()} +
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..c721e71 --- /dev/null +++ b/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Action from './card-action.svelte'; +import Content from './card-content.svelte'; +import Description from './card-description.svelte'; +import Footer from './card-footer.svelte'; +import Header from './card-header.svelte'; +import Title from './card-title.svelte'; +import Root from './card.svelte'; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/src/lib/components/ui/chart/chart-container.svelte b/src/lib/components/ui/chart/chart-container.svelte new file mode 100644 index 0000000..6585375 --- /dev/null +++ b/src/lib/components/ui/chart/chart-container.svelte @@ -0,0 +1,80 @@ + + +
+ + {@render children?.()} +
diff --git a/src/lib/components/ui/chart/chart-style.svelte b/src/lib/components/ui/chart/chart-style.svelte new file mode 100644 index 0000000..7427797 --- /dev/null +++ b/src/lib/components/ui/chart/chart-style.svelte @@ -0,0 +1,37 @@ + + +{#if themeContents} + {#key id} + + {themeContents} + + {/key} +{/if} diff --git a/src/lib/components/ui/chart/chart-tooltip.svelte b/src/lib/components/ui/chart/chart-tooltip.svelte new file mode 100644 index 0000000..5adb0b5 --- /dev/null +++ b/src/lib/components/ui/chart/chart-tooltip.svelte @@ -0,0 +1,175 @@ + + +{#snippet TooltipLabel()} + {#if formattedLabel} +
+ {#if typeof formattedLabel === 'function'} + {@render formattedLabel()} + {:else} + {formattedLabel} + {/if} +
+ {/if} +{/snippet} + + +
+ {#if !nestLabel} + {@render TooltipLabel()} + {/if} +
+ {#each visibleSeries as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.label || 'value'}`} + {@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key, chartCtx.tooltip.data)} + {@const indicatorColor = color || item.config?.color || item.color} +
svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5', + indicator === 'dot' && 'items-center', + )} + > + {#if formatter && item.value !== undefined && item.label} + {@render formatter({ + value: item.value, + name: item.label, + item, + index: i, + payload: visibleSeries, + })} + {:else} + {#if itemConfig?.icon} + + {:else if !hideIndicator} +
+ {/if} +
+
+ {#if nestLabel} + {@render TooltipLabel()} + {/if} + + {itemConfig?.label || item.label} + +
+ {#if item.value !== undefined} + + {item.value.toLocaleString()} + + {/if} +
+ {/if} +
+ {/each} +
+
+
diff --git a/src/lib/components/ui/chart/chart-utils.ts b/src/lib/components/ui/chart/chart-utils.ts new file mode 100644 index 0000000..9cb853d --- /dev/null +++ b/src/lib/components/ui/chart/chart-utils.ts @@ -0,0 +1,65 @@ +import { getContext, setContext, type Component, type Snippet } from 'svelte'; +import type { Tooltip } from 'layerchart'; + +export const THEMES = { light: '', dark: '.dark' } as const; + +export type ChartConfig = { + [k in string]: { + label?: string; + icon?: Component; + } & ({ color?: string; theme?: never } | { color?: never; theme: Record }); +}; + +export type ExtractSnippetParams = T extends Snippet<[infer P]> ? P : never; + +export type TooltipPayload = Tooltip.TooltipSeries; + +// Helper to extract item config from a payload. +export function getPayloadConfigFromPayload( + config: ChartConfig, + payload: TooltipPayload, + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record | null, +) { + if (typeof payload !== 'object' || payload === null) return undefined; + + const payloadConfig = + 'config' in payload && typeof payload.config === 'object' && payload.config !== null + ? payload.config + : undefined; + + let configLabelKey: string = key; + + if (payload.key === key) { + configLabelKey = payload.key; + } else if (payload.label === key) { + configLabelKey = payload.label; + } else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadConfig !== undefined && + key in payloadConfig && + typeof payloadConfig[key as keyof typeof payloadConfig] === 'string' + ) { + configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string; + } else if (data != null && key in data && typeof data[key] === 'string') { + configLabelKey = data[key] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]; +} + +type ChartContextValue = { + config: ChartConfig; +}; + +const chartContextKey = Symbol('chart-context'); + +export function setChartContext(value: ChartContextValue) { + return setContext(chartContextKey, value); +} + +export function useChart() { + return getContext(chartContextKey); +} diff --git a/src/lib/components/ui/chart/index.ts b/src/lib/components/ui/chart/index.ts new file mode 100644 index 0000000..25cfcd4 --- /dev/null +++ b/src/lib/components/ui/chart/index.ts @@ -0,0 +1,6 @@ +import ChartContainer from './chart-container.svelte'; +import ChartTooltip from './chart-tooltip.svelte'; + +export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js'; + +export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip }; diff --git a/src/lib/components/ui/collapsible/collapsible-content.svelte b/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..6588d5c --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..a24a44e --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/collapsible/collapsible.svelte b/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..81bf979 --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/ui/collapsible/index.ts b/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..7b254a3 --- /dev/null +++ b/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Content from './collapsible-content.svelte'; +import Trigger from './collapsible-trigger.svelte'; +import Root from './collapsible.svelte'; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..6b4d728 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..66a2aba --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,44 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..7f5c55d --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..fc8aaf3 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..bf972af --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..4aa2eb3 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..08718eb --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,27 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..130c20f --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..434418a --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..5b4ac51 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,34 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..e391c96 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..e25e61f --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..4e79986 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..c2baf19 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..02f5f63 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..7409c77 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..5c2776c --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..6ec2de8 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import CheckboxGroup from './dropdown-menu-checkbox-group.svelte'; +import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; +import Content from './dropdown-menu-content.svelte'; +import GroupHeading from './dropdown-menu-group-heading.svelte'; +import Group from './dropdown-menu-group.svelte'; +import Item from './dropdown-menu-item.svelte'; +import Label from './dropdown-menu-label.svelte'; +import Portal from './dropdown-menu-portal.svelte'; +import RadioGroup from './dropdown-menu-radio-group.svelte'; +import RadioItem from './dropdown-menu-radio-item.svelte'; +import Separator from './dropdown-menu-separator.svelte'; +import Shortcut from './dropdown-menu-shortcut.svelte'; +import SubContent from './dropdown-menu-sub-content.svelte'; +import SubTrigger from './dropdown-menu-sub-trigger.svelte'; +import Sub from './dropdown-menu-sub.svelte'; +import Trigger from './dropdown-menu-trigger.svelte'; +import Root from './dropdown-menu.svelte'; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/src/lib/components/ui/empty/empty-content.svelte b/src/lib/components/ui/empty/empty-content.svelte new file mode 100644 index 0000000..4dc5aeb --- /dev/null +++ b/src/lib/components/ui/empty/empty-content.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-description.svelte b/src/lib/components/ui/empty/empty-description.svelte new file mode 100644 index 0000000..49e7e78 --- /dev/null +++ b/src/lib/components/ui/empty/empty-description.svelte @@ -0,0 +1,23 @@ + + +
a:hover]:text-primary text-sm/relaxed text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4', + className, + )} + {...restProps} +> + {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-header.svelte b/src/lib/components/ui/empty/empty-header.svelte new file mode 100644 index 0000000..efafb14 --- /dev/null +++ b/src/lib/components/ui/empty/empty-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-media.svelte b/src/lib/components/ui/empty/empty-media.svelte new file mode 100644 index 0000000..9d4fac7 --- /dev/null +++ b/src/lib/components/ui/empty/empty-media.svelte @@ -0,0 +1,41 @@ + + + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty-title.svelte b/src/lib/components/ui/empty/empty-title.svelte new file mode 100644 index 0000000..7c90720 --- /dev/null +++ b/src/lib/components/ui/empty/empty-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/empty.svelte b/src/lib/components/ui/empty/empty.svelte new file mode 100644 index 0000000..3583d0d --- /dev/null +++ b/src/lib/components/ui/empty/empty.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/empty/index.ts b/src/lib/components/ui/empty/index.ts new file mode 100644 index 0000000..c1d32c7 --- /dev/null +++ b/src/lib/components/ui/empty/index.ts @@ -0,0 +1,22 @@ +import Content from './empty-content.svelte'; +import Description from './empty-description.svelte'; +import Header from './empty-header.svelte'; +import Media from './empty-media.svelte'; +import Title from './empty-title.svelte'; +import Root from './empty.svelte'; + +export { + Root, + Header, + Media, + Title, + Description, + Content, + // + Root as Empty, + Header as EmptyHeader, + Media as EmptyMedia, + Title as EmptyTitle, + Description as EmptyDescription, + Content as EmptyContent, +}; diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..0888f0e --- /dev/null +++ b/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from './input.svelte'; + +export { + Root, + // + Root as Input, +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..955e2ec --- /dev/null +++ b/src/lib/components/ui/input/input.svelte @@ -0,0 +1,48 @@ + + +{#if type === 'file'} + +{:else} + +{/if} diff --git a/src/lib/components/ui/pagination/index.ts b/src/lib/components/ui/pagination/index.ts new file mode 100644 index 0000000..29e5695 --- /dev/null +++ b/src/lib/components/ui/pagination/index.ts @@ -0,0 +1,31 @@ +import Content from './pagination-content.svelte'; +import Ellipsis from './pagination-ellipsis.svelte'; +import Item from './pagination-item.svelte'; +import Link from './pagination-link.svelte'; +import NextButton from './pagination-next-button.svelte'; +import Next from './pagination-next.svelte'; +import PrevButton from './pagination-prev-button.svelte'; +import Previous from './pagination-previous.svelte'; +import Root from './pagination.svelte'; + +export { + Root, + Content, + Item, + Link, + PrevButton, // old + NextButton, // old + Ellipsis, + Previous, + Next, + // + Root as Pagination, + Content as PaginationContent, + Item as PaginationItem, + Link as PaginationLink, + PrevButton as PaginationPrevButton, // old + NextButton as PaginationNextButton, // old + Ellipsis as PaginationEllipsis, + Previous as PaginationPrevious, + Next as PaginationNext, +}; diff --git a/src/lib/components/ui/pagination/pagination-content.svelte b/src/lib/components/ui/pagination/pagination-content.svelte new file mode 100644 index 0000000..268a134 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-content.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
diff --git a/src/lib/components/ui/pagination/pagination-ellipsis.svelte b/src/lib/components/ui/pagination/pagination-ellipsis.svelte new file mode 100644 index 0000000..f3ebde2 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-ellipsis.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/lib/components/ui/pagination/pagination-item.svelte b/src/lib/components/ui/pagination/pagination-item.svelte new file mode 100644 index 0000000..519d106 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-item.svelte @@ -0,0 +1,10 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/pagination/pagination-link.svelte b/src/lib/components/ui/pagination/pagination-link.svelte new file mode 100644 index 0000000..496cb44 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-link.svelte @@ -0,0 +1,42 @@ + + +{#snippet Fallback()} + {page.value} +{/snippet} + + + {#if children} + {@render children?.()} + {:else} + {@render Fallback()} + {/if} + diff --git a/src/lib/components/ui/pagination/pagination-next-button.svelte b/src/lib/components/ui/pagination/pagination-next-button.svelte new file mode 100644 index 0000000..a508a8b --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-next-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + Next + +{/snippet} + + + {#if children} + {@render children?.()} + {:else} + {@render Fallback()} + {/if} + diff --git a/src/lib/components/ui/pagination/pagination-next.svelte b/src/lib/components/ui/pagination/pagination-next.svelte new file mode 100644 index 0000000..8486026 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-next.svelte @@ -0,0 +1,22 @@ + + + + + + diff --git a/src/lib/components/ui/pagination/pagination-prev-button.svelte b/src/lib/components/ui/pagination/pagination-prev-button.svelte new file mode 100644 index 0000000..fde450d --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-prev-button.svelte @@ -0,0 +1,31 @@ + + +{#snippet Fallback()} + + Previous +{/snippet} + + + {#if children} + {@render children?.()} + {:else} + {@render Fallback()} + {/if} + diff --git a/src/lib/components/ui/pagination/pagination-previous.svelte b/src/lib/components/ui/pagination/pagination-previous.svelte new file mode 100644 index 0000000..9b581b5 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination-previous.svelte @@ -0,0 +1,22 @@ + + + + + + diff --git a/src/lib/components/ui/pagination/pagination.svelte b/src/lib/components/ui/pagination/pagination.svelte new file mode 100644 index 0000000..d23ebf7 --- /dev/null +++ b/src/lib/components/ui/pagination/pagination.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000..db7b003 --- /dev/null +++ b/src/lib/components/ui/popover/index.ts @@ -0,0 +1,28 @@ +import Close from './popover-close.svelte'; +import Content from './popover-content.svelte'; +import Description from './popover-description.svelte'; +import Header from './popover-header.svelte'; +import Portal from './popover-portal.svelte'; +import Title from './popover-title.svelte'; +import Trigger from './popover-trigger.svelte'; +import Root from './popover.svelte'; + +export { + Root, + Content, + Description, + Header, + Title, + Trigger, + Close, + Portal, + // + Root as Popover, + Content as PopoverContent, + Description as PopoverDescription, + Header as PopoverHeader, + Title as PopoverTitle, + Trigger as PopoverTrigger, + Close as PopoverClose, + Portal as PopoverPortal, +}; diff --git a/src/lib/components/ui/popover/popover-close.svelte b/src/lib/components/ui/popover/popover-close.svelte new file mode 100644 index 0000000..c9518f4 --- /dev/null +++ b/src/lib/components/ui/popover/popover-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000..aec952b --- /dev/null +++ b/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/ui/popover/popover-description.svelte b/src/lib/components/ui/popover/popover-description.svelte new file mode 100644 index 0000000..43525c8 --- /dev/null +++ b/src/lib/components/ui/popover/popover-description.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/popover/popover-header.svelte b/src/lib/components/ui/popover/popover-header.svelte new file mode 100644 index 0000000..6641adf --- /dev/null +++ b/src/lib/components/ui/popover/popover-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/popover/popover-portal.svelte b/src/lib/components/ui/popover/popover-portal.svelte new file mode 100644 index 0000000..234766e --- /dev/null +++ b/src/lib/components/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/popover/popover-title.svelte b/src/lib/components/ui/popover/popover-title.svelte new file mode 100644 index 0000000..fc91d53 --- /dev/null +++ b/src/lib/components/ui/popover/popover-title.svelte @@ -0,0 +1,15 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/popover/popover-trigger.svelte b/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000..bbb081b --- /dev/null +++ b/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/lib/components/ui/popover/popover.svelte b/src/lib/components/ui/popover/popover.svelte new file mode 100644 index 0000000..4551a94 --- /dev/null +++ b/src/lib/components/ui/popover/popover.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/select/index.ts b/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..06762f3 --- /dev/null +++ b/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import Content from './select-content.svelte'; +import GroupHeading from './select-group-heading.svelte'; +import Group from './select-group.svelte'; +import Item from './select-item.svelte'; +import Label from './select-label.svelte'; +import Portal from './select-portal.svelte'; +import ScrollDownButton from './select-scroll-down-button.svelte'; +import ScrollUpButton from './select-scroll-up-button.svelte'; +import Separator from './select-separator.svelte'; +import Trigger from './select-trigger.svelte'; +import Root from './select.svelte'; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + Portal, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal, +}; diff --git a/src/lib/components/ui/select/select-content.svelte b/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..659f7e6 --- /dev/null +++ b/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,43 @@ + + + + + + + {@render children?.()} + + + + diff --git a/src/lib/components/ui/select/select-group-heading.svelte b/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..7354e57 --- /dev/null +++ b/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/select/select-group.svelte b/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..ebf0ca6 --- /dev/null +++ b/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/ui/select/select-item.svelte b/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..b204d59 --- /dev/null +++ b/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,40 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + + {/snippet} + diff --git a/src/lib/components/ui/select/select-label.svelte b/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..027a1d4 --- /dev/null +++ b/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/select/select-portal.svelte b/src/lib/components/ui/select/select-portal.svelte new file mode 100644 index 0000000..fa48d6a --- /dev/null +++ b/src/lib/components/ui/select/select-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/select/select-scroll-down-button.svelte b/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..b67d1ef --- /dev/null +++ b/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/components/ui/select/select-scroll-up-button.svelte b/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..e093c57 --- /dev/null +++ b/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/components/ui/select/select-separator.svelte b/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..ce1f770 --- /dev/null +++ b/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/ui/select/select-trigger.svelte b/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..9c0b9ba --- /dev/null +++ b/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/select/select.svelte b/src/lib/components/ui/select/select.svelte new file mode 100644 index 0000000..0ed12dd --- /dev/null +++ b/src/lib/components/ui/select/select.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..56b2767 --- /dev/null +++ b/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from './separator.svelte'; + +export { + Root, + // + Root as Separator, +}; diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..e568b9a --- /dev/null +++ b/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/ui/sheet/index.ts b/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..2770976 --- /dev/null +++ b/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,34 @@ +import Close from './sheet-close.svelte'; +import Content from './sheet-content.svelte'; +import Description from './sheet-description.svelte'; +import Footer from './sheet-footer.svelte'; +import Header from './sheet-header.svelte'; +import Overlay from './sheet-overlay.svelte'; +import Portal from './sheet-portal.svelte'; +import Title from './sheet-title.svelte'; +import Trigger from './sheet-trigger.svelte'; +import Root from './sheet.svelte'; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription, +}; diff --git a/src/lib/components/ui/sheet/sheet-close.svelte b/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000..e7b07bf --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-content.svelte b/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000..50b50df --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,55 @@ + + + + + + + + {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} + + diff --git a/src/lib/components/ui/sheet/sheet-description.svelte b/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000..bbd52f2 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-footer.svelte b/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000..a5c8454 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sheet/sheet-header.svelte b/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000..36de9c4 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000..18aa450 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-portal.svelte b/src/lib/components/ui/sheet/sheet-portal.svelte new file mode 100644 index 0000000..a67693a --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-title.svelte b/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000..84e2f00 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-trigger.svelte b/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000..860f048 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet.svelte b/src/lib/components/ui/sheet/sheet.svelte new file mode 100644 index 0000000..9989a4f --- /dev/null +++ b/src/lib/components/ui/sheet/sheet.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sidebar/constants.ts b/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..9e59823 --- /dev/null +++ b/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = 'sidebar_state'; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = '16rem'; +export const SIDEBAR_WIDTH_MOBILE = '18rem'; +export const SIDEBAR_WIDTH_ICON = '3rem'; +export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; diff --git a/src/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..a6401db --- /dev/null +++ b/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,79 @@ +import { getContext, setContext } from 'svelte'; +import { IsMobile } from '$lib/hooks/is-mobile.svelte.js'; +import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js'; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps['setOpen']; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? 'expanded' : 'collapsed')); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = 'scn-sidebar'; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..4edd7c3 --- /dev/null +++ b/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import Content from './sidebar-content.svelte'; +import Footer from './sidebar-footer.svelte'; +import GroupAction from './sidebar-group-action.svelte'; +import GroupContent from './sidebar-group-content.svelte'; +import GroupLabel from './sidebar-group-label.svelte'; +import Group from './sidebar-group.svelte'; +import Header from './sidebar-header.svelte'; +import Input from './sidebar-input.svelte'; +import Inset from './sidebar-inset.svelte'; +import MenuAction from './sidebar-menu-action.svelte'; +import MenuBadge from './sidebar-menu-badge.svelte'; +import MenuButton from './sidebar-menu-button.svelte'; +import MenuItem from './sidebar-menu-item.svelte'; +import MenuSkeleton from './sidebar-menu-skeleton.svelte'; +import MenuSubButton from './sidebar-menu-sub-button.svelte'; +import MenuSubItem from './sidebar-menu-sub-item.svelte'; +import MenuSub from './sidebar-menu-sub.svelte'; +import Menu from './sidebar-menu.svelte'; +import Provider from './sidebar-provider.svelte'; +import Rail from './sidebar-rail.svelte'; +import Separator from './sidebar-separator.svelte'; +import Trigger from './sidebar-trigger.svelte'; +import Root from './sidebar.svelte'; +import { useSidebar } from './context.svelte.js'; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar, +}; diff --git a/src/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000..1bf2bd5 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000..6a7171e --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000..6e055bb --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000..5e27abd --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000..b83ea7c --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render children?.()} +
    +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000..19ce428 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000..4925c75 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000..ac821f3 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000..1fb734d --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,23 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000..48728b9 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,37 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000..3d15d5e --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000..94f1aa6 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,101 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000..634a08a --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000..a3c1ac2 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000..0452a1f --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,39 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000..0516c06 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000..b207dd4 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,24 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000..b1c20e7 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000..4245b91 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,50 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/src/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000..a552a92 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000..09e0b67 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000..6a38ba2 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000..47c6fb8 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,99 @@ + + +{#if collapsible === 'none'} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> + button]:hidden', className)} + style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};" + {side} + > + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/src/lib/components/ui/skeleton/index.ts b/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..2d93ff2 --- /dev/null +++ b/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from './skeleton.svelte'; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..1737f85 --- /dev/null +++ b/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/src/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..53d63a5 --- /dev/null +++ b/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Body from './table-body.svelte'; +import Caption from './table-caption.svelte'; +import Cell from './table-cell.svelte'; +import Footer from './table-footer.svelte'; +import Head from './table-head.svelte'; +import Header from './table-header.svelte'; +import Row from './table-row.svelte'; +import Root from './table.svelte'; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/src/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..26315a8 --- /dev/null +++ b/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..dbde6ca --- /dev/null +++ b/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..1f60661 --- /dev/null +++ b/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..e8293a9 --- /dev/null +++ b/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0', className)} + {...restProps} +> + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..156dea6 --- /dev/null +++ b/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..e51a3ad --- /dev/null +++ b/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..894e399 --- /dev/null +++ b/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..83eaeee --- /dev/null +++ b/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
    + + {@render children?.()} +
    +
    diff --git a/src/lib/components/ui/toggle-group/index.ts b/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 0000000..9b91e9a --- /dev/null +++ b/src/lib/components/ui/toggle-group/index.ts @@ -0,0 +1,10 @@ +import Item from './toggle-group-item.svelte'; +import Root from './toggle-group.svelte'; + +export { + Root, + Item, + // + Root as ToggleGroup, + Item as ToggleGroupItem, +}; diff --git a/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 0000000..8091ffb --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/lib/components/ui/toggle-group/toggle-group.svelte b/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 0000000..6d99c98 --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,75 @@ + + + + + + diff --git a/src/lib/components/ui/toggle/index.ts b/src/lib/components/ui/toggle/index.ts new file mode 100644 index 0000000..67d8c87 --- /dev/null +++ b/src/lib/components/ui/toggle/index.ts @@ -0,0 +1,8 @@ +import Root from './toggle.svelte'; +export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants } from './toggle.svelte'; + +export { + Root, + // + Root as Toggle, +}; diff --git a/src/lib/components/ui/toggle/toggle.svelte b/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 0000000..74cd2a3 --- /dev/null +++ b/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,51 @@ + + + + + diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..ace2ed2 --- /dev/null +++ b/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Content from './tooltip-content.svelte'; +import Portal from './tooltip-portal.svelte'; +import Provider from './tooltip-provider.svelte'; +import Trigger from './tooltip-trigger.svelte'; +import Root from './tooltip.svelte'; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..d133fbe --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/src/lib/components/ui/tooltip/tooltip-portal.svelte b/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..7b9e8f9 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip-provider.svelte b/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..49f8379 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..f9a3bf4 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip.svelte b/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..5a327a7 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/dashboard-context.svelte.ts b/src/lib/dashboard-context.svelte.ts new file mode 100644 index 0000000..83f2e37 --- /dev/null +++ b/src/lib/dashboard-context.svelte.ts @@ -0,0 +1,63 @@ +import { getContext, setContext } from 'svelte'; + +import { + ApiError, + api, + type AnalyticsRun, + type AuthSession, + type ReportingPeriod, + type Terminal, +} from '$lib/api/client'; +import { rangePeriodTypes, type DashboardRange } from '$lib/dashboard-filters'; + +const contextKey = Symbol('zarinpal-dashboard'); + +export class DashboardContext { + session = $state(null); + periods = $state([]); + terminals = $state([]); + ready = $state(false); + error = $state(''); + + async initialize() { + try { + const [session, periodList, terminalList] = await Promise.all([ + api.session(), + api.periods(), + api.terminals(), + ]); + this.session = session; + this.periods = periodList.periods; + this.terminals = terminalList.terminals; + } catch (error) { + if (error instanceof ApiError && error.status === 401) throw error; + this.error = 'اطلاعات پنل در دسترس نیست. لطفاً دوباره تلاش کنید.'; + throw error; + } finally { + this.ready = true; + } + } + + periodFor(range: DashboardRange): ReportingPeriod | undefined { + return this.periods.find((period) => period.periodType === rangePeriodTypes[range]); + } + + async runFor(range: DashboardRange, terminal: string): Promise { + const period = this.periodFor(range); + if (!this.session || !period) throw new Error('Dashboard context is not ready'); + return api.startRun(period.periodId, this.session.csrfToken, terminal === 'all' ? undefined : terminal); + } + + async logout() { + if (this.session) await api.logout(this.session.csrfToken); + this.session = null; + } +} + +export function provideDashboardContext() { + return setContext(contextKey, new DashboardContext()); +} + +export function useDashboardContext() { + return getContext(contextKey); +} diff --git a/src/lib/dashboard-filters.test.ts b/src/lib/dashboard-filters.test.ts new file mode 100644 index 0000000..5d4803b --- /dev/null +++ b/src/lib/dashboard-filters.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import { parseDashboardFilters, setDashboardQuery, type DashboardRange } from './dashboard-filters'; + +describe('dashboard filters', () => { + const terminals = ['T83', 'T84']; + + it('uses safe, shareable defaults for invalid query values', () => { + const query = new URLSearchParams('range=year&terminal=someone-elses-terminal&minAmount=-1'); + + expect(parseDashboardFilters(query, terminals)).toEqual({ + range: 'month', + terminal: 'all', + status: 'all', + minAmount: null, + maxAmount: null, + }); + }); + + it.each(['day', 'week', 'month', 'season'])( + 'keeps the %s range in the copied URL', + (range) => { + const next = setDashboardQuery(new URL('https://example.test/transactions?terminal=T83'), { + range, + }); + + expect(next.searchParams.get('range')).toBe(range); + expect(next.searchParams.get('terminal')).toBe('T83'); + }, + ); + + it('normalizes an inverted amount range', () => { + const filters = parseDashboardFilters( + new URLSearchParams('minAmount=900000&maxAmount=100000'), + terminals, + ); + + expect(filters.minAmount).toBe(100000); + expect(filters.maxAmount).toBe(900000); + }); +}); diff --git a/src/lib/dashboard-filters.ts b/src/lib/dashboard-filters.ts new file mode 100644 index 0000000..c0b017d --- /dev/null +++ b/src/lib/dashboard-filters.ts @@ -0,0 +1,68 @@ +export type DashboardRange = 'day' | 'week' | 'month' | 'season'; +export type TransactionStatus = 'all' | 'Paid' | 'Verified' | 'Failed' | 'Reversed'; + +export interface DashboardFilters { + range: DashboardRange; + terminal: string; + status: TransactionStatus; + minAmount: number | null; + maxAmount: number | null; +} + +const ranges = new Set(['day', 'week', 'month', 'season']); +const statuses = new Set(['all', 'Paid', 'Verified', 'Failed', 'Reversed']); + +function positiveAmount(value: string | null): number | null { + if (!value) return null; + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null; +} + +export function parseDashboardFilters( + query: URLSearchParams, + terminalIds: readonly string[] = [], +): DashboardFilters { + const rawRange = query.get('range') as DashboardRange | null; + const rawStatus = query.get('status') as TransactionStatus | null; + const rawTerminal = query.get('terminal'); + let minAmount = positiveAmount(query.get('minAmount')); + let maxAmount = positiveAmount(query.get('maxAmount')); + + if (minAmount !== null && maxAmount !== null && minAmount > maxAmount) { + [minAmount, maxAmount] = [maxAmount, minAmount]; + } + + return { + range: rawRange && ranges.has(rawRange) ? rawRange : 'month', + terminal: rawTerminal && terminalIds.includes(rawTerminal) ? rawTerminal : 'all', + status: rawStatus && statuses.has(rawStatus) ? rawStatus : 'all', + minAmount, + maxAmount, + }; +} + +export function setDashboardQuery( + current: URL, + updates: Partial>, +): URL { + const next = new URL(current); + for (const [key, value] of Object.entries(updates)) { + if (value === null || value === '' || value === 'all') next.searchParams.delete(key); + else next.searchParams.set(key, String(value)); + } + return next; +} + +export const rangeLabels: Record = { + day: 'روزانه', + week: 'هفتگی', + month: 'ماهانه', + season: 'فصلی', +}; + +export const rangePeriodTypes: Record = { + day: 'DAY', + week: 'WEEK', + month: 'JALALI_MONTH', + season: 'PERSIAN_SEASON', +}; diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..e3802d3 --- /dev/null +++ b/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from 'svelte/reactivity'; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/src/lib/insights.test.ts b/src/lib/insights.test.ts new file mode 100644 index 0000000..0308dbb --- /dev/null +++ b/src/lib/insights.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; + +import { describePercentile, formulas } from './insights'; + +describe('merchant-friendly insights', () => { + it('celebrates strong peer performance without claiming causality', () => { + expect(describePercentile('payment_success_rate', 82)).toContain('پیشرو'); + }); + + it('frames a low percentile as an attainable opportunity', () => { + const copy = describePercentile('payment_success_rate', 24); + + expect(copy).toContain('فرصت'); + expect(copy).not.toContain('شکست'); + expect(copy).not.toContain('باخته'); + }); + + it('explains the success-rate numerator and denominator', () => { + expect(formulas.paymentSuccess).toContain('پرداخت‌های موفق بانکی'); + expect(formulas.paymentSuccess).toContain('همه نشست‌های پرداخت'); + }); +}); diff --git a/src/lib/insights.ts b/src/lib/insights.ts new file mode 100644 index 0000000..d058b2d --- /dev/null +++ b/src/lib/insights.ts @@ -0,0 +1,28 @@ +export const formulas = { + paymentSuccess: 'تعداد پرداخت‌های موفق بانکی ÷ همه نشست‌های پرداخت × ۱۰۰', + verification: 'پرداخت‌های تأییدشده ÷ پرداخت‌های موفق بانکی × ۱۰۰', + retryRecovery: 'پرداخت‌های موفق پس از تلاش دوباره ÷ همه پرداخت‌های دارای تلاش دوباره × ۱۰۰', + feeRate: 'مجموع کارمزد پرداخت‌شده ÷ مبلغ پرداخت‌های موفق بانکی × ۱۰۰', +}; + +const metricNames: Record = { + payment_success_rate: 'موفقیت پرداخت', + verification_completion_rate: 'تکمیل تأیید', + retry_recovery_rate: 'بازیابی تلاش دوباره', + paid_fee_rate: 'نرخ کارمزد', + init_time_ms_p50: 'سرعت شروع پرداخت', + init_time_ms_p95: 'پایداری شروع پرداخت', + verify_time_ms_p50: 'سرعت تأیید پرداخت', + verify_time_ms_p95: 'پایداری تأیید پرداخت', +}; + +export function describePercentile(metricId: string, percentile: number): string { + const name = metricNames[metricId] ?? 'عملکرد این شاخص'; + if (percentile >= 75) { + return `${name} شما در جمع کسب‌وکارهای پیشرو این گروه قرار دارد؛ همین روند خوب را حفظ کنید.`; + } + if (percentile >= 45) { + return `${name} شما هم‌سطح بخش میانی کسب‌وکارهای مشابه است و با چند بهبود کوچک می‌تواند جلوتر برود.`; + } + return `در ${name} هنوز فرصت رشد خوبی دارید؛ بررسی مسیرهای ناموفق می‌تواند قدم بعدی روشنی نشان دهد.`; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..ee33998 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..2e5d2c9 --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,34 @@ + + +{#if !dashboard.ready} +
    +
    + +

    در حال آماده‌سازی پنل…

    +
    +
    +{:else if dashboard.session} + {@render children()} +{:else if dashboard.error} +

    {dashboard.error}

    +{/if} diff --git a/src/routes/(app)/dashboard/+page.svelte b/src/routes/(app)/dashboard/+page.svelte new file mode 100644 index 0000000..c75cfaf --- /dev/null +++ b/src/routes/(app)/dashboard/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/fees/+page.svelte b/src/routes/(app)/fees/+page.svelte new file mode 100644 index 0000000..976212b --- /dev/null +++ b/src/routes/(app)/fees/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/methodology/+page.svelte b/src/routes/(app)/methodology/+page.svelte new file mode 100644 index 0000000..9e62a44 --- /dev/null +++ b/src/routes/(app)/methodology/+page.svelte @@ -0,0 +1,35 @@ + + +روش محاسبه | تحلیل زرین‌پال + +
    + {#each [['نرخ موفقیت پرداخت', formulas.paymentSuccess, 'موفقیت بانکی معیار است و تأیید پذیرنده جداگانه گزارش می‌شود.'], ['تکمیل تأیید', formulas.verification, 'نشان می‌دهد چه سهمی از پرداخت‌های موفق به تأیید نهایی رسیده‌اند.'], ['بازیابی تلاش دوباره', formulas.retryRecovery, 'فقط نشست‌هایی در مخرج هستند که بیش از یک تلاش داشته‌اند.'], ['نرخ مؤثر کارمزد', formulas.feeRate, 'مبلغ واقعی کارمزد ثبت‌شده در داده مبنای محاسبه است.']] as item} +

    {item[0]}

    + {item[1]} +

    {item[2]}

    + {/each} +
    + +

    + مقایسه هم‌گروه فقط زمانی نمایش داده می‌شود که تعداد کسب‌وکارها و نشست‌ها به حد امن برسد. هیچ عددی از یک + پذیرنده دیگر به‌صورت جداگانه قابل مشاهده نیست. +

    diff --git a/src/routes/(app)/payments/+page.svelte b/src/routes/(app)/payments/+page.svelte new file mode 100644 index 0000000..a46fa13 --- /dev/null +++ b/src/routes/(app)/payments/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/peers/+page.svelte b/src/routes/(app)/peers/+page.svelte new file mode 100644 index 0000000..6f0f578 --- /dev/null +++ b/src/routes/(app)/peers/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/transactions/+page.svelte b/src/routes/(app)/transactions/+page.svelte new file mode 100644 index 0000000..5569928 --- /dev/null +++ b/src/routes/(app)/transactions/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/(app)/transactions/[sessionId]/+page.svelte b/src/routes/(app)/transactions/[sessionId]/+page.svelte new file mode 100644 index 0000000..89c3361 --- /dev/null +++ b/src/routes/(app)/transactions/[sessionId]/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5853ad7..67a4236 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,8 +1,8 @@