Compare commits

...

221 Commits

Author SHA1 Message Date
masoodafar-web 58037f0b49 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m9s
2026-08-24 00:10:52 +03:30
masoodafar-web aae4b2693d fix(checkout): add missing JSInterop import for enhanced interactivity
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m47s
- Included the Microsoft.JSInterop namespace in the Checkout.razor.cs file to enable JavaScript interop functionality.
- This addition prepares the Checkout component for future enhancements that may require interaction with JavaScript libraries or functions.

These changes improve the potential for interactive features in the checkout process, enhancing user experience.
2026-08-23 23:46:35 +03:30
masoodafar-web 61fd805c75 feat(profile): enhance withdrawal request processing with IBAN normalization
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m8s
- Implemented a new method to normalize Iranian IBANs, ensuring they are formatted correctly before processing withdrawal requests.
- Added validation to provide user feedback if the IBAN format is invalid, improving the user experience during withdrawal submissions.
- Updated the withdrawal request logic to utilize the normalized IBAN, enhancing the reliability of the withdrawal process.

These changes improve the accuracy and usability of the withdrawal request feature, ensuring users can submit valid IBANs effectively.
2026-08-23 23:01:39 +03:30
masoodafar-web ef6d233bbc refactor(checkout): simplify address management and improve package selection logic
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m45s
- Removed the address selection UI and related logic from the Checkout page to streamline the user experience.
- Updated the payment validation to only require package selection, enhancing clarity for users.
- Adjusted the loading of addresses in related components based on user authentication status, ensuring addresses are only loaded when available.

These changes improve the overall usability of the checkout process by focusing on essential selections and reducing unnecessary complexity.
2026-08-23 22:32:47 +03:30
masoodafar-web 3d84e726db Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m27s
2026-08-09 02:33:27 +03:30
masoodafar-web 13091849cf feat(wallet): update ChargeCreditWallet component for improved magic ceiling consent handling
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m1s
- Enhanced the user interface to provide clearer instructions and options for charging without a magic multiplier when the magic wallet ceiling is full.
- Removed the previous consent prompting logic and replaced it with direct buttons for user actions.
- Updated methods to streamline the consent process and improve navigation based on user choices.

These changes enhance user experience by simplifying the consent flow and providing immediate options for wallet charging.
2026-08-09 00:11:49 +03:30
masoodafar-web 8e98c50bdf feat(wallet): enhance ChargeCreditWallet and Wallet components with magic ceiling consent logic
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m29s
- Added new logic to handle user consent for charging without a magic multiplier when the magic wallet ceiling is full.
- Updated the ChargeCreditWallet component to display alerts and prompts based on the user's magic wallet status and consent.
- Enhanced the Wallet component to conditionally render buttons and alerts related to the magic wallet ceiling status.
- Improved user feedback in the InsufficientCreditDialog to guide users on charging options based on their wallet status.

These changes enhance the user experience by providing clear instructions and options for charging wallets under specific conditions.
2026-08-08 23:59:36 +03:30
masoodafar-web 6123e47968 feat(wallet): enhance ChargeCreditWallet and Wallet components with magic wallet functionality
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m28s
- Added logic to handle redirection and alerts for users with a magic wallet status in the ChargeCreditWallet component.
- Updated the Wallet component to conditionally render buttons and alerts based on the user's magic wallet status.
- Implemented a method to check the magic wallet status and provide user feedback regarding wallet charging options.

These changes improve user experience by clearly guiding users on the appropriate wallet charging methods based on their current wallet status.
2026-08-08 22:22:03 +03:30
masoodafar-web acad1a719f feat(index): add conditional rendering for testimonials section
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m31s
- Implemented a check to display the testimonials section only when there are testimonials available, enhancing the user experience by preventing empty sections.
- Updated the logic for loading testimonials from settings to handle cases where the testimonials list may be null, ensuring robustness in data handling.

These changes improve the layout and presentation of the Index page, providing users with relevant content based on available testimonials.
2026-08-07 18:39:49 +03:30
masoodafar-web c7e6d01c11 feat(orders): enhance order display and interaction for improved user experience
- Refactored the Orders page to improve the layout and presentation of order information, including a new responsive design for mobile and desktop views.
- Added a button to navigate to the store when no orders are present, enhancing user engagement.
- Updated the order details display to include payment and delivery status with color-coded chips for better visual feedback.
- Introduced helper methods for formatting prices and dates, improving code readability and maintainability.

These changes significantly enhance the usability and clarity of the order management interface, providing users with a more intuitive experience.
2026-08-07 18:28:38 +03:30
masoodafar-web c764614d26 feat(order-tracking): enhance tracking steps logic and delivery status handling
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m52s
- Refactored the BuildTrackingSteps method to improve the logic for displaying order tracking steps based on various delivery statuses.
- Added handling for new delivery statuses: "آماده تحویل در دفتر" and "مرجوع شده".
- Updated the payment status step to consolidate descriptions based on payment completion.
- Enhanced the overall user experience by providing clearer tracking information for different order states.

These changes improve the clarity and functionality of the order tracking feature, ensuring users receive accurate updates on their order status.
2026-08-07 17:35:05 +03:30
masoodafar-web 9203dc7924 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m24s
2026-08-07 14:38:22 +03:30
masoodafar-web b64d99c5b3 feat(profile): enhance ChargeCreditWallet and Wallet components with club membership checks
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 4m42s
- Added logic to display alerts and buttons based on club membership status in the ChargeCreditWallet and Wallet components.
- Implemented user authentication checks to determine if the user is a club member and has purchased a package before allowing wallet charging.
- Updated UI to guide users on purchasing packages or signing club contracts if they are not eligible to charge their wallet.

These changes improve user experience by providing clear instructions and preventing unauthorized wallet charges.
2026-08-06 23:59:42 +03:30
masoodafar-web 4989cd00e0 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 16m3s
2026-07-26 00:52:30 +03:30
masoodafar-web a708727241 feat: enhance product listing and navigation functionality
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Added a wrapper div for product items in the DiscountStore and Store pages to improve layout consistency.
- Implemented state management for URL synchronization and scroll restoration in both product components.
- Refactored product loading logic to streamline data retrieval and improve user experience when navigating between product pages.

These changes enhance the overall functionality and user experience of the product listing pages, ensuring better state management and navigation.
2026-07-26 00:17:55 +03:30
masoodafar-web bed35935ad Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 17m17s
2026-07-15 22:02:31 +03:30
masoodafar-web 2339ca99b4 fix: correct Persian labels in OrganizationChart component
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Updated the labels for left and right legs in the OrganizationChart component from "پای چپ" and "پای راست" to "سازمان چپ" and "سازمان راست" for improved clarity and accuracy.
- Removed unnecessary whitespace in the FrontOffice.Main.csproj file.

These changes enhance the user interface by ensuring the terminology used in the OrganizationChart is more appropriate for the context.
2026-07-15 21:51:38 +03:30
masoodafar-web ceaaac3e23 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 18m42s
2026-07-03 06:04:56 +03:30
masoodafar-web 7f5a4f2c37 feat: enhance SEO and sitemap functionality
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Updated the sitemap generation to return a byte array instead of a string for improved performance.
- Added new SEO metadata for categories and store chooser routes to enhance search engine visibility.
- Implemented redirects for legacy wishlist URLs to the homepage, improving user experience.
- Updated robots.txt to disallow indexing of specific paths, ensuring better control over search engine crawling.

These changes significantly improve the SEO capabilities and sitemap handling of the application, enhancing both performance and user navigation.
2026-07-03 06:04:44 +03:30
masoodafar-web db35814ce3 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-03 05:43:14 +03:30
masoodafar-web 06100ca6dd fix: update sitemap generation to use XmlWriter for improved XML handling
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Changed the encoding in the XML declaration from "UTF-8" to "utf-8" for consistency.
- Replaced StringWriter with XmlWriter to enhance XML document writing and ensure proper encoding settings.
- Updated the return statement to convert the MemoryStream to a UTF-8 string, improving the efficiency of the sitemap generation process.

These changes enhance the reliability and correctness of the sitemap generation, ensuring better compliance with XML standards.
2026-07-03 05:30:23 +03:30
masoodafar-web 7a5a2110c6 feat: add SEO configuration for production environment
Build and Deploy to Production / build-and-deploy (push) Failing after 19m58s
- Introduced SEO settings in appsettings.Production.json, including SiteUrl, SiteName, and DefaultDescription.
- These additions enhance the application's search engine optimization capabilities, improving visibility and user engagement.
2026-07-01 21:48:49 +03:30
masoodafar-web 8dbc64cd86 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-01 21:45:43 +03:30
masoodafar-web 2573729db5 feat: implement SEO enhancements and sitemap generation
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Added SEO configuration settings in appsettings for both development and staging environments, including site URL, name, and default description.
- Registered SEO-related services in the dependency injection container, including `SeoMetadataProvider` and `SitemapGenerator`.
- Updated the main program to configure SEO settings and map a new endpoint for generating the sitemap.
- Enhanced the `_Host.cshtml` page to utilize SEO metadata, including title, description, and Open Graph tags, improving search engine visibility and user engagement.

These changes significantly enhance the SEO capabilities of the application, ensuring better indexing and presentation in search results.
2026-07-01 21:45:24 +03:30
masoodafar-web deadd197e6 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-01 21:31:49 +03:30
masoodafar-web 5d58d354d7 feat: enhance product detail loading and initialization logic
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m50s
- Introduced an `_initialized` flag to manage product loading state more effectively.
- Updated `OnParametersSetAsync` to conditionally load product details based on the initialization state and valid product ID.
- Refactored product retrieval logic in `ProductService` to handle invalid IDs and improve fallback mechanisms for fetching product details.

These changes improve the reliability and performance of the product detail component, ensuring that product information is loaded correctly and efficiently.
2026-07-01 21:21:57 +03:30
masoodafar-web 671c8407e5 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-06-30 02:50:03 +03:30
masoodafar-web 454d27f37e feat: add new sections for steps, features, and stats on the landing page
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Introduced a "How It Works" section with a three-step guide to enhance user onboarding.
- Added a "Features" section showcasing six key features with icons to highlight the platform's benefits.
- Implemented a "Stats" section displaying counter cards for key metrics, improving the overall presentation of information on the landing page.

These changes enhance the user experience by providing clear and engaging content that guides users through the platform's offerings.
2026-06-30 02:49:50 +03:30
masoodafar-web 1956dcc5f4 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-06-30 02:28:13 +03:30
masoodafar-web 37c7cebf92 feat: update product service to support in-stock filtering and enhance top-selling product retrieval
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 23m11s
- Modified `GetTopSellingAsync` methods in `ProductService` and `DiscountProductService` to include an optional `inStock` parameter for filtering products based on availability.
- Introduced a constant for the top product count in `Index.razor.cs` to improve maintainability and readability.
- Updated the product retrieval logic in `Index.razor.cs` to utilize the new in-stock filtering feature.

These changes enhance the product listing functionality by allowing users to view only in-stock top-selling products, improving the overall shopping experience.
2026-06-30 00:17:30 +03:30
masoodafar-web d59d829d5f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 17m21s
2026-06-26 19:30:51 +03:30
masoodafar-web 81d139cdab fix(profile): standardize JSRuntime usage in ChargeCreditWallet component
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 27m33s
- Updated references to `jsRuntime` to `JSRuntime` for consistency across the ChargeCreditWallet component.
- Improved the clarity of the code by ensuring uniform naming conventions in the handling of return URLs.

These changes enhance code readability and maintainability in the ChargeCreditWallet functionality.
2026-06-26 18:47:13 +03:30
masoodafar-web d44e99f4ab feat(profile): enhance ChargeCreditWallet with return URL handling and continue shopping button
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 22m27s
- Added support for return URL handling in ChargeCreditWallet to improve user experience after payment.
- Introduced a "Continue Shopping" button that allows users to navigate back to their previous shopping session after successfully charging their wallet.
- Updated the component to manage pending return URLs and ensure they are saved and retrieved correctly.

These changes enhance the functionality of the ChargeCreditWallet page, providing a smoother transition for users post-transaction.
2026-06-26 18:17:19 +03:30
masoodafar-web cd835ae6f9 feat(wallet): add credit wallet charge functionality and update payment callback
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Introduced methods for initiating and verifying credit wallet charges in the WalletService.
- Updated PaymentCallback.razor to handle credit wallet charge verification.
- Added a button for charging the credit wallet in Wallet.razor.
- Updated route constants to include the new credit wallet charge route.

These changes enhance the wallet functionality by allowing users to charge their credit wallets and verify transactions seamlessly.
2026-06-26 16:13:18 +03:30
masoodafar-web 3c216feb32 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 15m3s
2026-06-19 02:38:15 +03:30
masoodafar-web 7f8f99d4eb feat: enhance organization chart data mapping for improved clarity
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 17m19s
Updated the organization chart component to explicitly map network node properties to a flat array with camelCase keys. This change aligns the data structure with the BackOffice admin chart, enhancing consistency and clarity in the displayed information.
2026-06-19 01:56:54 +03:30
masoodafar-web 8766c29a5a fix(k8s): update imagePullPolicy to Always in frontoffice-deployment.yaml
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 20m28s
Changed the imagePullPolicy for the frontoffice container to Always to ensure that the latest image is always pulled from the registry during deployment, improving the consistency of the application updates. Additionally, added a cleanup step in the kub-deploy workflow to remove the latest image from the local cache after deployment.
2026-06-19 00:13:02 +03:30
masoodafar-web 8080b75f05 fix(k8s): change imagePullPolicy to IfNotPresent in frontoffice-deployment.yaml
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 19m56s
Updated the imagePullPolicy for the frontoffice container to IfNotPresent to optimize image pulling behavior, ensuring that images are only pulled when not already present on the node.
2026-06-14 00:08:23 +03:30
masoodafar-web 34c5c508e0 feat: update package version and enhance network membership DTOs
- Updated the version of the Foursat.CMSMicroservice.Protobuf package from 0.0.197 to 0.0.199.
- Added a new property `PackageName` to `NetworkNodeDto` and `FlatNetworkNodeDto` for better representation of membership packages.
- Updated mapping in `NetworkMembershipService` to include the new `PackageName` property.
- Enhanced the org chart display by adding a badge for the package name in the UI.

These changes improve the clarity and functionality of the network membership features.
2026-06-11 23:33:40 +03:30
masoodafar-web 032b8eb448 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m14s
2026-06-08 01:55:28 +03:30
masoodafar-web c2fd8790b6 fix(payment): guard against duplicate package payment verify
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m36s
Gate payment callback with a semaphore and block double-clicks on checkout
to avoid sending multiple verify requests after gateway redirect.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 01:32:47 +03:30
masoodafar-web 088ea8dc7f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m36s
2026-05-29 19:59:19 +03:30
masoodafar-web d8289237e9 fix: update terminology for sales organization and rewards
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m46s
Replaced instances of "شجره‌نامه" with "سازمان فروش" across multiple files to ensure consistent terminology. Additionally, updated references to "پاداش تیمی" to "پاداش های دریافتی" for clarity in user-facing text.

Affected files:
- FAQ.razor.cs
- Index.razor
- Index.razor.cs
- Profile/Index.razor
- Profile/Index.razor.cs
- Profile/MagicWallet.razor
- Profile/Tree.razor
- Profile/Wallet.razor
- Utilities/WalletService.cs
2026-05-29 19:58:48 +03:30
masoodafar-web cd92a2f7b0 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m24s
2026-05-14 20:12:20 +03:30
masoodafar-web 2874b931df feat: hide product prices from unauthenticated (guest) users
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 4m48s
Guests can browse products and product details for presentation purposes,
but prices are hidden behind a 'وارد شوید' prompt (lock icon).
Authenticated users see prices as before.

Affected pages:
- Landing page (Index): top-selling regular + discount sections
- Store/Products list
- Store/ProductDetail (desktop price + mobile sticky bar)
- DiscountStore/Products list
- DiscountStore/ProductDetail (full price box)

Each code-behind now checks AuthService.IsAuthenticatedAsync() on init
and stores the result in _isAuthenticated for use in the template.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 19:58:28 +03:30
masoodafar-web 0c9eb5f9ef Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m25s
2026-05-14 02:07:56 +03:30
masoodafar-web e5b1e11895 fix: update product titles in Index.razor for clarity
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 4m49s
Changed the text for best-selling products in both the main store and credit store sections to improve clarity and consistency. The previous titles "پرخریدترین محصولات فروشگاه" and "پرخریدترین محصولات فروشگاه اعتباری" have been updated to "محصولات پرفروش فروشگاه" and "محصولات پرفروش فروشگاه اعتباری" respectively.
2026-05-14 02:07:46 +03:30
masoodafar-web 94fbf49fb9 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m51s
2026-05-14 01:56:18 +03:30
masoodafar-web 0b1cc1c1c8 fix: add kubectl apply to CI workflows and add production K8s manifest
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 4m19s
Both kub-deploy and prod-deploy were only running kubectl rollout restart,
meaning changes to the deployment manifest (env vars, resources, image tag)
were never applied to the cluster.

- Add scp + kubectl apply before rollout restart in kub-deploy.yml (staging)
- Add scp + kubectl apply before rollout restart in prod-deploy.yml (production)
- Add k8s/production/frontoffice-deployment.yaml with ASPNETCORE_ENVIRONMENT=Production
  and image tag :prod

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 01:44:13 +03:30
masoodafar-web eef13d1471 fix: remove hardcoded GW_URL from Dockerfile and staging manifest
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 4m15s
GW_URL/GwUrl env vars were baked into the Dockerfile (ENV) and the
staging K8s manifest, overriding appsettings.*.json at runtime.
This meant merging to production still used the staging CMS URL.

Now only ASPNETCORE_ENVIRONMENT is set in the manifest; the correct
GwUrl is read from appsettings.Staging.json or appsettings.Production.json
depending on the environment — as intended.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 01:35:18 +03:30
masoodafar-web 4330a5eaa5 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m16s
2026-05-14 01:12:35 +03:30
masoodafar-web 77a835311a fix: move DefaultRequestVersion/Policy to HttpClient (not SocketsHttpHandler)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m57s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 00:47:52 +03:30
masoodafar-web 738119cf6d fix: trust staging-ca in container to resolve PartialChain gRPC error
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m0s
Add staging-ca.crt to the Dockerfile so the ASP.NET runtime inside the
container trusts the internal staging CA. This fixes server-side gRPC
calls from FrontOffice (Blazor Server) to https://cms.se.kbs1.ir which
were failing with PartialChain because the container CA store did not
include the staging root certificate.

Also adds GW_URL env support alongside GwUrl for parity with BackOffice.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 00:42:36 +03:30
masoodafar-web 28a55246df chore: update gateway URL configuration for staging environment
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m25s
- Changed GW_URL and GwUrl in frontoffice-deployment.yaml and Dockerfile to use the internal service URL http://cms-svc:8080 for the staging environment.
- Enhanced ConfigureServices to handle HTTP/1.1 for gRPC when using the internal service URL, ensuring compatibility with Kestrel's requirements.
2026-05-14 00:36:54 +03:30
masoodafar-web ceaf6de226 chore: enhance configuration for gateway URL handling
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m43s
- Added GW_URL and GwUrl environment variables in frontoffice-deployment.yaml and Dockerfile for consistent gateway URL configuration.
- Updated AddGrpcServices method to resolve gateway URL from both environment variables, ensuring proper error handling for missing values.
- Modified TokenNotificationService to prioritize GW_URL over GwUrl for improved configuration flexibility.
2026-05-14 00:27:16 +03:30
masoodafar-web f19711eabc chore: update staging configuration for CMS URL
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m46s
- Changed GwUrl in appsettings.Staging.json from a local HTTP endpoint to a secure HTTPS URL for the CMS service.
2026-05-14 00:00:59 +03:30
masoodafar-web 310400b9e7 refactor: simplify configuration handling for CMS URL
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m24s
- Removed CmsServerBaseUrl utility class and updated references to directly access GwUrl from configuration.
- Adjusted frontoffice-deployment.yaml to remove CmsInternalBaseUrl environment variable.
- Updated appsettings for staging and main environments to reflect the correct CMS service URLs.
- Ensured consistent handling of GwUrl in TokenNotificationService and AddGrpcServices methods.
2026-05-13 23:52:16 +03:30
masoodafar-web 3600fc7709 chore: update Foursat.CMSMicroservice.Protobuf package version to 0.0.197
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m37s
2026-05-13 22:21:13 +03:30
masoodafar-web ae5ab1492e fix: update gRPC service configuration to use CmsServerBaseUrl resolver
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m34s
- Replace direct access to configuration for GwUrl with CmsServerBaseUrl.Resolve in AddGrpcServices and TokenNotificationService.
- Ensure proper error handling for missing GwUrl and add support for optional bypass of public TLS using CmsInternalBaseUrl.
- Adjust channel credentials based on whether the base URL is HTTPS or not.
2026-05-13 22:05:04 +03:30
masoodafar-web 231da2cbaa feat: add top-seller product sections to landing page with guest browsing support
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m10s
- Add two sections to landing page (Index.razor) below the hero:
  - Top 6 best-selling regular products (3-per-row grid)
  - Top 6 best-selling discount store products (3-per-row grid)
  - Each section has a 'More' button linking to /products and /discount-store
- Add GuestActionGate utility service: wraps auth-required actions; shows
  login modal for guests and resumes the original action after successful login
- Register GuestActionGate as scoped service in ConfigureServices
- Add GetTopSellingAsync(count) to ProductService and DiscountProductService
- Add optional sortBy parameter to DiscountProductService.GetProductsAsync
- Hybridize all product pages for guest browsing:
  - Store/Products, Store/ProductDetail: wrap AddToCart with GuestActionGate
  - DiscountStore/Products, DiscountStore/ProductDetail: wrap AddToCart with GuestActionGate
  - Store/Cart, DiscountStore/Cart, Store/CheckoutSummary: soft auth gate on page init
- Fix MembershipPage membership benefit text to be package-agnostic

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 19:48:14 +03:30
masoodafar-web 1e5e6bcf3d Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m50s
2026-05-04 00:04:13 +03:30
masoodafar-web a1d67c5865 update
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m21s
2026-05-04 00:02:49 +03:30
masoodafar-web d751c06a67 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m5s
2026-05-01 00:16:24 +03:30
masoodafar-web b00af9e326 feat: add referral code support to phone verification form
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m9s
2026-04-29 00:46:53 +03:30
masoodafar-web 1c5aa0ce15 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 14m28s
- Proto version: 0.0.192
- Remove local nupkg source from NuGet.config (breaks Docker builds)
2026-03-15 03:26:17 +03:30
masoodafar-web 647e596050 fix: remove local nupkg source from NuGet.config (breaks Docker builds)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 12m52s
2026-03-12 02:28:29 +03:30
masoodafar-web 7c86353a34 ci: retry build - all NuGet packages pushed to Nexus
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 1m22s
2026-03-12 01:14:32 +03:30
masoodafar-web cd0fb2fa57 Revert "fix: bypass broken Nexus, use nuget.org + local proto build"
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 1m23s
This reverts commit 4dc7b52289.
2026-03-12 00:06:33 +03:30
masoodafar-web 4dc7b52289 fix: bypass broken Nexus, use nuget.org + local proto build
- NuGet.config: nuget.org for standard packages, local nupkg for proto
- Dockerfile: COPY nupkg/ before restore
- CI: build proto from CMS source before Docker build
2026-03-12 00:04:04 +03:30
masoodafar-web c793630962 chore: update proto NuGet to 0.0.192
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 1m15s
2026-03-11 23:46:25 +03:30
masoodafar-web caaf28b08e fix: update CMS protobuf package version to 0.0.191 and comment out local package reference
Build and Deploy to Production / build-and-deploy (push) Successful in 6m22s
2026-02-28 05:07:54 +03:30
masoodafar-web 1914f703db Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 2m40s
2026-02-28 04:47:34 +03:30
masoodafar-web ff3696cda3 feat: implement payment verification methods for magic wallet and discount wallet
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m37s
2026-02-28 03:27:58 +03:30
masoodafar-web 2b1dc47dc7 cleanup: remove CallbackUrl from package purchase requests — CMS reads from config now
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m7s
2026-02-27 22:21:02 +03:30
masoodafar-web 2f9ef15859 fix: remove double ×10 conversion in wallet charge — FO should send Toman, not Rial
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m40s
Root cause: FrontOffice was converting Toman→Rial (×10) before sending to CMS,
but CMS ZarinPalService already does Toman→Rial conversion internally.
This caused double ×10 = amounts shown 10x higher in payment gateway.

Fixes:
- MagicWallet.razor.cs: remove ×10 conversion, send _chargeAmount (Toman) directly
- MagicWallet.razor: fix Max field (no /10), fix preset filter (no ×10)
- MagicWallet.razor.cs: fix FormatPrice — values from CMS are Toman, not Rial
- ChargeDiscountWallet.razor.cs: same fix — send Toman directly
- ClubMembershipContractDialog.razor: fix Price display (no /10)
2026-02-27 21:53:01 +03:30
masoodafar-web 5ded91a58a fix: payment success page — show TransactionId, real wallet balance, fix back button
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m53s
- کد ارجاعی: از TransactionId جدول تراکنش‌ها بجای RefId زرین‌پال
- موجودی: از WalletService.GetBalancesAsync() موجودی واقعی کیف پول اصلی
- حذف نمایش موجودی کیف پول اعتباری (نیازی نیست)
- دکمه بازگشت: Href=/profile بجای history.back() (که به درگاه می‌رفت)
2026-02-27 21:31:45 +03:30
masoodafar-web 71b7e40994 fix: revert Price/10 — DB stores Toman, display directly
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m37s
Previous fix incorrectly divided by 10 assuming DB was Rial.
DB values are in Toman, so FormattedPrice shows Price directly.
2026-02-27 21:02:54 +03:30
masoodafar-web 3c1a8ffd53 fix: 4 UI fixes for package display
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m54s
1. Toman/Rial mismatch — Price from server is in Rial, now divides by 10 for correct Toman display
2. ضریب تخفیف → ضریب اعتبار label change in dialog + packages page
3. Back button already functional (ArrowForward in TitleContent) — verified
4. HTML rendering in package description via MarkupString
2026-02-27 20:44:18 +03:30
masoodafar-web a3681a8bcd feat: داشبورد — لیست کاشی پکیج‌ها + مدال انتخاب روش پرداخت
- حذف متن hardcoded 'پکیج پایه ۵۶ میلیون تومان' و دیالوگ inline
- ایجاد PackagePurchaseDialog — کامپوننت مستقل 2 مرحله‌ای:
  مرحله ۱: نمایش کاشی‌های پکیج (ریسپانسیو ۲-۳ تایی)
  مرحله ۲: انتخاب روش پرداخت (درگاه بانکی / دایا الماس)
- دایا الماس فقط برای پکیج پایه + دور اول نمایش داده میشه
- پکیج‌ها داینامیک از API بارگذاری میشن
- تغییر متن CTA از 'شروع فرآیند تامین اعتبار' به 'تهیه پکیج'
- تغییر متن راهنما از 'پکیج پایه را تهیه کنید' به 'یکی از پکیج‌ها را تهیه کنید'
- CSS: hover effects + badge + responsive tiles
2026-02-27 20:14:40 +03:30
masoodafar-web ecc4f447ba F6+F7: مقادیر داینامیک کیف‌پول جادویی، اصلاح rename، افزایش سقف تخفیفی
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m34s
F6: MagicWallet.razor — جایگزینی ×2.5 و سقف‌های ثابت با مقادیر از API
    - WalletService: MagicMultiplier + MagicMaxCredit از gRPC response
    - Index.razor.cs: 'شارژ ×2.5' → 'شارژ چند‌برابری' (تایل داشبورد)
F7: ChargeDiscountWallet.razor — Max از 100M تومان به 1B تومان
اصلاح rename:
    - ConfigureServices: UserWalletChangeLog → UserWalletHistory
    - WalletService: اصلاح تمام رفرنس‌ها
NuGet: v0.0.189
2026-02-27 09:01:12 +03:30
masoodafar-web 474d364a25 feat(Q28): UI Guidance alerts across FrontOffice (G1-G7)
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m53s
Q28: Added informational/warning alerts to 7 FO pages:
G1: Packages — package system explanation
G2: Checkout — package benefits summary
G3: MyPackages — cycle completion guidance
G4: MagicWallet — per-package settings warning
G5: Commission — independent pool explanation
G6: Membership — single-sign contract info
G7: ActivationSection — estimated cost note
2026-02-27 05:40:04 +03:30
masoodafar-web 816dcb70d1 fix: remove hardcoded 56M amounts — dynamic package pricing
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m52s
- ActivationSection: load base package price via PackageService on init
- ClubMembershipContractDialog: dynamic package name/price in contract text
2026-02-27 02:41:24 +03:30
masoodafar-web 3bffc13ffd feat(T4.2+T4.3+F3): conditional payment, re-purchase, PV display
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m33s
T4.2: Conditional Payment in Checkout
- Load package flags via PackageService.GetAllPackagesAsync
- Respect SupportsDirectPurchase/SupportsDayaPurchase flags
- Add Daya loan payment button when supported
- Show alert when no payment method available

T4.3: MyPackages Re-Purchase Logic
- Load MagicWallet status to detect cycle completion
- Show re-purchase CTA when magic cycle is complete
- Show magic wallet progress bar during active cycle
- Display deposit progress, remaining, and credited amounts

F3: PV Display in Order Details
- Add CalculateOrderPVAsync to OrderService
- Show per-product PV and total PV in Store OrderDetail
- Update NuGet to v0.0.188
2026-02-26 22:01:55 +03:30
masoodafar-web a956cb90a1 feat(commission): add per-package filtering to commission pages
- CommissionDtos: add PackageId + PackageTitle to CommissionPayoutDto and WeeklyBalanceDto
- CommissionService: add packageId parameter to GetMyCommissionPayoutsAsync and GetMyWeeklyBalanceAsync
- CommissionDashboardPage: add package filter dropdown + package column in desktop/mobile views
- WeeklyBalancePage: add package filter dropdown + package chip in balance info section
- Update NuGet Foursat.CMSMicroservice.Protobuf to v0.0.187
2026-02-26 21:08:30 +03:30
masoodafar-web 40882c8b80 Phase 8d: NuGet bump 0.0.185 → 0.0.186 (proto cleanup) 2026-02-26 18:00:47 +03:30
masoodafar-web d71d463668 Phase 8c: FO Package pages — Customer RPCs + dynamic features
- NuGet: 0.0.182 → 0.0.185 + local feed source
- PackageDetail: GetPackageAsync → GetCustomerPackageDetailsAsync
  Features & specs now driven by API (PackageFeature) not hardcoded
- Packages.razor: Un-excluded from build, dynamic feature bullets
  from CustomerPackageModel (DiscountMultiplier, MagicWallet, etc.)
- PackageService: Enriched PackageDto with 8 new package fields
  GetUserPackageStatusAsync connected to real RPC (was stub)
2026-02-26 17:52:37 +03:30
masoodafar-web 0bbc11e82c Phase 8a: Wire up Checkout page to Customer* RPCs
- Checkout.razor.cs: Rewrite ProcessPayment() to use
  CustomerPurchasePackageAsync instead of disabled dead code
  (old TransactionsContract + UserOrderContract flow removed)
- Callback URL now points to /profile/payment-callback
  (reuses existing verify page with CustomerVerifyPackagePurchase)
- Remove unused DI: UserOrderContract, TransactionContract
- Remove unused usings: Transactions, UserOrder, WellKnownTypes
- Profile/Index.razor.cs: Rename basePackage → selectedPackage,
  tempCallbackUrl → callbackUrl (cosmetic)
- Build: 0 errors
2026-02-26 03:33:58 +03:30
masoodafar-web 71f391ac80 Phase 7b: Migrate deprecated RPCs to Customer* RPCs
- Profile/Index: InitiateBasePackagePayment → CustomerPurchasePackage
  - Fetches base package via GetCustomerPackages, passes PackageId + PurchaseMethod
- Profile/PaymentCallback: VerifyBasePackagePayment → CustomerVerifyPackagePurchase
  - Uses Authority/Status (from ZarinPal callback) + orderId (from embedded URL param)
  - TransactionId no longer required (CMS finds it internally)
2026-02-26 02:35:59 +03:30
masoodafar-web b82cac4af1 Cleanup: Remove hardcoded 'پکیج طلایی' labels, add PackageTitle to DTO
- MyPackages.razor: dynamic PackageTitle from DTO instead of hardcoded 'پکیج طلایی'
- Packages.razor: generic text for purchase status and info section
- PackageService.cs: add PackageTitle to UserPackageStatusDto record
2026-02-26 02:01:54 +03:30
masoodafar-web a606cd93cd fix: rename wallet labels — اصلی، اعتباری، پاداش تیمی
Build and Deploy to Production / build-and-deploy (push) Successful in 6m19s
- عادی → اصلی (کیف پول اصلی)
- شبکه → پاداش تیمی
- پورسانت → پاداش تیمی
- موجودی کیف پول → موجودی کیف پول اصلی

Files: Wallet.razor, WalletService.cs, MagicWallet.razor,
       Profile/Index.razor, PaymentCallback.razor
2026-02-23 23:26:52 +03:30
masoodafar-web ab8693b381 fix: rename wallet labels — اصلی، اعتباری، پاداش تیمی
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m11s
- عادی → اصلی (کیف پول اصلی)
- شبکه → پاداش تیمی
- پورسانت → پاداش تیمی
- موجودی کیف پول → موجودی کیف پول اصلی

Files: Wallet.razor, WalletService.cs, MagicWallet.razor,
       Profile/Index.razor, PaymentCallback.razor
2026-02-23 23:24:29 +03:30
masoodafar-web 8b78235ef5 chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Production / build-and-deploy (push) Successful in 6m27s
2026-02-23 00:03:00 +03:30
masoodafar-web 86f43d8fdd chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m0s
2026-02-23 00:02:31 +03:30
masoodafar-web f02d082d7b fix: correct GwUrl in Production config to cms.kbs2.ir
Build and Deploy to Production / build-and-deploy (push) Successful in 6m23s
2026-02-22 22:29:17 +03:30
masoodafar-web a11a3765b8 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-22 22:22:43 +03:30
masoodafar-web 2fa9dbc900 feat: enable gateway buttons - remove بزودی disabled state
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m40s
- ChargeDiscountWallet: active payment button with StartDiscountCharge
- MagicWallet: active payment button with StartMagicCharge
- Index: both branch buttons call DirectPayment with loading state
2026-02-22 22:13:23 +03:30
masoodafar-web 8e1f7d7a0c fix: change ریال to تومان in MagicWallet helper text
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m2s
2026-02-22 21:30:26 +03:30
masoodafar-web 93b7375db5 feat: add VAT display to all DiscountStore pages — Products (note), ProductDetail (breakdown), Cart (summary line), Checkout (refactor to VATService), OrderDetail (financial summary)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-02-22 21:26:01 +03:30
masoodafar-web 00da4b98dd refactor: rename 'تخفیف' to 'اعتبار' in DiscountStore pages
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m1s
- Products.razor: X% تخفیف → X% اعتبار
- ProductDetail.razor: تخفیف از کیف اعتباری → اعتبار از کیف اعتباری
- Cart.razor: سقف تخفیف/تخفیف → سقف اعتبار/اعتبار (desktop + mobile)
- Checkout.razor: تخفیف از کیف اعتباری → اعتبار، مبلغ پس از تخفیف → مبلغ پس از اعتبار
- OrderDetail.razor: سقف تخفیف/تخفیف → سقف اعتبار/اعتبار
2026-02-22 21:03:10 +03:30
masoodafar-web 7371a263e8 refactor: rename 'تخفیفی' to 'اعتباری' in all user-facing UI
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- ChargeDiscountWallet.razor: فروشگاه تخفیفی → فروشگاه اعتباری
- PaymentCallback.razor: اعتبار تخفیف → کیف پول اعتباری
- Profile/Index.razor: تخفیف باشگاه → کیف اعتباری
2026-02-22 20:56:38 +03:30
masoodafar-web 214fe9efbd refactor: update CMS protobuf package to version 0.0.182
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-02-22 20:48:08 +03:30
masoodafar-web 8cef475877 temp: disable gateway payment buttons - show 'بزودی' until gateway is activated
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- MagicWallet.razor: disable charge button
- ChargeDiscountWallet.razor: disable charge button
- Profile/Index.razor: disable IPG package purchase button for all cycles
2026-02-22 20:44:22 +03:30
masoodafar-web 28695a88f8 feat: ChargeDiscountWallet UI - page, route, client method & wallet link
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Add ChargeDiscountWallet.razor page with charge form and preset amounts
- Add ChargeDiscountWallet.razor.cs code-behind with gateway redirect logic
- Add InitiateDiscountChargeAsync client method in WalletService
- Add ChargeDiscountWallet route constant
- Add charge button link on main Wallet page
2026-02-22 20:22:52 +03:30
masoodafar-web a2e1e1fead feat: Magic Wallet Phase 6 UI + purchase cycle controls
- FrontOffice.Main.csproj: switch to ProjectReference for CMS Protobuf
- MagicWallet.razor.cs: update default record with PurchaseCycleCount
- WalletService.cs: add PurchaseCycleCount to MagicWalletStatus record
- Profile/Index.razor.cs: load _purchaseCycleCount from MagicWalletStatus
- Profile/Index.razor: conditional purchase dialog (IPG vs Daya by cycle)
- Licenses.razor: fix grid column size xs=6
2026-02-22 20:04:44 +03:30
admin 3cf5ad429d Update src/FrontOffice.Main/Shared/Footer.razor
Build and Deploy to Production / build-and-deploy (push) Successful in 6m39s
2026-02-22 10:25:35 +00:00
masoodafar-web 710ddbe261 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m2s
2026-02-21 20:38:23 +03:30
masoodafar-web be2633ddaf refactor: update CMS protobuf package to version 0.0.181
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m28s
2026-02-21 20:20:47 +03:30
masoodafar-web 104e4df1a6 feat: implement Magic Wallet feature with charging and status display
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 6m25s
2026-02-21 19:50:43 +03:30
masoodafar-web c08d776ecc docs: move docs to totalDoc, replace with placeholders
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m42s
2026-02-18 21:44:32 +03:30
masoodafar-web e2ca65ed98 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m42s
2026-02-18 01:19:34 +03:30
masoodafar-web bed1ad7c66 feat: lazy load products in both stores (infinite scroll)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- ProductService: add GetProductsPagedAsync with PaginationState support
- Store/Products: replace load-all with page-by-page lazy loading (12 per page)
- DiscountStore/Products: replace pagination with lazy load (load more button)
- Both stores show 'load more' button + loading indicator + 'all shown' text
2026-02-18 01:15:48 +03:30
masoodafar-web ae9b448a64 merge: kub-stage → production (1:1 square images + site pages + landing animation fix)
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-18 01:01:44 +03:30
masoodafar-web 22203fd8f4 feat: make all product images 1:1 square ratio across both stores
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m18s
- Store/Products.razor: aspect-ratio:1/1 instead of height:60%
- DiscountStore/Products.razor: same square ratio
- Store/ProductDetail.razor: aspect-ratio:1/1 instead of max-height:400px
- DiscountStore/ProductDetail.razor: ObjectFit.Cover + aspect-ratio:1/1
- Store/Cart.razor: rounded-lg instead of rounded-circle (desktop+mobile)
- Store/CheckoutSummary.razor: rounded-lg + ObjectFit.Cover
- Store/OrderDetail.razor: rounded-lg + ObjectFit.Cover (desktop+mobile)
- DiscountStore/Cart.razor: rounded-lg + ObjectFit.Cover (desktop+mobile)
- DiscountStore/Checkout.razor: rounded-lg + ObjectFit.Cover
2026-02-18 01:00:42 +03:30
masoodafar-web bf0eb089bb fix: re-init scroll animations after data loads from DB
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m55s
- initScrollAnimations ran only on firstRender when data wasn't loaded yet
- Items with fade-in-up class stayed invisible (opacity:0) after data render
- Now tracks _dataLoaded/_animationsInitialized flags to re-init JS after data loads
- Also fixes stat counter animations not running (stats were empty on firstRender)
2026-02-17 23:57:41 +03:30
masoodafar-web be78bc683a refactor: migrate Landing page to SitePageSettings system
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m33s
- Index.razor.cs: load landing SettingsJson via SitePageSettingsService
- PopulateFromSettings(): parse trustBadges, steps, features, stats, testimonials, faqs from DB
- ResolveIcon(): dynamically resolve MudBlazor icons from icon name strings
- Index.razor: make hero text, section titles, CTA dynamic with fallback defaults
- All hardcoded data preserved as fallbacks when DB is unavailable
2026-02-17 23:17:09 +03:30
masoodafar-web 997ab80fa4 refactor: migrate About & Contact pages to SitePageSettings system
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m58s
- About.razor: use SitePageSettingsService + GetSettings<AboutSettings>()
- About.razor.cs: read values/team from ImageGroups instead of SitePage sections
- Contact.razor: use SitePageSettingsService + GetSettings<ContactSettings>()
- Contact.razor.cs: read address/phone/email/social from SettingsJson
2026-02-17 23:02:58 +03:30
masoodafar-web bba9c1d1d8 feat: Add Licenses page + SitePageSettingsService
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m18s
- SitePageSettingsService: new gRPC client for simplified page settings
- Licenses.razor: new page showing license certificates from CMS
- RouteConstants: added Licenses route
- Footer + MainLayout: added Licenses navigation link
- Proto NuGet updated to 0.0.180
2026-02-17 22:35:20 +03:30
Masoud 183ce0a2e5 fix: update references from "تخفیفی" to "اعتباری" across multiple files
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m55s
2026-02-17 15:28:52 +03:30
Masoud 1ea928118d fix: update references from تخفیفی to اعتباری across multiple files
Build and Deploy to Production / build-and-deploy (push) Successful in 5m46s
2026-02-17 15:26:09 +03:30
Masoud 8f2d739d1f fix: update references from تخفیفی to اعتباری across multiple files
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-17 15:24:13 +03:30
Masoud f070a0b294 fix: update references from "فروشگاه تخفیفی" to "فروشگاه اعتباری" across multiple files
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m38s
2026-02-17 15:22:19 +03:30
masoodafar-web 585109ab86 chore: clean up commented-out GwUrl in appsettings
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m0s
2026-02-17 02:56:01 +03:30
masoodafar-web 8e89d5e064 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m43s
2026-02-17 01:59:40 +03:30
masoodafar-web c094c10da2 fix: add appsettings.Production.json — GwUrl=cms.kbs1.ir
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m31s
2026-02-17 01:58:17 +03:30
masoodafar-web 44ef166109 fix: update appsettings.Development.json with necessary configuration settings
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m53s
2026-02-17 01:46:10 +03:30
masoodafar-web aca6101e47 docs: اضافه شدن نسخه ۲.۷.۰ به CHANGELOG — تخفیف اجباری, VAT, PaymentStatus, DeliveryStatus mapping 2026-02-17 01:44:19 +03:30
masoodafar-web 8bd961ade1 Fix production workflow: use SERVER_PASSWORD, add docker login, use set image for deploy
Build and Deploy to Production / build-and-deploy (push) Successful in 5m39s
2026-02-17 01:24:06 +03:30
masoodafar-web 750d4c6b97 Remove staging workflow files from production branch to prevent cross-deployment 2026-02-17 01:15:59 +03:30
masoodafar-web 276a4c20f2 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m12s
Features merged:
- Discount store UI improvements (100% auto-discount, VAT breakdown)
- PaymentStatus display on orders (Pending/Success/Failed/Refunded)
- Failed payment UX with re-order link
- Product discount badges show actual percent
- Various UI and bug fixes

Production workflow preserved.
2026-02-17 00:55:52 +03:30
masoodafar-web 81f32e6ec1 feat: show PaymentStatus on discount orders + failed payment UX
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m25s
- Updated NuGet to Protobuf v0.0.179
- Added PaymentStatus to DiscountOrderSummary and DiscountOrderDetail records
- OrderDetail: show payment status chip (Pending/Success/Failed/Refunded)
- OrderDetail: show alert with re-order link for failed payments
- Orders: updated desktop table and mobile cards with PaymentStatus chips
- Added GetPaymentStatusText() and GetPaymentStatusColor() helpers
2026-02-16 22:38:40 +03:30
masoodafar-web 0df5d6a595 feat: show VAT breakdown on discount checkout — discount, net, 9% VAT, final amount
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m56s
2026-02-16 22:22:07 +03:30
masoodafar-web f60b007fdd fix: show actual product discount percent instead of misleading '100% تخفیفی'
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m3s
2026-02-16 22:05:52 +03:30
masoodafar-web 01b9f1eb98 fix: remove discount balance UI entirely — 100% discount auto-applied by server
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-02-16 21:57:18 +03:30
masoodafar-web 1dc60faf4a feat: force 100% discount — remove slider, auto-apply max discount from wallet
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Remove MudSlider and MudNumericField from Checkout
- Auto-apply MaxUsable (full discount balance)
- Show fixed '100% تخفیفی' badge on Products, Cart, ProductDetail, Checkout
- GatewayAmount = TotalPrice - MaxUsable (no user choice)
2026-02-16 21:52:21 +03:30
masoodafar-web fa9205bd8a docs: add v2.6.0 — ZarinPal, PaymentTransaction, Docker build fix
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m27s
2026-02-16 02:23:54 +03:30
masoodafar-web b102b89bfb fix: switch to NuGet package reference for CMS Protobuf (Docker build fix)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m36s
- Changed from ProjectReference to PackageReference (v0.0.178)
- Docker build context only includes src/, cannot resolve ../../../CMS path
- Local dev and Docker both now use NuGet package from Nexus
2026-02-16 01:39:06 +03:30
masoodafar-web 6db83ccd90 feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s
- Discount store pages (products, cart, orders, order detail)
- Blog pages and services
- Payment gateway callback page
- AppImage component, EmptyState, LoadingState, PageHeader
- DiscountCartService, DiscountOrderService, DiscountProductService
- BlogCategoryService, BlogPostService, SitePageService, ImageCacheService
- PhoneVerifyForm component
- Profile Hub page
- UI/UX improvements across all pages
- landing.js for homepage
- Config and routing updates
2026-02-16 00:51:39 +03:30
masoodafar-web f0d1156457 fix: update Docker daemon startup options and improve readiness checks in deployment workflows
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m40s
2026-02-11 00:41:38 +03:30
masoodafar-web 83d0ca1e0c fix: update Gateway URL and SignalR HubPath in staging configuration
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m9s
2026-02-10 22:06:13 +03:30
masoodafar-web 081b9d3c10 refactor: update user address models and methods to use CustomerAddressModel across multiple components
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m8s
2026-02-08 00:25:12 +03:30
masoodafar-web eab978188a Refactor: Update Protobuf references to CMSMicroservice across multiple components
- Changed Protobuf imports from FrontOffice.BFF to CMSMicroservice in Profile components (EditAddressDialog, Index, PaymentCallback, Personal, Settings).
- Updated CheckoutSummary, OrderDetail, OrderTracking, and Orders pages to use new UserOrder Protobuf definitions.
- Refactored WalletService, PackageService, and other utility services to align with new Protobuf structure.
- Adjusted appsettings.json for local development URL.
- Removed unused validators and simplified validation logic in AuthDialog.
- Enhanced ClubMembershipContractDialog to utilize new OtpToken service for OTP handling.
2026-02-03 00:02:16 +03:30
masoodafar-web 98a68074bc chore: Update production workflow to use Nexus registry (no proxy needed)
Build and Deploy to Production / build-and-deploy (push) Failing after 0s
- Changed Dockerfile base images to use 194.5.195.53:32082 (Nexus)
- Updated workflow to use docker-sshpass image from Nexus
- Removed HTTP_PROXY/HTTPS_PROXY dependencies
- Changed deploy method to SSH instead of kubeconfig
- Registry: 194.5.195.53:30080 (Gitea internal)
- K8S Server: 45.149.79.127 (Production)
2026-01-29 22:04:31 +03:30
masoodafar-web 26ce7fd2b6 fix: increase Docker daemon startup timeout to 3 minutes
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 0s
2026-01-29 17:13:49 +03:30
masoodafar-web 36838ac3ef trigger: test offline CI/CD pipeline
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 07:03:32 +03:30
masoodafar-web 88a86ecf69 refactor: use docker-sshpass image (fully offline)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 06:49:42 +03:30
masoodafar-web 95f8cb9f73 refactor: simplified pipeline with offline Nexus + auto deploy
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 06:12:22 +03:30
masoodafar-web eda53f9498 Revert: use only Nexus (no fallback) 2026-01-29 03:44:43 +03:30
masoodafar-web 3e5fafaf2d Add nuget.org as fallback source 2026-01-29 03:41:21 +03:30
masoodafar-web fb20fc59ef Remove afrino NuGet sources - use only Nexus 2026-01-29 03:14:42 +03:30
masoodafar-web 2273fa6a35 chore: تنظیم Nexus برای NuGet و Docker offline
- Dockerfile: استفاده از Nexus Docker (194.5.195.53:32082)
- NuGet.config: استفاده از Nexus NuGet با HTTP و allowInsecureConnections
- workflow: حذف kubectl dependency، اضافه کردن 32082 به insecure-registries
2026-01-29 02:56:32 +03:30
masoodafar-web 522d052745 Update deployment configurations and Dockerfile to use internal registry 2026-01-29 02:54:07 +03:30
masoodafar-web a543e43d0a update
Build and Deploy to Production / build-and-deploy (push) Failing after 39s
2026-01-13 23:46:09 +03:30
masoodafar-web 6de06fc8ce update
Build and Deploy to Production / build-and-deploy (push) Failing after 1s
2026-01-13 23:45:20 +03:30
masoodafar-web 03f9e383e4 update
Build and Deploy to Production / build-and-deploy (push) Failing after 2s
2026-01-13 23:44:29 +03:30
masoodafar-web d2e015d07c update
Build and Deploy to Production / build-and-deploy (push) Failing after 39s
2026-01-12 16:11:25 +03:30
masoodafar-web f2cc32a9c6 update
Build and Deploy to Production / build-and-deploy (push) Failing after 37s
2026-01-12 16:08:56 +03:30
masoodafar-web 04ce4f1a31 update
Build and Deploy to Production / build-and-deploy (push) Failing after 2m2s
2026-01-12 16:05:56 +03:30
masoodafar-web c6797e4b84 update2
Build and Deploy to Production / build-and-deploy (push) Failing after 2m2s
2026-01-10 18:23:46 +03:30
masoodafar-web 06c222308f update
Build and Deploy to Production / build-and-deploy (push) Failing after 2m1s
2026-01-10 18:09:31 +03:30
Admin 9ae9d53f4a fix: Remove domain from insecure-registries, use IP only
Build and Deploy / build (push) Failing after 1m47s
2026-01-03 15:15:34 +00:00
Admin a6e0777974 fix: Update registry to IP address for CI/CD
Build and Deploy / build (push) Failing after 3m5s
2026-01-03 15:03:28 +00:00
masoodafar-web 9e92ab951a fix: update gateway URL to new domain in configuration files
Build and Deploy / build (push) Failing after 1m42s
2026-01-03 17:30:37 +03:30
masoodafar-web 641f3651d7 feat: add release notes for version 1.5.0; include new features, bug fixes, and UI improvements
Build and Deploy / build (push) Successful in 1m29s
2025-12-29 01:03:12 +03:30
masoodafar-web 72fa159840 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 2m1s
2025-12-29 00:41:48 +03:30
masoodafar-web 29ce8cad38 Refactor commission-related pages and terminology to enhance clarity and consistency
Build and Deploy / build (push) Successful in 1m22s
- Updated CommissionDashboardPage to reflect new terminology for rewards instead of commissions.
- Removed CommissionHistoryPage as its functionality has been integrated into the dashboard.
- Adjusted WeeklyBalancePage to align with new reward terminology and improved layout.
- Modified Index, NetworkStatisticsPage, MyPackages, Packages, Profile, and Wallet pages to use consistent terms related to team and rewards.
- Updated ClubMembershipContractDialog to clarify user invitation terms.
- Enhanced footer description to reflect the focus on sales teams.
2025-12-29 00:35:12 +03:30
masoodafar-web 8588db949a fix: ensure state update before loading weekly balance in WeeklyBalancePage
Build and Deploy / build (push) Successful in 1m22s
2025-12-28 23:59:07 +03:30
masoodafar-web 368d429897 feat: update commission protobuf version; enhance weekly balance display with new member counts and carryover logic
Build and Deploy / build (push) Successful in 1m26s
2025-12-28 23:53:45 +03:30
masoodafar-web eb76791dc0 feat: add production configuration settings; include URLs, encryption settings, and logging levels
Build and Deploy to Production / build-and-deploy (push) Successful in 2m0s
2025-12-27 07:45:16 +03:30
masoodafar-web 2a8c24ef42 feat: update query parameter handling for week selection; improve fallback logic for week definition creation
Build and Deploy / build (push) Successful in 1m21s
2025-12-27 07:41:47 +03:30
masoodafar-web 3fdaa2c22a feat: update commission status handling and UI; enhance status representation with computed properties and improve filtering logic
Build and Deploy / build (push) Successful in 2m20s
2025-12-27 07:31:31 +03:30
masoodafar-web e3521af071 feat: enhance WithdrawalRequests page with payout selection and improved submission logic; add sorting options for products
Build and Deploy / build (push) Successful in 1m18s
2025-12-27 06:22:41 +03:30
masoodafar-web 43cdf7c897 feat: enhance ReleaseNotesDialog with improved UI elements and animations; update button labels and alert messages
Build and Deploy / build (push) Successful in 1m26s
2025-12-27 05:06:03 +03:30
masoodafar-web 9cd275a777 feat: implement version check and update dialog; add browser cache clearing functionality
Build and Deploy / build (push) Successful in 1m40s
2025-12-27 04:47:27 +03:30
masoodafar-web 51129704a0 feat: add staging configuration with gateway and logging settings
Build and Deploy / build (push) Successful in 1m25s
2025-12-27 02:56:24 +03:30
masoodafar-web 00f805dcd8 fix: update gateway URL to new front office endpoint
Build and Deploy / build (push) Successful in 2m16s
2025-12-27 01:13:21 +03:30
masoodafar-web 1e086c389a fix: update gateway URL to new production endpoint
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-26 10:06:39 +03:30
masoodafar-web c136e853c6 fix: update gateway URL to new production endpoint
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-26 10:06:07 +03:30
masoodafar-web 54f8781774 feat: enhance NetworkMembershipService with updated member properties and add referral code functionality; improve org chart styling and debug logging
Build and Deploy to Production / build-and-deploy (push) Successful in 2m3s
2025-12-26 05:52:17 +03:30
masoodafar-web 1c61fe6846 feat: update NetworkMembership Protobuf version and add referral code functionality with UI enhancements
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-26 05:51:24 +03:30
masoodafar-web 516da671c6 feat(AppVersion): Add AppVersionService for cache invalidation on version update
Build and Deploy to Production / build-and-deploy (push) Successful in 1m37s
- Add AppVersionService to check version and clear cache if needed
- Update Configuration.Protobuf package to 0.0.4 with AppVersion proto
- Integrate version check in App.razor on app initialization
2025-12-25 18:45:01 +03:30
masoodafar-web 45a291b529 fix: update fallback behavior to return empty responses when backend is unavailable 2025-12-25 17:57:39 +03:30
masoodafar-web cb6102191e fix: update gateway URL to secure production endpoint
Build and Deploy to Production / build-and-deploy (push) Successful in 2m21s
2025-12-25 17:35:32 +03:30
admin 7d491059f8 fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Successful in 1m29s
2025-12-23 22:34:57 +00:00
admin 6b41ae8b5e fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-23 22:34:48 +00:00
admin 8be14089c6 feat: add production deployment workflow
Build and Deploy to Production / build-and-deploy (push) Failing after 11s
2025-12-23 22:28:27 +00:00
admin fa5987bf3e fix: update appsettings for production K8s 2025-12-23 22:18:38 +00:00
admin ac5818855c fix: update appsettings for production K8s 2025-12-23 22:18:18 +00:00
masoud 8f58f01781 Update src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs 2025-12-23 04:12:18 -08:00
masoodafar-web cca89e4100 feat: update membership and network terminology to customer club language 2025-12-21 16:51:39 +03:30
masoodafar-web d7abcf4278 feat: update membership and network terminology to customer club language 2025-12-21 16:00:49 +03:30
masoodafar-web 10331663ce feat: disable payment button and add coming soon label 2025-12-20 05:57:53 +03:30
masoodafar-web 34f5cbd5a8 fix: update gateway URL to production environment 2025-12-20 05:23:14 +03:30
masoodafar-web 8ae25d2b38 feat: update deployment branch to stage-new in workflow configuration 2025-12-20 05:05:41 +03:30
masoodafar-web 025e8d3c3e feat: add withdrawal requests feature and update VAT loading mechanism
Build and Deploy / build (push) Successful in 1m25s
2025-12-20 04:03:11 +03:30
masoodafar-web 9ee464b8b4 feat: implement VAT management service and integrate VAT calculations across components 2025-12-19 00:30:33 +03:30
masoodafar-web 6b457d0ce6 feat: integrate d3-org-chart for network visualization and add SignalR token notification service 2025-12-18 03:19:06 +03:30
masoodafar-web 27c2c0259b feat: add club membership contract dialog and city autocomplete 2025-12-17 22:46:07 +03:30
masoodafar-web 3af564be73 feat: add payment callback and referral link features
Build and Deploy / build (push) Failing after 1m21s
2025-12-16 02:33:56 +03:30
masoud 3c528aba48 fix: Revert to aspnet runtime - FrontOffice is Blazor Server not WASM
Build and Deploy / build (push) Successful in 1m42s
- Project uses Microsoft.NET.Sdk.Web (Blazor Server)
- Not Microsoft.NET.Sdk.BlazorWebAssembly
- Needs aspnet runtime to run the .dll, not nginx for static files
- Changed back from nginx:alpine to aspnet:9.0
2025-12-07 23:35:09 +00:00
masoud 380b4788dd fix: Remove default nginx files before copying Blazor WASM output
Build and Deploy / build (push) Successful in 1m51s
- nginx:alpine has default index.html that prevents Blazor from loading
- Added RUN rm -rf /usr/share/nginx/html/* before COPY
2025-12-07 23:30:17 +00:00
masoud 0395c74041 fix: Change FrontOffice Dockerfile to nginx runtime for Blazor WASM
Build and Deploy / build (push) Successful in 1m44s
- Changed from aspnet to nginx:alpine
- Copy wwwroot output instead of FrontOffice.dll
- Configure nginx SPA routing (try_files for client-side routing)
- Remove ENTRYPOINT dotnet command
- Fix project name references (FrontOffice.Main.csproj)
2025-12-07 22:48:28 +00:00
masoud 0f20d216b6 remove: Delete incorrect Dockerfile from root
Build and Deploy / build (push) Failing after 16s
Workflow uses src/FrontOffice.Main/Dockerfile which is correct (nginx).
Root Dockerfile was using wrong runtime (aspnet instead of nginx).
2025-12-07 22:44:46 +00:00
admin baf984a942 Update src/FrontOffice.Main/Dockerfile
Build and Deploy / build (push) Failing after 14s
2025-12-07 22:34:16 +00:00
admin 2b07cc2270 Update src/FrontOffice.Main/Dockerfile
Build and Deploy / build (push) Failing after 14s
2025-12-07 22:29:24 +00:00
masoodafar-web b51a4b51cc refactor: migrate from nginx to aspnet runtime
Build and Deploy / build (push) Failing after 13s
2025-12-08 01:55:13 +03:30
masoud 410f31671d fix: update kubeconfig for deployment
Build and Deploy / build (push) Successful in 4m39s
2025-12-07 19:10:58 +00:00
masoud 1f9580cd6e test: trigger workflow to debug failure
Build and Deploy / build (push) Successful in 1m40s
2025-12-07 19:09:00 +00:00
masoud 39c1c979db chore: test multi-remote push to git.afrino.co
Build and Deploy / build (push) Failing after 1m30s
2025-12-07 18:52:37 +00:00
masoud 8eac688ad9 chore: trigger rebuild with new registry domain
Build and Deploy / build (push) Successful in 1m27s
2025-12-07 17:32:24 +00:00
masoud 07be693b92 chore: migrate from IP to domain (git.foursat.afrino.co)
Build and Deploy / build (push) Has been cancelled
2025-12-07 17:01:57 +00:00
masoud 65a64bf63b Update GwUrl to HTTPS domain with Let's Encrypt
Build and Deploy / build (push) Has been cancelled
2025-12-07 15:05:21 +00:00
masoud 1d8aec3b7e Update GwUrl to external NodePort endpoint 2025-12-07 14:49:56 +00:00
masoud 08595e4f95 Revert: Back to nginx for Blazor WASM 2025-12-06 23:11:07 +00:00
masoud abcc079fdd Fix: Revert to aspnet runtime for Blazor Server with gRPC 2025-12-06 23:08:54 +00:00
masoodafar-web 2a2efd0376 feat: Add proxy configuration for Docker daemon and build process 2025-12-07 01:15:25 +03:30
masoud e3841f07cc Fix: Correct Dockerfile path to FrontOffice.Main/Dockerfile 2025-12-06 21:42:24 +00:00
masoud de5c33196b Fix: Use nginx for Blazor WASM instead of aspnet runtime 2025-12-06 21:24:28 +00:00
masoodafar-web 559fbbf6cd feat: Update Kubernetes deployment with proxy and registry config 2025-12-07 00:49:01 +03:30
masoodafar-web 8a7f68a167 Merge branch 'main' into kub-stage
# Conflicts:
#	.gitea/workflows/kub-deploy.yml
2025-12-07 00:25:09 +03:30
masoodafar-web ebae8f3e16 Add Docker deployment configuration and update package references 2025-12-07 00:05:55 +03:30
masoud fba5cdbabe Add Kubernetes CI/CD pipeline for kub-stage 2025-12-05 16:25:14 +00:00
189 changed files with 17714 additions and 3208 deletions
+2
View File
@@ -0,0 +1,2 @@
# Keep production config from current branch during merges — never overwrite
src/FrontOffice.Main/appsettings.Production.json merge=ours
-46
View File
@@ -1,46 +0,0 @@
name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage
jobs:
Deploy:
runs-on: windows
steps:
- name: Checkout
uses: https://git.afrino.co/actions/checkout@v3
- name: Setup dotnet
uses: https://git.afrino.co/actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Remove Package Source
run: dotnet nuget remove source FourSat
continue-on-error: true
- name: Add Package Source
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
- name: Install dependencies
run: dotnet restore ".\src\FrontOffice.Main\FrontOffice.Main.csproj"
- name: Build
run: dotnet build ".\src\FrontOffice.Main\FrontOffice.Main.csproj" --configuration Release --no-restore
- name: Test
run: dotnet test ".\src\FrontOffice.Main\FrontOffice.Main.csproj" --no-restore --verbosity normal
- name: Recycle Apppool
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:kbs1.ir
shell: powershell
- name: Stop Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:kbs1.ir
shell: powershell
- name: Publish
run: dotnet publish ".\src\FrontOffice.Main\FrontOffice.Main.csproj" -c Release -o publish
- name: Copy Publish To IIS Directory
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\kbs1.ir\" -Recurse -Force
- name: Start Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:kbs1.ir
shell: powershell
+82
View File
@@ -0,0 +1,82 @@
name: Build and Deploy to Kubernetes
on:
push:
branches:
- kub-stage
env:
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 194.5.195.53
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
# Wait up to 3 minutes for Docker to be ready
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
done
# Final check
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/FrontOffice.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f FrontOffice.Main/Dockerfile .
- name: Push to Registry
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to Kubernetes
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
kubectl rollout restart deployment/frontoffice &&
kubectl rollout status deployment/frontoffice --timeout=180s &&
rm -f /tmp/frontoffice-deployment.yaml
"
echo "✅ Deployed!"
+83
View File
@@ -0,0 +1,83 @@
name: Build and Deploy to Production
on:
push:
branches:
- production
env:
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 45.149.79.127
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
done
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/FrontOffice.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -f FrontOffice.Main/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
.
- name: Push to Registry
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
- name: Deploy to Production
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/production/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
kubectl rollout restart deployment/frontoffice &&
kubectl rollout status deployment/frontoffice --timeout=300s &&
rm -f /tmp/frontoffice-deployment.yaml
"
echo "✅ Deployed to Production!"
+1 -1
View File
@@ -14,7 +14,7 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
# Mono auto generated files
mono_crash.*
# Build results
+4
View File
@@ -0,0 +1,4 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
+263
View File
@@ -0,0 +1,263 @@
# سرویس‌های FrontOffice (سمت مشتری)
این سند تمام سرویس‌هایی را که **FrontOffice** (فرانت مشتری) به **CMS** از طریق gRPC Gateway (`GW_URL`) فراخوانی می‌کند، به ترتیب **Use Case** فهرست می‌کند.
- **مسیر کد سرویس‌ها:** `FrontOffice/src/FrontOffice.Main/Utilities/`
- **ثبت gRPC:** `FrontOffice/src/FrontOffice.Main/ConfigureServices.cs`
- **تاریخ:** ۱۴۰۵/۰۳/۱۷
---
## ۱. احراز هویت و ثبت‌نام
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 1.1 | `AuthService``RefreshTokenAsync` | `UserContract.RefreshToken` | `/profile` (`Profile/Index`) | تمدید JWT پس از بازگشت از درگاه پرداخت |
| 1.2 | `AuthService``InitUserAuthInfo` / claims | — (local JWT parse) | سراسری | خواندن UserId، نام، کدملی، وضعیت قرارداد/پکیج/باشگاه از توکن |
| 1.3 | `UserContract.CreateNewOtpToken` | `UserContract` | `Shared/AuthDialog` | ارسال OTP ورود/ثبت‌نام با موبایل |
| 1.4 | `UserContract.VerifyOtpToken` | `UserContract` | `Shared/AuthDialog` | تأیید OTP و دریافت JWT |
| 1.5 | `UserContract.CreateNewOtpToken` (Purpose=`signContract`) | `UserContract` | `/register` (`RegisterWizard`) | ارسال OTP برای امضای قرارداد اصلی |
| 1.6 | `UserContract.AcceptContract` | `UserContract` | `/register` (`RegisterWizard`) | ثبت امضای قرارداد اصلی + دریافت توکن به‌روز |
| 1.7 | `UserContract.GetUserForCustomer` | `UserContract` | `/register`, `/profile`, `/profile/personal`, `/profile/settings` | بارگذاری پروفایل موجود کاربر |
| 1.8 | `UserContract.UpdateCustomerProfile` | `UserContract` | `/register`, `/profile`, `/profile/personal` | ذخیره نام، نام‌خانوادگی، کدملی و… |
| 1.9 | `AuthDialogService.ShowAuthDialogAsync` | — (UI) | فروشگاه، سبد، checkout | نمایش دیالوگ ورود برای مهمان |
| 1.10 | `GuestActionGate` | — (wrapper روی Auth) | `/`, `/products`, `/product/{id}`, Discount Store | جلوگیری از افزودن به سبد بدون لاگین |
---
## ۲. پروفایل و آدرس
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 2.1 | `UserContract.GetUserForCustomer` | `UserContract` | `/profile`, `/profile/personal`, `/profile/settings` | نمایش و ویرایش اطلاعات شخصی |
| 2.2 | `UserContract.GetUser` | `UserContract` | `/profile/hub`, `Club/ActivationSection` | اطلاعات کامل کاربر (هاب پروفایل / پس از فعال‌سازی باشگاه) |
| 2.3 | `UserContract.UpdateUser` | `UserContract` | `/profile/settings` | به‌روزرسانی تنظیمات حساب |
| 2.4 | `UserAddressContract.GetCustomerAddresses` | `UserAddressContract` | `/profile`, `/profile/addresses`, `/checkout`, Store/Discount checkout | لیست آدرس‌های تحویل |
| 2.5 | `UserAddressContract.CreateCustomerAddress` | `UserAddressContract` | `Profile/Components/AddAddressDialog` | ثبت آدرس جدید |
| 2.6 | `UserAddressContract.UpdateCustomerAddress` | `UserAddressContract` | `Profile/Components/EditAddressDialog` | ویرایش آدرس |
| 2.7 | `UserAddressContract.DeleteCustomerAddress` / `DeleteUserAddress` | `UserAddressContract` | `/profile/addresses`, `/profile` | حذف آدرس |
| 2.8 | `UserAddressContract.SetCustomerDefaultAddress` / `SetAddressAsDefault` | `UserAddressContract` | `/profile/addresses`, `/profile`, `/checkout` | تنظیم آدرس پیش‌فرض |
| 2.9 | `CityContract.GetAllCitiesByFilter` | `CityContract` | `AddAddressDialog`, `EditAddressDialog` | جستجو و انتخاب شهر/استان |
---
## ۳. پکیج و خرید عضویت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 3.1 | `PackageService.GetAllPackagesAsync` | `PackageContract.GetCustomerPackages` | `/packages`, `/my-packages`, `/`, `/checkout`, `PackagePurchaseDialog`, `ClubMembershipContractDialog`, Commission pages | لیست پکیج‌های قابل خرید |
| 3.2 | `PackageService.GetPackageByIdAsync` | `PackageContract.GetCustomerPackageDetails` | (از طریق wrapper؛ صفحه جزئیات مستقیم RPC دارد) | جزئیات یک پکیج |
| 3.3 | `PackageContract.GetCustomerPackageDetails` | `PackageContract` | `/package/{id}` (`PackageDetail`) | جزئیات، ویژگی‌ها و مشخصات پکیج |
| 3.4 | `PackageService.GetUserPackageStatusAsync` | `PackageContract.GetUserPackageStatus` | `/packages`, `/my-packages`, `/profile` | وضعیت خرید پکیج، باشگاه، موجودی کیف پول |
| 3.5 | `PackageContract.CustomerPurchasePackage` | `PackageContract` | `/profile` (`ProcessDirectPayment`), `/checkout` | شروع پرداخت درگاهی (Zarinpal) برای خرید پکیج |
| 3.6 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback?type=package` | تأیید پرداخت پکیج پس از بازگشت از درگاه |
| 3.7 | `PackagePurchaseDialog` | — (UI + `PackageService`) | `/profile`, `/my-packages` | انتخاب پکیج و روش پرداخت (درگاه / Daya) |
| 3.8 | Daya Loan (external URL) | — | `/profile` | هدایت به `dayadiamond.ir` برای خرید اقساطی (بدون RPC) |
---
## ۴. کیف پول، شارژ و برداشت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 4.1 | `WalletService.GetBalancesAsync` | `UserWalletContract.GetCustomerWallet` | `/profile`, `/profile/wallet`, Store checkout, PaymentCallback | موجودی اصلی، تخفیفی و شبکه |
| 4.2 | `WalletService.GetTransactionsAsync` | `UserWalletContract.GetCustomerWalletHistory` | `/profile/wallet` | تاریخچه تراکنش‌های کیف پول |
| 4.3 | `WalletService.GetMagicWalletStatusAsync` | `UserWalletContract.GetMagicWalletStatus` | `/profile`, `/profile/magic-wallet`, `/my-packages` | وضعیت کیف پول جادویی (Magic) |
| 4.4 | `WalletService.InitiateMagicChargeAsync` | `UserWalletContract.InitiateMagicCharge` | `/profile/magic-wallet` | شروع شارژ کیف پول جادویی → درگاه |
| 4.5 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract.VerifyMagicCharge` | `/profile/payment-callback?type=magic-wallet` | تأیید شارژ Magic پس از درگاه |
| 4.6 | `WalletService.InitiateDiscountChargeAsync` | `UserWalletContract.InitiateDiscountCharge` | `/profile/charge-discount-wallet` | شروع شارژ کیف پول تخفیفی → درگاه |
| 4.7 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract.VerifyDiscountCharge` | `/profile/payment-callback?type=discount-wallet` | تأیید شارژ کیف تخفیفی |
| 4.8 | `WalletService.InitiateCreditChargeAsync` | `UserWalletContract.InitiateCreditCharge` | `/profile/charge-credit-wallet` | شروع شارژ کیف پول اصلی → درگاه |
| 4.9 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract.VerifyCreditCharge` | `/profile/payment-callback?type=credit-wallet` | تأیید شارژ کیف اصلی |
| 4.10 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواست‌های برداشت |
| 4.11 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت |
| 4.12 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) |
| 4.13 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداش‌های قابل برداشت برای انتخاب |
---
## ۵. Callback پرداخت (مشترک)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 5.1 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback` (type=package) | تأیید خرید پکیج |
| 5.2 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=magic-wallet) | تأیید شارژ Magic |
| 5.3 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=discount-wallet) | تأیید شارژ تخفیفی |
| 5.4 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=credit-wallet) | تأیید شارژ کیف اصلی |
| 5.5 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی |
| 5.6 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | به‌روزرسانی claims کاربر پس از پرداخت موفق |
---
## ۶. فروشگاه اصلی (Store)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 6.1 | `ProductService.GetProductsPagedAsync` | `ProductsContract.GetProductsForCustomer` | `/products` | لیست محصولات با فیلتر/صفحه‌بندی |
| 6.2 | `ProductService.GetTopSellingAsync` | `ProductsContract` | `/` (صفحه اصلی) | پرفروش‌ترین محصولات |
| 6.3 | `ProductService.GetByIdAsync` | `ProductsContract.GetProductByIdForCustomer` | `/product/{id}` | جزئیات محصول + گالری |
| 6.4 | `CategoryService.GetAllAsync` / `GetByIdAsync` | `CategoryContract.GetAllCategoriesForCustomer` | `/categories`, `/products` | درخت دسته‌بندی محصولات |
| 6.5 | `CartService` — Add/Update/Remove/Get | `UserCartsContract.*ForCustomer` | `/`, `/products`, `/product/{id}`, `/cart` | مدیریت سبد خرید (AddNew, Update, Remove, GetCustomerCart) |
| 6.6 | `UserOrderContract.SubmitShopBuyOrder` | `UserOrderContract` | `/checkout-summary` | ثبت سفارش و پرداخت از کیف پول |
| 6.7 | `OrderService.GetOrdersAsync` | `UserOrderContract.GetCustomerOrders` | `/orders` | لیست سفارش‌های کاربر |
| 6.8 | `OrderService.GetOrderAsync` | `UserOrderContract.GetCustomerOrder` | `/order/{id}`, `/order-tracking/{id}` | جزئیات یک سفارش |
| 6.9 | `OrderService.CalculateOrderPVAsync` | `UserOrderContract.CalculateOrderPV` | `/order/{id}` | محاسبه PV سفارش |
| 6.10 | `VATService.GetRateAsync` | `UserOrderContract` (via scope) | Store/Discount صفحات قیمت | نرخ مالیات بر ارزش افزوده (کش روزانه) |
| 6.11 | `WalletService.GetBalancesAsync` | `UserWalletContract` | `/checkout-summary` | بررسی موجودی برای پرداخت سفارش |
---
## ۷. فروشگاه تخفیفی (Discount Store)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 7.1 | `DiscountProductService.GetProductsAsync` | `DiscountProductContract.GetDiscountProducts` | `/discount-store` | لیست محصولات تخفیفی |
| 7.2 | `DiscountProductService.GetTopSellingAsync` | `DiscountProductContract` | `/` | پرفروش‌های فروشگاه تخفیفی |
| 7.3 | `DiscountProductService.GetByIdAsync` | `DiscountProductContract.GetDiscountProductById` + Images | `/discount-store/product/{id}` | جزئیات محصول تخفیفی |
| 7.4 | `DiscountProductService.GetCategoriesAsync` | `DiscountCategoryContract.GetDiscountCategories` | `/discount-store` | دسته‌بندی‌های فروشگاه تخفیفی |
| 7.5 | `DiscountCartService` — Add/Update/Remove/Get | `DiscountShoppingCartContract.*` | `/discount-store`, `/discount-store/cart` | سبد خرید تخفیفی |
| 7.6 | `DiscountOrderService.PlaceOrderAsync` | `DiscountOrderContract.PlaceOrder` | `/discount-store/checkout` | ثبت سفارش (کیف تخفیفی + درگاه) |
| 7.7 | `DiscountOrderService.GetUserOrdersAsync` | `DiscountOrderContract.GetUserOrders` | `/discount-store/orders` | لیست سفارش‌های تخفیفی |
| 7.8 | `DiscountOrderService.GetOrderByIdAsync` | `DiscountOrderContract.GetOrderById` | `/discount-store/order/{id}` | جزئیات سفارش تخفیفی |
| 7.9 | `UserAddressContract.GetCustomerAddresses` | `UserAddressContract` | `/discount-store/checkout` | انتخاب آدرس تحویل |
---
## ۸. باشگاه مشتریان (Club)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 8.1 | `ClubMembershipService.GetMyMembershipAsync` | `ClubMembershipContract.GetClubMembership` | `/club/membership` | وضعیت عضویت باشگاه |
| 8.2 | `ClubConfigurationService.GetClubConfigurationAsync` | `ConfigurationContract.GetClubConfiguration` | `/club/membership` | هزینه فعال‌سازی و هدیه عضویت |
| 8.3 | `ClubConfigurationService.GetClubFeaturesAsync` | `ConfigurationContract.GetClubFeatures` | `/club/features` | لیست امکانات باشگاه |
| 8.4 | `ClubMembershipService.ActivateMembershipAsync` | `ClubMembershipContract.ActivateClubMembership` | `Club/ActivationSection` | فعال‌سازی/تمدید عضویت باشگاه |
| 8.5 | `OtpTokenContract.CreateNewOtpToken` | `OtpTokenContract` | `ClubMembershipContractDialog` | OTP برای امضای قرارداد باشگاه |
| 8.6 | `ClubMembershipContract.AcceptClubMembershipContract` | `ClubMembershipContract` | `ClubMembershipContractDialog` | ثبت امضای قرارداد باشگاه |
---
## ۹. شبکه فروش (Network / MLM)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 9.1 | `NetworkMembershipService.GetMyNetworkTreeAsync` | `NetworkMembershipContract.GetMyNetworkTree` | `/profile/tree` (`OrganizationChart`) | درخت سازمان فروش کاربر |
| 9.2 | `NetworkMembershipService.GetSubordinateTreeAsync` | `NetworkMembershipContract.GetSubordinateTree` | `/profile/tree` | drill-down به زیرمجموعه |
| 9.3 | `NetworkMembershipService.GetMyNetworkStatisticsAsync` | `NetworkMembershipContract.GetMyNetworkStatistics` | `/profile/tree`, `/network/statistics` | آمار پا چپ/راست، تعداد اعضا |
---
## ۱۰. کمیسیون و پاداش
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 10.1 | `CommissionService.GetWeekDefinitionsAsync` | `CommissionContract.GetWeekDefinitions` | `Shared/WeekSelector`, Commission pages | لیست هفته‌های محاسبه پاداش |
| 10.2 | `CommissionService.GetMyCommissionPayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/commission/dashboard` | تاریخچه پرداخت پاداش‌ها |
| 10.3 | `CommissionService.GetMyWeeklyBalanceAsync` | `CommissionContract.GetMyWeeklyBalances` | `/commission/weekly-balance` | جزئیات بالانس هفتگی (چپ/راست) |
| 10.4 | `PackageService.GetAllPackagesAsync` | `PackageContract` | Commission pages | فیلتر پاداش بر اساس پکیج |
---
## ۱۱. محتوای سایت، بلاگ و صفحات ثابت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 11.1 | `SitePageSettingsService.GetPageAsync` | `SitePageSettingsContract.GetPageSettings` | `/` (landing), `/about`, `/contact`, `/licenses` | محتوای داینامیک صفحات (hero، تصاویر، JSON تنظیمات) |
| 11.2 | `BlogPostService.GetFeaturedPostsAsync` | `BlogPostContract.GetFeaturedBlogPosts` | `/`, `/profile` | پست‌های ویژه |
| 11.3 | `BlogPostService.GetPublishedPostsAsync` | `BlogPostContract.GetPublishedBlogPosts` | `/`, `/blog` | لیست پست‌های منتشرشده |
| 11.4 | `BlogPostService.GetBySlugAsync` | `BlogPostContract.GetBlogPostBySlug` | `/blog/{slug}` | محتوای کامل یک پست |
| 11.5 | `BlogPostService.IncrementViewCountAsync` | `BlogPostContract.IncrementViewCount` | `/blog/{slug}` | افزایش شمارنده بازدید |
| 11.6 | `BlogCategoryService.GetActiveCategoriesAsync` | `BlogCategoryContract` | `/blog` | فیلتر دسته‌بندی بلاگ |
| 11.7 | FAQ | — (hardcoded) | `/faq` | سوالات متداول — **بدون فراخوانی API** |
---
## ۱۲. زیرساخت مشترک (Infrastructure)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 12.1 | `ImageCacheService.ResolveAsync` | `ImageResolverContract` | `Shared/AppImage` (سراسری) | resolve مسیر تصویر CMS به data-URI |
| 12.2 | `AppVersionService.CheckVersionAsync` | `AppVersionContract` | `App.razor`, `MainLayout` | بررسی نسخه جدید اپ |
| 12.3 | `AppVersionService.ApplyUpdateAsync` / `SkipVersionAsync` | — (localStorage + reload) | `App.razor`, `MainLayout` | اعمال یا رد آپدیت |
| 12.4 | `MainService.OnChangeHandler` | — (in-memory) | `/`, `AuthDialog` | اطلاع‌رسانی تغییر state سراسری (مثلاً پس از login) |
| 12.5 | `IChromiumPdfService` | — (local endpoint `/contract/generate`) | `/register` | تولید PDF قرارداد (Chromium headless) |
---
## ۱۳. Gateway / انتخاب‌گر
| صفحه | سرویس | کاربرد |
|------|--------|--------|
| `/stores` | — | انتخاب بین فروشگاه اصلی و تخفیفی |
| `/my-orders` | — | انتخاب لیست سفارش‌ها |
| `/my-cart` | — | انتخاب سبد خرید |
این صفحات فقط مسیریابی UI هستند و مستقیماً gRPC صدا نمی‌زنند.
---
## ۱۴. سرویس‌های ثبت‌شده ولی بدون استفاده در UI
| سرویس / Contract | وضعیت |
|------------------|--------|
| `SitePageService` (`SitePageContract`) | ثبت در DI؛ **هیچ صفحه‌ای inject نمی‌کند** (جایگزین: `SitePageSettingsService`) |
| `TransactionsContract` | ثبت در DI؛ **استفاده نشده** |
| `UserWalletHistoryContract` | ثبت در DI؛ **استفاده نشده** (تاریخچه از `UserWalletContract.GetCustomerWalletHistory` می‌آید) |
| `TokenNotificationService` | ثبت در DI؛ **هنوز به Layout وصل نشده** (SignalR برای invalidation توکن) |
---
## نمودار جریان پرداخت
```mermaid
flowchart LR
subgraph initiate [شروع پرداخت]
A1[Profile / Checkout]
A2[MagicWallet / ChargeDiscount]
A3[Discount Checkout]
end
subgraph gateway [Zarinpal]
G[درگاه پرداخت]
end
subgraph callback [PaymentCallback]
C1[Verify Package]
C2[Verify Magic]
C3[Verify Discount Wallet]
C4[Verify Discount Order]
end
A1 --> G
A2 --> G
A3 --> G
G --> callback
```
---
## فهرست gRPC Contractهای فعال
| Contract | Wrapper اصلی |
|----------|----------------|
| `UserContract` | `AuthService`, صفحات Profile/Register |
| `UserAddressContract` | Profile, Checkout pages |
| `CityContract` | Address dialogs |
| `PackageContract` | `PackageService`, PaymentCallback, Checkout |
| `UserWalletContract` | `WalletService` |
| `UserOrderContract` | `OrderService`, CheckoutSummary |
| `UserCartsContract` | `CartService` |
| `ProductsContract` | `ProductService` |
| `CategoryContract` | `CategoryService` |
| `DiscountProductContract` / `DiscountCategoryContract` | `DiscountProductService` |
| `DiscountShoppingCartContract` | `DiscountCartService` |
| `DiscountOrderContract` | `DiscountOrderService` |
| `ClubMembershipContract` | `ClubMembershipService`, Dialog |
| `OtpTokenContract` | Club contract dialog |
| `ConfigurationContract` | `ClubConfigurationService` |
| `NetworkMembershipContract` | `NetworkMembershipService` |
| `CommissionContract` | `CommissionService` |
| `BlogPostContract` | `BlogPostService` |
| `BlogCategoryContract` | `BlogCategoryService` |
| `SitePageSettingsContract` | `SitePageSettingsService` |
| `AppVersionContract` | `AppVersionService` |
| `ImageResolverContract` | `ImageCacheService` |
+3
View File
@@ -0,0 +1,3 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
@@ -0,0 +1,56 @@
---
# FrontOffice (Blazor Server) production.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontoffice
namespace: default
labels:
app: frontoffice
environment: production
spec:
replicas: 1
selector:
matchLabels:
app: frontoffice
template:
metadata:
labels:
app: frontoffice
spec:
containers:
- name: frontoffice
image: 194.5.195.53:30080/admin/frontoffice:prod
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
imagePullSecrets:
- name: gitea-registry-secret
---
apiVersion: v1
kind: Service
metadata:
name: frontoffice-svc
namespace: default
labels:
app: frontoffice
spec:
selector:
app: frontoffice
ports:
- port: 80
targetPort: 80
name: http
type: ClusterIP
+56
View File
@@ -0,0 +1,56 @@
---
# FrontOffice (Blazor Server) staging.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontoffice
namespace: default
labels:
app: frontoffice
environment: staging
spec:
replicas: 1
selector:
matchLabels:
app: frontoffice
template:
metadata:
labels:
app: frontoffice
spec:
containers:
- name: frontoffice
image: 194.5.195.53:30080/admin/frontoffice:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Staging"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
imagePullSecrets:
- name: gitea-registry-secret
---
apiVersion: v1
kind: Service
metadata:
name: frontoffice-svc
namespace: default
labels:
app: frontoffice
spec:
selector:
app: frontoffice
ports:
- port: 80
targetPort: 80
name: http
type: ClusterIP
+25
View File
@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
+78 -1
View File
@@ -11,10 +11,15 @@ public partial class App
{
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private AppVersionService AppVersionService { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// Check app version and show update dialog if needed
await CheckAndShowVersionUpdateAsync();
// Check for referral code in URL query parameters
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var query = QueryHelpers.ParseQuery(uri.Query);
@@ -53,5 +58,77 @@ public partial class App
}
}
}
}
/// <summary>
/// بررسی نسخه و نمایش دیالوگ آپدیت در صورت نیاز
/// فقط برای کاربران لاگین شده
/// </summary>
private async Task CheckAndShowVersionUpdateAsync()
{
try
{
// فقط برای کاربران لاگین شده
if (!await AuthService.IsAuthenticatedAsync())
return;
// forDialog: true یعنی اگه skip کرده ولی یک روز گذشته، دوباره نشون بده
var versionCheck = await AppVersionService.CheckVersionAsync(forDialog: true);
if (!versionCheck.HasNewVersion || string.IsNullOrEmpty(versionCheck.NewVersion))
return;
// ثبت زمان نمایش دیالوگ
await AppVersionService.RecordDialogShownAsync();
// تنظیمات دیالوگ
var dialogOptions = new DialogOptions
{
BackdropClick = !versionCheck.IsForceUpdate, // اگر force باشه نتونه بیرون کلیک کنه
CloseOnEscapeKey = !versionCheck.IsForceUpdate,
CloseButton = !versionCheck.IsForceUpdate,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var parameters = new DialogParameters<ReleaseNotesDialog>
{
{ x => x.OldVersion, versionCheck.OldVersion },
{ x => x.NewVersion, versionCheck.NewVersion },
{ x => x.ReleaseNotes, versionCheck.ReleaseNotes },
{ x => x.UpdateMessage, versionCheck.UpdateMessage },
{ x => x.IsForceUpdate, versionCheck.IsForceUpdate }
};
var dialog = await DialogService.ShowAsync<ReleaseNotesDialog>("بروزرسانی", parameters, dialogOptions);
var result = await dialog.Result;
if (result is { Canceled: false, Data: bool shouldUpdate })
{
if (shouldUpdate)
{
// کاربر خواست آپدیت کنه
await AppVersionService.ApplyUpdateAsync(versionCheck.NewVersion);
// رفرش صفحه
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
}
else
{
// کاربر گفت بعداً (فقط وقتی force نیست)
await AppVersionService.SkipVersionAsync(versionCheck.NewVersion);
}
}
else if (versionCheck.IsForceUpdate)
{
// اگر force بود و دیالوگ بسته شد، باز هم آپدیت کن
await AppVersionService.ApplyUpdateAsync(versionCheck.NewVersion);
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
}
}
catch (Exception ex)
{
// در صورت خطا، لاگ کن ولی ادامه بده
Console.WriteLine($"Error checking version: {ex.Message}");
}
}
}
+121 -40
View File
@@ -5,20 +5,32 @@ using Grpc.Net.Client;
using MudBlazor.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using FrontOffice.BFF.Category.Protobuf.Protos.Category;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.Transaction.Protobuf.Protos.Transaction;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using FrontOffice.BFF.UserWallet.Protobuf.Protos.UserWallet;
using FrontOffice.BFF.ShopingCart.Protobuf.Protos.ShopingCart;
// New Proto imports
using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
using FrontOffice.BFF.Commission.Protobuf.Protos.Commission;
using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
using FrontOffice.BFF.DiscountShop.Protobuf.Protos.DiscountShop;
// Updated imports to use CMS protobuf instead of FrontOffice.BFF
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.City;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities.Seo;
namespace Microsoft.Extensions.DependencyInjection;
@@ -45,72 +57,137 @@ public static class ConfigureServices
services.AddSingleton<UserAuthInfo>();
services.AddScoped<AuthService>();
services.AddScoped<AuthDialogService>();
services.AddScoped<GuestActionGate>();
// Storefront services
services.AddScoped<CartService>();
services.AddScoped<ProductService>();
services.AddScoped<CategoryService>();
services.AddScoped<OrderService>();
services.AddScoped<WalletService>();
services.AddScoped<VATService>(); // Singleton برای cache روزانه
// Package service
services.AddScoped<PackageService>();
// New services for Club, Network, Commission
services.AddScoped<ClubMembershipService>();
services.AddScoped<ClubConfigurationService>();
services.AddScoped<NetworkMembershipService>();
services.AddScoped<CommissionService>();
// App Version Service for cache invalidation
services.AddScoped<AppVersionService>();
// Main Service for core functionality
services.AddScoped<MainService>();
// Site Page service for dynamic content (About, Contact)
services.AddScoped<SitePageService>();
// Blog Post service for landing page latest posts & blog pages
services.AddScoped<BlogPostService>();
// Blog Category service for blog sidebar filters
services.AddScoped<BlogCategoryService>();
// Discount Store services
services.AddScoped<DiscountProductService>();
services.AddScoped<DiscountCartService>();
services.AddScoped<DiscountOrderService>();
// Site Page Settings Service (simplified page management)
services.AddScoped<SitePageSettingsService>();
// Device detection: very light, dependency-free
services.AddTransient<IDeviceDetector, DeviceDetector>();
// PDF generation (Chromium only)
services.AddSingleton<FrontOffice.Main.Utilities.Pdf.IChromiumPdfService, FrontOffice.Main.Utilities.Pdf.ChromiumPdfService>();
// SignalR Token Notification Service
services.AddScoped<TokenNotificationService>();
// SEO
services.AddScoped<SeoMetadataProvider>();
services.AddScoped<SitemapGenerator>();
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration)
{
var baseUrl = configuration["GwUrl"];
var baseUrl = ResolveGatewayUrl(configuration)
?? throw new InvalidOperationException("Gateway URL is missing. Set GW_URL or GwUrl.");
// Register optimized HttpClient for gRPC
var isHttp = baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase);
// When the base URL is plain HTTP (e.g. in-cluster http://cms-svc:8080), we must force HTTP/1.1
// so that GrpcChannel does not upgrade to h2c and trigger HTTP_1_1_REQUIRED from Kestrel.
// For HTTPS the default negotiation is fine.
services.AddScoped(sp =>
{
var handler = new HttpClientHandler
{
MaxConnectionsPerServer = 10,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
HttpMessageHandler inner = isHttp
? new SocketsHttpHandler
{
MaxConnectionsPerServer = 10,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
}
: new HttpClientHandler
{
MaxConnectionsPerServer = 10,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, handler))
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, inner))
{
Timeout = TimeSpan.FromMinutes(10),
BaseAddress = new Uri(baseUrl)
BaseAddress = new Uri(baseUrl),
DefaultRequestVersion = isHttp ? System.Net.HttpVersion.Version11 : System.Net.HttpVersion.Version20,
DefaultVersionPolicy = isHttp
? System.Net.Http.HttpVersionPolicy.RequestVersionExact
: System.Net.Http.HttpVersionPolicy.RequestVersionOrHigher
};
});
// Register gRPC clients with authentication
services.AddScoped(CreateAuthenticatedClient<PackageContract.PackageContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserContract.UserContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserAddressContract.UserAddressContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserOrderContract.UserOrderContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserWalletContract.UserWalletContractClient>);
services.AddScoped(CreateAuthenticatedClient<CategoryContract.CategoryContractClient>);
// Products gRPC
services.AddScoped(CreateAuthenticatedClient<FrontOffice.BFF.Products.Protobuf.Protos.Products.ProductsContract.ProductsContractClient>);
services.AddScoped(CreateAuthenticatedClient<TransactionContract.TransactionContractClient>);
services.AddScoped(CreateAuthenticatedClient<ShopingCartContract.ShopingCartContractClient>);
// New gRPC clients for Club, Network, Commission, DiscountShop
services.AddScoped(CreateAuthenticatedClient<ClubMembershipContract.ClubMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<NetworkMembershipContract.NetworkMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountShopContract.DiscountShopContractClient>);
// Register gRPC clients with authentication - Updated for CMS
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Package.PackageContract.PackageContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.User.UserContract.UserContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserOrder.UserOrderContract.UserOrderContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWallet.UserWalletContract.UserWalletContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Category.CategoryContract.CategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Products.ProductsContract.ProductsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Transactions.TransactionsContract.TransactionsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserCarts.UserCartsContract.UserCartsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.City.CityContract.CityContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserAddress.UserAddressContract.UserAddressContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWalletHistory.UserWalletHistoryContract.UserWalletHistoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ClubMembership.ClubMembershipContract.ClubMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.OtpToken.OtpTokenContract.OtpTokenContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Configuration.ConfigurationContract.ConfigurationContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.NetworkMembership.NetworkMembershipContract.NetworkMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Commission.CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.AppVersion.AppVersionContract.AppVersionContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient>);
services.AddScoped(CreateAuthenticatedClient<SitePageSettingsContract.SitePageSettingsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient>);
// Image Resolver Service
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ImageResolver.ImageResolverContract.ImageResolverContractClient>);
services.AddScoped<ImageCacheService>();
// Discount Store gRPC clients
services.AddScoped(CreateAuthenticatedClient<DiscountProductContract.DiscountProductContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountCategoryContract.DiscountCategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountShoppingCartContract.DiscountShoppingCartContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountOrderContract.DiscountOrderContractClient>);
return services;
}
private static string? ResolveGatewayUrl(IConfiguration configuration)
{
var envUrl = configuration["GW_URL"];
if (!string.IsNullOrWhiteSpace(envUrl))
return envUrl.TrimEnd('/');
return configuration["GwUrl"]?.TrimEnd('/');
}
private static TClient CreateAuthenticatedClient<TClient>(IServiceProvider sp)
where TClient : class
{
var httpClient = sp.GetRequiredService<HttpClient>();
var localStorage = sp.GetRequiredService<ILocalStorageService>();
var baseUrl = httpClient.BaseAddress?.ToString() ?? throw new InvalidOperationException("Base URL not configured");
var isHttps = baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
@@ -130,10 +207,14 @@ public static class ConfigureServices
}
});
var channelCredentials = isHttps
? ChannelCredentials.Create(new SslCredentials(), credentials)
: ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
var channel = GrpcChannel.ForAddress(baseUrl, new GrpcChannelOptions
{
UnsafeUseInsecureChannelCallCredentials = true,
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
UnsafeUseInsecureChannelCallCredentials = !isHttps,
Credentials = channelCredentials,
HttpClient = httpClient,
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
+30
View File
@@ -0,0 +1,30 @@
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy NuGet config and project file
COPY ["FrontOffice.Main/NuGet.config", "NuGet.config"]
COPY ["FrontOffice.Main/FrontOffice.Main.csproj", "FrontOffice.Main/"]
# Restore dependencies
RUN dotnet restore "FrontOffice.Main/FrontOffice.Main.csproj" --configfile NuGet.config
# Copy all source code
COPY . .
# Build and publish
WORKDIR "/src/FrontOffice.Main"
RUN dotnet publish "FrontOffice.Main.csproj" -c Release -o /app/publish
# Runtime stage - aspnet for Blazor Server
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
# Trust the staging-ca so server-side gRPC calls to https://cms.se.kbs1.ir succeed without PartialChain
COPY ["FrontOffice.Main/staging-ca.crt", "/usr/local/share/ca-certificates/staging-ca.crt"]
RUN update-ca-certificates
ENV ASPNETCORE_URLS=http://+:80
EXPOSE 80
ENTRYPOINT ["dotnet", "FrontOffice.Main.dll"]
+34 -18
View File
@@ -1,23 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>6dab807c-c6d8-4711-bf64-11c69e8d39f4</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.112" />
<PackageReference Include="Foursat.FrontOffice.BFF.Products.Protobuf" Version="0.0.15" />
<PackageReference Include="Foursat.FrontOffice.BFF.Transaction.Protobuf" Version="0.0.111" />
<PackageReference Include="Foursat.FrontOffice.BFF.Category.Protobuf" Version="0.0.12" />
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.116" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.114" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.114" />
<PackageReference Include="Foursat.FrontOffice.BFF.ShopingCart.Protobuf" Version="0.0.15" />
<!-- UserWallet moved to ProjectReference for latest proto -->
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.204" />
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
@@ -26,20 +21,41 @@
<!-- Keep only PuppeteerSharp for Chromium-based PDF generation -->
<PackageReference Include="PuppeteerSharp" Version="11.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
</ItemGroup>
<!-- Local Proto Project Reference for development -->
<!-- <ItemGroup>-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Package.Protobuf\FrontOffice.BFF.Package.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.City.Protobuf\FrontOffice.BFF.City.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Configuration.Protobuf\FrontOffice.BFF.Configuration.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.User.Protobuf\FrontOffice.BFF.User.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserOrder.Protobuf\FrontOffice.BFF.UserOrder.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Commission.Protobuf\FrontOffice.BFF.Commission.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" />-->
<!-- </ItemGroup>-->
<!-- New Proto Projects (local references until NuGet publish) -->
<ItemGroup>
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.Protobuf.csproj" />
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Commission.Protobuf\FrontOffice.BFF.Commission.Protobuf.csproj" />
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj" />
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.DiscountShop.Protobuf\FrontOffice.BFF.DiscountShop.Protobuf.csproj" />
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" />
</ItemGroup>
<!-- <ItemGroup>-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Commission.Protobuf\FrontOffice.BFF.Commission.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.DiscountShop.Protobuf\FrontOffice.BFF.DiscountShop.Protobuf.csproj" />-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" />-->
<!-- </ItemGroup>-->
<!-- -->
<ItemGroup>
<Folder Include="Components" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources>
<packageSourceCredentials>
<Nexus>
<add key="Username" value="admin" />
<add key="ClearTextPassword" value="87zH26nbqT" />
</Nexus>
</packageSourceCredentials>
</configuration>
+243 -173
View File
@@ -1,192 +1,262 @@
@attribute [Route(RouteConstants.About.Index)]
@inject NavigationManager Navigation
@inject SitePageSettingsService PageSettingsService
<PageTitle>درباره ما | کارا بازار سلامت</PageTitle>
<!-- Hero Section -->
<section class="about-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudGrid Justify="Justify.Center" Spacing="4">
<MudItem xs="12" md="6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">درباره کارا بازار سلامت</MudChip>
<MudStack Spacing="3">
<MudText Typo="Typo.h2" Class="mb-3">
پلتفرم هوشمند شبکه‌سازی و مدیریت تیم
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا شبکه‌های فروش خود را گسترش دهند و تیم‌های خود را به صورت کارآمد مدیریت کنند.
</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
شروع کنید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
OnClick="ScrollToContact">
تماس با ما
</MudButton>
</MudStack>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="images/team-work.jpg"
Alt="جلسه تیم فروش و شبکه‌سازی"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
</MudGrid>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<LoadingState />
</MudContainer>
</section>
<MudStack Spacing="8">
<!-- Mission & Vision -->
<section class="py-12">
}
else
{
<!-- Hero Section -->
<section class="about-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">چشم‌انداز و مأموریت ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
<MudGrid Justify="Justify.Center" Spacing="4">
<MudItem xs="12" md="6">
<MudPaper Elevation="3" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">چشم‌انداز</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
تبدیل شدن به پیشروترین پلتفرم مدیریت شبکه‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">درباره کارا بازار سلامت</MudChip>
<MudStack Spacing="3">
<MudText Typo="Typo.h2" Class="mb-3">
@(_page?.HeroTitle ?? "پلتفرم هوشمند تیم‌سازی و مدیریت فروش")
</MudText>
</MudPaper>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
@(_page?.HeroSubtitle ?? "ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند.")
</MudText>
<MudStack Row="true" Spacing="2" Class="flex-wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
شروع کنید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
تماس با ما
</MudButton>
</MudStack>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="3" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">مأموریت</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار.
</MudText>
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<AppImage Path="@(_page?.HeroImagePath ?? "images/team-work.jpg")"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- Company Values -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Large" Color="Color.Info" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">صداقت و شفافیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Lightbulb" Size="Size.Large" Color="Color.Warning" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">نوآوری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Success" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">مشتری‌مداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Verified" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">کیفیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Large" Color="Color.Secondary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پاسخگویی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.EmergencyRecording" Size="Size.Large" Color="Color.Error" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پایداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم.
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<MudStack Spacing="8">
<!-- Team Section -->
<section class="pb-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="3" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar3.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">علی رضایی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیرعامل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="3" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar4.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">مریم احمدی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر محصول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
متخصص در طراحی تجربه کاربری و توسعه محصول
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="3" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar5.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">حسن کریمی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر فنی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
</MudStack>
<!-- Mission & Vision -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">چشم‌انداز و مأموریت ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">
@(_settings?.VisionTitle ?? "چشم‌انداز")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@(_settings?.VisionText ?? "تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.")
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">
@(_settings?.MissionTitle ?? "مأموریت")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@(_settings?.MissionText ?? "توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد فرصت‌های جدید برای رشد و توسعه پایدار.")
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- Company Values -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@if (_valueImages.Any())
{
@foreach (var value in _valueImages)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
@if (!string.IsNullOrWhiteSpace(value.IconName))
{
<MudIcon Icon="@($"Icons.Material.Filled.{value.IconName}")" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
}
<MudText Typo="Typo.h6" Class="mb-2">@value.Title</MudText>
@if (!string.IsNullOrWhiteSpace(value.Description))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@value.Description
</MudText>
}
</MudPaper>
</MudItem>
}
}
else
{
@* Fallback: hardcoded values *@
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Large" Color="Color.Info" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">صداقت و شفافیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Lightbulb" Size="Size.Large" Color="Color.Warning" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">نوآوری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Success" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">مشتری‌مداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Verified" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">کیفیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Large" Color="Color.Secondary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پاسخگویی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.EmergencyRecording" Size="Size.Large" Color="Color.Error" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پایداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم.
</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<!-- Team Section -->
<section class="pb-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
@if (_teamImages.Any())
{
@foreach (var member in _teamImages)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
@if (!string.IsNullOrWhiteSpace(member.ImagePath))
{
<AppImage Path="@member.ImagePath"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Person" />
}
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">@member.Title</MudText>
@if (!string.IsNullOrWhiteSpace(member.Subtitle))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@member.Subtitle</MudText>
}
@if (!string.IsNullOrWhiteSpace(member.Description))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@member.Description
</MudText>
}
</MudPaper>
</MudItem>
}
}
else
{
@* Fallback: hardcoded team *@
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar3.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">علی رضایی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیرعامل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar4.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">مریم احمدی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر محصول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
متخصص در طراحی تجربه کاربری و توسعه محصول
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar5.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">حسن کریمی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر فنی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده
</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
</MudStack>
}
+41 -2
View File
@@ -1,8 +1,47 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages;
public partial class About
{
private void ScrollToContact()
private bool _loading = true;
private PageSettingsDto? _page;
private AboutSettings? _settings;
private List<PageSettingsImageDto> _valueImages = new();
private List<PageSettingsImageDto> _teamImages = new();
protected override async Task OnInitializedAsync()
{
// TODO: Implement smooth scroll to contact section
try
{
_page = await PageSettingsService.GetPageAsync("about");
if (_page != null)
{
_settings = _page.GetSettings<AboutSettings>();
_valueImages = _page.GetImages("values");
_teamImages = _page.GetImages("team");
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
}
private class AboutSettings
{
public string? VisionTitle { get; set; }
public string? VisionText { get; set; }
public string? VisionIcon { get; set; }
public string? MissionTitle { get; set; }
public string? MissionText { get; set; }
public string? MissionIcon { get; set; }
public string? ValuesTitle { get; set; }
public string? TeamTitle { get; set; }
}
}
+172
View File
@@ -0,0 +1,172 @@
@attribute [Route(RouteConstants.Blog.Index)]
<PageTitle>بلاگ | کارا بازار سلامت</PageTitle>
@* ═══════════════════════════════════════════════
BLOG HEADER
═══════════════════════════════════════════════ *@
<section class="blog-hero">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center">
<MudText Typo="Typo.h3" Class="dash-hero-name">بلاگ</MudText>
<MudText Typo="Typo.body1" Class="dash-hero-sub mt-2">
آخرین مطالب، آموزش‌ها و اخبار کارا بازار سلامت
</MudText>
</div>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
SEARCH + FILTERS
═══════════════════════════════════════════════ *@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudPaper Elevation="0" Class="pa-4 rounded-xl mb-6" Style="border:1px solid var(--mud-palette-divider);">
<MudGrid Spacing="3" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="6" md="5">
<MudTextField @bind-Value="_searchTerm"
Placeholder="جستجو در مقالات..."
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="false"
OnKeyUp="OnSearchKeyUp"
Class="rounded-lg" />
</MudItem>
<MudItem xs="12" sm="6" md="5">
<MudSelect T="long?" Value="_selectedCategoryId"
Placeholder="همه دسته‌بندی‌ها"
Variant="Variant.Outlined"
Class="rounded-lg"
Clearable="true"
ValueChanged="OnCategoryChanged">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@cat.Id">
@cat.Title
@if (cat.PostCount > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mr-2" Style="display:inline;">(@cat.PostCount)</MudText>
}
</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Class="rounded-lg"
StartIcon="@Icons.Material.Filled.Search"
OnClick="SearchPosts">
جستجو
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@* ═══════════════════════════════════════════════
BLOG POSTS GRID
═══════════════════════════════════════════════ *@
@if (_isLoading)
{
<LoadingState />
}
else if (!_posts.Any())
{
<MudPaper Elevation="0" Class="pa-12 text-center rounded-xl" Style="border:1px dashed var(--mud-palette-divider);">
<MudIcon Icon="@Icons.Material.Outlined.Article" Size="Size.Large" Class="mud-text-secondary mb-3" Style="font-size:3.5rem;" />
<MudText Typo="Typo.h6" Class="mud-text-secondary">مقاله‌ای یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-1">
@if (!string.IsNullOrWhiteSpace(_searchTerm) || _selectedCategoryId.HasValue)
{
<span>فیلترها را تغییر دهید یا عبارت دیگری جستجو کنید.</span>
}
else
{
<span>به‌زودی مقالات جدید منتشر خواهد شد.</span>
}
</MudText>
</MudPaper>
}
else
{
<MudGrid Spacing="4">
@foreach (var post in _posts)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="1" Class="rounded-xl blog-card-v2 cursor-pointer" Style="overflow:hidden;"
@onclick="() => NavigateToPost(post.Slug)">
@* Thumbnail *@
<div class="blog-thumb" style="height:200px; background:#f0f0f0;">
@if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl))
{
<AppImage Path="@post.ThumbnailUrl"
Alt="@post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:200px;" />
}
else
{
<div style="height:200px; display:flex; align-items:center; justify-content:center; background:linear-gradient(135deg,#6366f1,#a78bfa);">
<MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Large" Style="color:rgba(255,255,255,.4);" />
</div>
}
</div>
@* Content *@
<div class="pa-4">
@if (post.Categories.Any())
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="Color.Primary" Class="mb-2">
@post.Categories.First().Title
</MudChip>
}
<MudText Typo="Typo.h6" Class="mb-1 blog-title-clamp">
@post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(post.Summary))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3 blog-summary-clamp">
@post.Summary
</MudText>
}
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mt-auto">
@if (post.PublishedAt.HasValue)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="ml-1" />
@post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
}
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Class="ml-1" />
@post.ViewCount.ToString("N0")
</MudText>
</MudStack>
</div>
</MudPaper>
</MudItem>
}
</MudGrid>
@* ── Pagination ── *@
@if (_totalPages > 1)
{
<div class="d-flex justify-center mt-8">
<MudPagination Count="@_totalPages"
Selected="@_currentPage"
SelectedChanged="OnPageChanged"
Color="Color.Primary"
Variant="Variant.Outlined"
Class="rounded-lg" />
</div>
}
}
</MudContainer>
@@ -0,0 +1,91 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace FrontOffice.Main.Pages.Blog;
public partial class Index
{
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private BlogCategoryService BlogCategoryService { get; set; } = default!;
// ── State ──
private List<BlogPostCardDto> _posts = new();
private List<BlogCategoryDto> _categories = new();
private string? _searchTerm;
private long? _selectedCategoryId;
private int _currentPage = 1;
private int _totalPages;
private bool _isLoading = true;
private const int PageSize = 9;
protected override async Task OnInitializedAsync()
{
// Load categories and posts in parallel
var categoriesTask = BlogCategoryService.GetActiveCategoriesAsync();
var postsTask = LoadPostsAsync();
_categories = await categoriesTask;
await postsTask;
}
private async Task LoadPostsAsync()
{
_isLoading = true;
StateHasChanged();
try
{
var result = await BlogPostService.GetPublishedPostsAsync(
page: _currentPage,
pageSize: PageSize,
search: _searchTerm,
categoryId: _selectedCategoryId);
_posts = result.Posts;
_totalPages = result.TotalPages;
}
catch
{
_posts = new();
_totalPages = 0;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private async Task SearchPosts()
{
_currentPage = 1;
await LoadPostsAsync();
}
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SearchPosts();
}
}
private async Task OnCategoryChanged(long? categoryId)
{
_selectedCategoryId = categoryId;
_currentPage = 1;
await LoadPostsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadPostsAsync();
}
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
}
+122
View File
@@ -0,0 +1,122 @@
@page "/blog/{Slug}"
<PageTitle>@(_post?.Title ?? "مقاله") | بلاگ</PageTitle>
@if (_isLoading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-12">
<LoadingState />
</MudContainer>
}
else if (_post == null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-12">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="مقاله یافت نشد"
Description="این مقاله حذف شده یا آدرس اشتباه است."
ActionText="بازگشت به بلاگ"
ActionHref="/blog" />
</MudContainer>
}
else
{
@* ═══ Featured Image Header ═══ *@
@if (!string.IsNullOrWhiteSpace(_post.FeaturedImagePath))
{
<div class="blog-detail-hero">
<AppImage Path="@_post.FeaturedImagePath"
Alt="@_post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:100%;" />
<div class="blog-detail-hero-overlay"></div>
</div>
}
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
@* ── Breadcrumb ── *@
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Class="mb-4">
<MudLink Href="/" Typo="Typo.caption" Class="mud-text-secondary">خانه</MudLink>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Size="Size.Small" Class="mud-text-secondary" />
<MudLink Href="/blog" Typo="Typo.caption" Class="mud-text-secondary">بلاگ</MudLink>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Size="Size.Small" Class="mud-text-secondary" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_post.Title</MudText>
</MudStack>
@* ── Article Header ── *@
<MudPaper Elevation="0" Class="pa-0 mb-6">
@if (_post.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="mb-3 flex-wrap">
@foreach (var cat in _post.Categories)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Primary" Class="rounded-pill"
OnClick="() => NavigateToCategoryFilter(cat.Id)">
@cat.Title
</MudChip>
}
</MudStack>
}
<MudText Typo="Typo.h4" Class="mb-3 fw-bold" Style="line-height:1.6;">
@_post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(_post.Summary))
{
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-4" Style="line-height:1.8;">
@_post.Summary
</MudText>
}
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="mb-4 flex-wrap">
@if (_post.PublishedAt.HasValue)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@_post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
</MudStack>
}
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@_post.ViewCount.ToString("N0") بازدید
</MudText>
</MudStack>
</MudStack>
<MudDivider Class="mb-6" />
</MudPaper>
@* ── Article Content ── *@
<MudPaper Elevation="0" Class="blog-content mb-8">
@((MarkupString)_post.HtmlContent)
</MudPaper>
@* ── Tags ── *@
@if (_post.Tags.Any())
{
<MudDivider Class="mb-4" />
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Class="mb-6 flex-wrap">
<MudIcon Icon="@Icons.Material.Filled.LocalOffer" Size="Size.Small" Class="mud-text-secondary ml-2" />
@foreach (var tag in _post.Tags)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Class="rounded-pill">
@tag.Title
</MudChip>
}
</MudStack>
}
@* ── Back to Blog ── *@
<div class="text-center mt-6 mb-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="GoBack"
Class="rounded-pill" StartIcon="@Icons.Material.Filled.ArrowForward">
بازگشت به بلاگ
</MudButton>
</div>
</MudContainer>
}
@@ -0,0 +1,51 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace FrontOffice.Main.Pages.Blog;
public partial class Post
{
[Inject] private IJSRuntime JS { get; set; } = default!;
[Parameter] public string Slug { get; set; } = string.Empty;
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
private BlogPostDetailDto? _post;
private bool _isLoading = true;
protected override async Task OnParametersSetAsync()
{
if (string.IsNullOrWhiteSpace(Slug)) return;
_isLoading = true;
StateHasChanged();
try
{
_post = await BlogPostService.GetBySlugAsync(Slug);
// Fire-and-forget: increment view count
if (_post != null)
{
_ = BlogPostService.IncrementViewCountAsync(_post.Id);
}
}
catch
{
_post = null;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private void NavigateToCategoryFilter(long categoryId)
{
Navigation.NavigateTo($"/blog?category={categoryId}");
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
+35 -82
View File
@@ -5,10 +5,11 @@
<PageTitle>تکمیل خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تکمیل خرید" BackHref="@RouteConstants.Package.List" />
<MudGrid Spacing="4">
<!-- Header -->
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
@@ -23,9 +24,14 @@
<MudItem xs="12" md="8">
<MudStack>
<!-- Package Details -->
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText>
@* ── G2: توضیح خرید پکیج (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
با خرید این پکیج، سقف پاداش هفتگی، ضریب کیف‌پول جادویی و دسترسی به فیچرهای اختصاصی برای شما فعال می‌شود.
</MudAlert>
@if (_selectedPackage != null)
{
<MudCard>
@@ -33,9 +39,9 @@
<MudGrid Spacing="3">
<MudItem xs="12" md="4">
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_selectedPackage.Image"
<AppImage Path="@_selectedPackage.Image"
Alt="@_selectedPackage.Title"
Height="150"
ImgHeight="150"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
@@ -65,80 +71,6 @@
</MudStack>
}
</MudPaper>
<!-- Address Selection -->
<MudPaper Elevation="4" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
@if (_isLoadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Any())
{
<MudStack Spacing="2">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => SetAddressAsDefault(address.Id)">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudCheckBox @bind-Value="address.IsDefault"
Disabled="true"
Color="Color.Primary" />
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیش‌فرض</MudChip>
}
@if (!address.IsDefault)
{
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
{
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
}
else
{
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Size="Size.Small"
OnClick="() => SetAddressAsDefault(address.Id)">
تنظیم به عنوان پیش‌فرض
</MudButton>
}
}
</MudStack>
<MudStack Row="true">
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
Class="mt-2">
افزودن آدرس
</MudButton>
</MudStack>
}
</MudPaper>
</MudStack>
</MudItem>
@@ -149,7 +81,7 @@
<MudItem xs="12" md="4">
<MudStack Spacing="4">
<!-- Discount Code Section -->
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">کد تخفیف</MudText>
<MudStack Spacing="3">
<MudTextField @bind-Value="_discountCode"
@@ -176,7 +108,7 @@
<!-- Order Summary -->
@if (_selectedPackage != null)
{
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">خلاصه سفارش</MudText>
<MudStack Spacing="3">
@@ -206,15 +138,36 @@
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="ProcessPayment"
Disabled="@(!CanProceedToPayment || _isProcessingPayment)"
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
Class="mt-2">
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
</MudButton>
@if (_selectedPackage.SupportsDayaPurchase)
{
<MudButton Variant="Variant.Filled"
Color="Color.Tertiary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Diamond"
OnClick="DayaLoanPayment"
Disabled="@(!CanProceedToPayment)"
Class="mt-2">
تأمین اعتبار الماسی دایا
</MudButton>
}
@if (!_selectedPackage.SupportsDirectPurchase && !_selectedPackage.SupportsDayaPurchase)
{
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-2">
این پکیج در حال حاضر قابل خرید نیست.
</MudAlert>
}
@if (!CanProceedToPayment)
{
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
لطفاً پکیج و آدرس را انتخاب کنید.
لطفاً پکیج را انتخاب کنید.
</MudText>
}
</MudStack>
+48 -108
View File
@@ -1,10 +1,7 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.Transaction.Protobuf.Protos.Transaction;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.Package;
using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using Severity = MudBlazor.Severity;
@@ -12,17 +9,12 @@ namespace FrontOffice.Main.Pages;
public partial class Checkout
{
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private TransactionContract.TransactionContractClient TransactionContract { get; set; } = default!;
[Parameter] public long? PackageId { get; set; }
private Pack? _selectedPackage;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private GetAllUserAddressByFilterResponseModel? _selectedAddress;
private bool _isLoadingAddresses;
private bool _isProcessingPayment;
// Discount code
@@ -33,16 +25,11 @@ public partial class Checkout
private long _discountAmount;
private long _finalPrice;
// Address management
private bool _isSettingDefaultAddress;
private long? _settingDefaultAddressId;
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
private bool CanProceedToPayment => _selectedPackage != null;
protected override async Task OnInitializedAsync()
{
await LoadPackageDetails();
await LoadAddresses();
}
private async Task LoadPackageDetails()
@@ -51,15 +38,18 @@ public partial class Checkout
{
try
{
var response = await PackageClient.GetPackageAsync(new() { Id = PackageId.Value });
if (response != null)
var packages = await PackageService.GetAllPackagesAsync();
var pkg = packages.FirstOrDefault(p => p.Id == PackageId.Value);
if (pkg != null)
{
_selectedPackage = new Pack(
Id: response.Id,
Title: response.Title,
Body: response.Description,
Image: UrlUtility.DownloadUrl + response.ImagePath,
Price: response.Price
Id: pkg.Id,
Title: pkg.Title,
Body: pkg.Description,
Image: pkg.ImageUrl,
Price: pkg.Price,
SupportsDirectPurchase: pkg.SupportsDirectPurchase,
SupportsDayaPurchase: pkg.SupportsDayaPurchase
);
_finalPrice = _selectedPackage.Price;
}
@@ -71,60 +61,6 @@ public partial class Checkout
}
}
private async Task LoadAddresses()
{
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
// Select default address if available
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
}
finally
{
_isLoadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task SetAddressAsDefault(long addressId)
{
if (_isSettingDefaultAddress) return;
_isSettingDefaultAddress = true;
_settingDefaultAddressId = addressId;
try
{
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
await LoadAddresses(); // Reload addresses to reflect the change
Snackbar.Add("آدرس پیش‌فرض با موفقیت تغییر یافت.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر آدرس پیش‌فرض: {ex.Message}", Severity.Error);
}
finally
{
_isSettingDefaultAddress = false;
_settingDefaultAddressId = null;
await InvokeAsync(StateHasChanged);
}
}
private async Task ApplyDiscountCode()
{
if (string.IsNullOrWhiteSpace(_discountCode))
@@ -156,7 +92,7 @@ public partial class Checkout
_finalPrice = _selectedPackage!.Price;
}
}
catch (Exception ex)
catch (Exception)
{
_discountMessage = "خطا در اعمال کد تخفیف.";
_discountApplied = false;
@@ -172,12 +108,11 @@ public partial class Checkout
private async Task ProcessPayment()
{
Snackbar.Add("درگاه پرداخت متصل نیست! لطفا در زمان دیگری مجددا تلاش فرمایید!", Severity.Warning);
return;
if (_isProcessingPayment) return;
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
if (!CanProceedToPayment || _selectedPackage == null)
{
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
@@ -185,33 +120,26 @@ public partial class Checkout
try
{
// Step 1: Create payment request
var paymentRequest = new PaymentRequestRequest
var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
{
Amount = _finalPrice,
CallbackUrl = $"{Navigation.BaseUri}checkout/callback",
Description = $"خرید پکیج {_selectedPackage.Title}",
Currency = CurrencyEnum.Irt,
Type = TransactionTypeEnum.Real
};
var paymentResponse = await TransactionContract.PaymentRequestAsync(paymentRequest);
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
Snackbar.Add("آدرس درگاه پرداخت دریافت نشد.", Severity.Error);
// Step 2: Create user order
var orderRequest = new CreateNewUserOrderRequest
{
Price = _finalPrice,
PackageId = _selectedPackage.Id,
PaymentStatus = false // Will be updated after payment verification
};
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
});
var orderResponse = await UserOrderContract.CreateNewUserOrderAsync(orderRequest);
if (!response.Success)
{
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
return;
}
// Step 3: Redirect to payment gateway
Navigation.NavigateTo(paymentResponse.PaymentGWUrl);
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
@@ -224,5 +152,17 @@ public partial class Checkout
}
}
private record Pack(long Id, string Title, string Body, string Image, long Price);
}
private async Task DayaLoanPayment()
{
if (_selectedPackage == null)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
}
@@ -3,7 +3,12 @@
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">فعال‌سازی یا تمدید عضویت</MudText>
@* ── G7: راهنمای هزینه فعال‌سازی (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
هزینه تخمینی بر اساس پکیج انتخابی محاسبه می‌شود. هر پکیج قیمت و شرایط فعال‌سازی متفاوتی دارد.
</MudAlert>
<MudForm @ref="_form" @bind-IsValid="_formIsValid">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6">
@@ -1,4 +1,5 @@
using FrontOffice.Main.Utilities;
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -7,6 +8,9 @@ namespace FrontOffice.Main.Pages.Club.Components;
public partial class ActivationSection : ComponentBase
{
[Inject] private ClubMembershipService ClubService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Parameter] public EventCallback OnActivationSuccess { get; set; }
@@ -18,6 +22,18 @@ public partial class ActivationSection : ComponentBase
private long _packageId = 1;
private int _durationMonths = 1;
private string? _activationCode;
private long _basePackagePrice;
protected override async Task OnInitializedAsync()
{
try
{
var packages = await PackageService.GetAllPackagesAsync();
var basePackage = packages.FirstOrDefault(p => p.IsBasePackage) ?? packages.FirstOrDefault();
_basePackagePrice = basePackage?.Price ?? 0;
}
catch { /* fallback to 0 */ }
}
private async Task HandleActivateAsync()
{
@@ -39,6 +55,19 @@ public partial class ActivationSection : ComponentBase
if (result.Success)
{
Snackbar.Add($"عضویت فعال شد! مبلغ پرداختی: {result.AmountPaid:N0} تومان", Severity.Success);
// Refresh Token to update Claims (IsClubMemberActive, HasPurchasedPackage)
try
{
var userResponse = await UserContract.GetUserAsync(new());
await AuthService.InitUserAuthInfo();
}
catch
{
// Token refresh failed but activation succeeded
Snackbar.Add("لطفاً مجدداً وارد شوید تا تغییرات اعمال شود.", Severity.Warning);
}
await OnActivationSuccess.InvokeAsync();
}
else
@@ -59,9 +88,8 @@ public partial class ActivationSection : ComponentBase
private string GetEstimatedCost()
{
// فرمول تقریبی: 56M per month (base amount from BFF implementation)
var baseAmount = 56_000_000;
var total = baseAmount * _durationMonths;
if (_basePackagePrice <= 0) return "در حال بارگذاری...";
var total = _basePackagePrice * _durationMonths;
return $"{total:N0} تومان";
}
}
+171 -130
View File
@@ -1,152 +1,193 @@
@attribute [Route(RouteConstants.Club.Features)]
@using FrontOffice.Main.Utilities
@using MudBlazor
@inject ClubConfigurationService ClubConfigService
@inject IDialogService DialogService
@inject IJSRuntime JS
<PageTitle>ویژگی‌های باشگاه مشتریان</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">ویژگی‌های باشگاه مشتریان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Club.Membership">بازگشت</MudButton>
</MudStack>
<PageHeader Title="ویژگی‌های باشگاه مشتریان" BackHref="@RouteConstants.Club.Membership" />
<!-- توضیحات کلی باشگاه -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">چرا باشگاه مشتریان؟</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Primary" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Primary">ویژگی‌های اختصاصی شما</MudText>
</MudStack>
<MudText Typo="Typo.body1" Class="mb-2">
با عضویت در باشگاه مشتریان فورست، از تخفیف‌های ویژه، امتیازات خرید و خدمات اختصاصی بهره‌مند شوید.
علاوه بر مزایای پایه عضویت در باشگاه مشتریان، شما به مجموعه‌ای از ویژگی‌های اختصاصی دسترسی دارید. برخی از این ویژگی‌ها ممکن است نیاز به زمان برای فعال‌سازی داشته باشند.
با کلیک روی دکمه «جزئیات» هر ویژگی، می‌توانید اطلاعات بیشتری درباره نحوه استفاده از آن امکان را مشاهده کنید.
</MudText>
</MudPaper>
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Discount" Color="Color.Primary" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Primary">تخفیف‌های ویژه</MudText>
</MudStack>
<MudText Typo="Typo.body2">
• تخفیف 10% تا 30% برای تمام محصولات<br />
• تخفیف‌های فصلی و مناسبتی اختصاصی<br />
• پیشنهادات ویژه برای اعضای باشگاه<br />
• کد تخفیف اختصاصی ماهانه
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Secondary" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Secondary">امتیاز خرید</MudText>
</MudStack>
<MudText Typo="Typo.body2">
• دریافت امتیاز برای هر خرید<br />
• تبدیل امتیاز به تخفیف باشگاه<br />
• امتیاز ویژه در روزهای خاص<br />
• قابلیت انتقال امتیاز به دوستان
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.LocalShipping" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Success">ارسال رایگان</MudText>
</MudStack>
<MudText Typo="Typo.body2">
• ارسال رایگان برای خریدهای بالای 500 هزار تومان<br />
• اولویت در ارسال سفارشات<br />
• ارسال اکسپرس با تخفیف 50%<br />
• امکان ارسال به چند آدرس
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Support" Color="Color.Info" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Info">پشتیبانی اختصاصی</MudText>
</MudStack>
<MudText Typo="Typo.body2">
• پشتیبانی 24/7 برای اعضای باشگاه<br />
• مشاوره خرید تخصصی<br />
• خط ویژه پاسخگویی<br />
• پیگیری سریع‌تر سفارشات
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Groups" Color="Color.Warning" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Warning">درآمد شبکه</MudText>
</MudStack>
<MudText Typo="Typo.body2">
• دریافت کمیسیون از خریدهای زیرمجموعه<br />
• سیستم باینری درختی<br />
• محاسبه خودکار درآمد هفتگی<br />
• امکان برداشت درآمد شبکه
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Event" Color="Color.Tertiary" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Tertiary">رویدادهای ویژه</MudText>
</MudStack>
<MudText Typo="Typo.body2">
• دعوت به رویدادهای اختصاصی<br />
• پیش‌فروش محصولات جدید<br />
• وبینارهای آموزشی رایگان<br />
• جوایز و مسابقات ماهانه
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">چگونه عضو شویم؟</MudText>
<MudStepper Orientation="Orientation.Horizontal" Color="Color.Primary">
<MudStep Title="انتخاب بسته">
<MudText>بسته عضویت مناسب خود را انتخاب کنید</MudText>
</MudStep>
<MudStep Title="پرداخت">
<MudText>هزینه عضویت را پرداخت کنید</MudText>
</MudStep>
<MudStep Title="فعال‌سازی">
<MudText>عضویت شما فورا فعال می‌شود</MudText>
</MudStep>
<MudStep Title="استفاده">
<MudText>از مزایای باشگاه لذت ببرید</MudText>
</MudStep>
</MudStepper>
</MudPaper>
<!-- لیست فیچرها -->
<MudText Typo="Typo.h6" Class="mt-4">ویژگی‌های شما</MudText>
@if (_isLoading)
{
<LoadingState Message="در حال دریافت ویژگی‌ها..." />
}
else if (_features == null || !_features.Any())
{
<EmptyState Icon="@Icons.Material.Filled.Stars"
Title="هنوز ویژگی‌ای برای شما فعال نشده است"
Description="با خرید پکیج یا ارتقاء عضویت، ویژگی‌های اختصاصی فعال می‌شوند." />
}
else
{
<MudPaper Elevation="2" Class="pa-2 rounded-lg">
<MudList T="ClubFeatureDto" Dense="true">
@foreach (var feature in _features)
{
<MudListItem>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Style="width: 100%;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle"
Color="@(feature.IsEnabled ? Color.Success : Color.Default)" />
<MudText>@feature.Title</MudText>
</MudStack>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => ShowFeatureDetails(feature))">
جزئیات
</MudButton>
</MudStack>
</MudListItem>
@if (feature != _features.Last())
{
<MudDivider />
}
}
</MudList>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.CheckCircle"
Href="@RouteConstants.Club.Membership">
همین حالا عضو شوید
StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack">
بازگشت به صفحه باشگاه
</MudButton>
</MudStack>
</MudContainer>
<!-- مدال جزئیات فیچر -->
<MudDialog @bind-Visible="_showDetailDialog" Options="@_dialogOptions">
<TitleContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Primary" />
<MudText Typo="Typo.h6">@_selectedFeature?.Title</MudText>
</MudStack>
</TitleContent>
<DialogContent>
@if (_selectedFeature != null)
{
<MudStack Spacing="3">
<!-- وضعیت -->
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudText Typo="Typo.subtitle2">وضعیت:</MudText>
@if (_selectedFeature.IsEnabled)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">فعال</MudChip>
}
else
{
<MudChip T="string" Color="Color.Default" Size="Size.Small">غیرفعال</MudChip>
}
</MudStack>
<!-- توضیحات کوتاه -->
@if (!string.IsNullOrEmpty(_selectedFeature.Description))
{
<MudText Typo="Typo.body1">@_selectedFeature.Description</MudText>
}
<!-- تاریخ‌ها -->
<MudDivider />
<MudGrid>
<MudItem xs="6">
<MudText Typo="Typo.caption" Color="Color.Default">تاریخ ایجاد:</MudText>
<MudText Typo="Typo.body2">
@(_selectedFeature.CreatedAt?.ToLocalTime().ToString("yyyy/MM/dd HH:mm") ?? "-")
</MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.caption" Color="Color.Default">تاریخ فعال‌سازی:</MudText>
<MudText Typo="Typo.body2">
@(_selectedFeature.GrantedAt.HasValue && _selectedFeature.GrantedAt.Value > DateTime.MinValue
? _selectedFeature.GrantedAt.Value.ToLocalTime().ToString("yyyy/MM/dd HH:mm")
: "-")
</MudText>
</MudItem>
</MudGrid>
<!-- یادداشت (Notes) - محتوای اصلی مدال -->
@if (!string.IsNullOrEmpty(_selectedFeature.Notes))
{
<MudDivider />
<MudText Typo="Typo.subtitle2" Class="mb-2">توضیحات تکمیلی:</MudText>
<MudPaper Elevation="0" Class="pa-3 rounded" Style="background-color: var(--mud-palette-background-grey);">
@((MarkupString)_selectedFeature.Notes)
</MudPaper>
}
</MudStack>
}
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => _showDetailDialog = false)">
بستن
</MudButton>
</DialogActions>
</MudDialog>
@code {
private List<ClubFeatureDto>? _features;
private bool _isLoading = true;
private bool _showDetailDialog;
private ClubFeatureDto? _selectedFeature;
private DialogOptions _dialogOptions = new()
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseButton = true,
CloseOnEscapeKey = true
};
protected override async Task OnInitializedAsync()
{
await LoadFeatures();
}
private async Task LoadFeatures()
{
_isLoading = true;
try
{
_features = await ClubConfigService.GetClubFeaturesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error loading features: {ex.Message}");
_features = new List<ClubFeatureDto>();
}
finally
{
_isLoading = false;
}
}
private void ShowFeatureDetails(ClubFeatureDto feature)
{
_selectedFeature = feature;
_showDetailDialog = true;
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -7,14 +7,16 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">عضویت باشگاه مشتریان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" />
@* ── G6: راهنمای قرارداد باشگاه (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Gavel">
قرارداد باشگاه مشتریان فقط یک‌بار امضا می‌شود و با هر بار خرید پکیج جدید نیازی به امضای مجدد نیست.
</MudAlert>
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت اطلاعات عضویت..." />
}
else if (_membership is not null)
{
@@ -31,7 +33,7 @@
@if (_membership.IsActive)
{
<MudAlert Severity="Severity.Success" Variant="Variant.Outlined">
<MudText>عضویت شما در باشگاه فعال است! از مزایای باشگاه مشتریان استفاده کنید.</MudText>
<MudText>عضویت شما در باشگاه مشتریان فعال است و هم اکنون میتوانید از مزایای باشگاه مشتریان استفاده کنید.</MudText>
</MudAlert>
@if (_membership.DaysRemaining.HasValue)
@@ -63,48 +65,33 @@
<!-- مزایای باشگاه -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">مزایای عضویت باشگاه</MudText>
<MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background-color: var(--mud-palette-primary-lighten);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Discount" Color="Color.Primary" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1" Color="Color.Primary">تخفیف ویژه</MudText>
<MudText Typo="Typo.caption">تا 30% تخفیف</MudText>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background-color: var(--mud-palette-secondary-lighten);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Secondary" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1" Color="Color.Secondary">امتیاز خرید</MudText>
<MudText Typo="Typo.caption">برای هر خرید</MudText>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background-color: var(--mud-palette-success-lighten);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.LocalShipping" Color="Color.Success" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1" Color="Color.Success">ارسال رایگان</MudText>
<MudText Typo="Typo.caption">برای سفارشات بالای 500 هزار تومان</MudText>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
<MudText Typo="Typo.caption" Color="Color.Default">شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری</MudText>
</MudStack>
</MudStack>
<MudDivider />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Primary" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1">عضویت در باشگاه مشتریان</MudText>
<MudText Typo="Typo.caption" Color="Color.Default"> دریافت پاداش وفاداری</MudText>
</MudStack>
</MudStack>
<MudDivider />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
<MudIcon Icon="@Icons.Material.Filled.GroupAdd" Color="Color.Secondary" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1">معرفی مشتریان جدید</MudText>
<MudText Typo="Typo.caption" Color="Color.Default">توسعه باشگاه مشتریان</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
<!-- فعال‌سازی یا تمدید عضویت -->
@if (!_membership.IsActive || (_membership.DaysRemaining.HasValue && _membership.DaysRemaining.Value <= 30))
{
<ActivationSection OnActivationSuccess="HandleActivationSuccess" />
}
<!-- دکمه مشاهده ویژگی‌های بیشتر -->
<MudButton Variant="Variant.Outlined"
@@ -120,7 +107,7 @@
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
خطا در دریافت اطلاعات عضویت. لطفا دوباره تلاش کنید.
</MudAlert>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadMembershipAsync">تلاش مجدد</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadDataAsync">تلاش مجدد</MudButton>
}
</MudStack>
</MudContainer>
@@ -7,23 +7,33 @@ namespace FrontOffice.Main.Pages.Club;
public partial class MembershipPage : ComponentBase
{
[Inject] private ClubMembershipService ClubService { get; set; } = default!;
[Inject] private ClubConfigurationService ConfigService { get; set; } = default!;
private ClubMembershipDto? _membership;
private ClubConfigDto? _clubConfig;
private bool _isLoading = true;
private bool _hasError = false;
protected override async Task OnInitializedAsync()
{
await LoadMembershipAsync();
await LoadDataAsync();
}
private async Task LoadMembershipAsync()
private async Task LoadDataAsync()
{
try
{
_isLoading = true;
_hasError = false;
_membership = await ClubService.GetMyMembershipAsync();
// بارگذاری همزمان عضویت و تنظیمات
var membershipTask = ClubService.GetMyMembershipAsync();
var configTask = ConfigService.GetClubConfigurationAsync();
await Task.WhenAll(membershipTask, configTask);
_membership = await membershipTask;
_clubConfig = await configTask;
}
catch (Exception ex)
{
@@ -38,7 +48,7 @@ public partial class MembershipPage : ComponentBase
private async Task HandleActivationSuccess()
{
await LoadMembershipAsync();
await LoadDataAsync();
Snackbar.Add("عضویت باشگاه با موفقیت فعال شد!", Severity.Success);
}
@@ -1,111 +1,170 @@
@attribute [Route(RouteConstants.Commission.Dashboard)]
@attribute [Route(RouteConstants.Commission.History)]
@using FrontOffice.Main.Utilities
@using FrontOffice.Main.Shared
@using MudBlazor
<PageTitle>داشبورد کمیسیون</PageTitle>
<PageTitle>پاداش‌های من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">داشبورد کمیسیون</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="پاداش‌های من" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else
{
<!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.End">
<MudNumericField @bind-Value="_filterWeekNumber"
Label="شماره هفته"
Variant="Variant.Outlined"
Min="1"
Clearable="true"
Style="max-width: 150px;" />
<MudSelect T="string" @bind-Value="_filterStatus" Label="وضعیت" Variant="Variant.Outlined" Style="max-width: 200px;">
@* ── G5: توضیح محاسبه پاداش (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Info">
پاداش هفتگی بر اساس هر پکیج جداگانه محاسبه می‌شود. هر پکیج Pool پورسانت مستقل دارد و Carryover بر اساس پکیج زیرمجموعه‌هاست.
</MudAlert>
<!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="3">
<WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته"
Placeholder="جستجوی هفته..."
Variant="Variant.Outlined"
Dense="false"
Clearable="true"
OnlyActive="false" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="string" @bind-Value="_filterStatus" Label="وضعیت" Variant="Variant.Outlined">
<MudSelectItem T="string" Value="@string.Empty">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("Created")">ایجاد شده</MudSelectItem>
<MudSelectItem T="string" Value="@("Pending")">در انتظار</MudSelectItem>
<MudSelectItem T="string" Value="@("Paid")">پرداخت شده</MudSelectItem>
<MudSelectItem T="string" Value="@("WithdrawalRequested")">درخواست برداشت</MudSelectItem>
<MudSelectItem T="string" Value="@("Withdrawn")">برداشت شده</MudSelectItem>
<MudSelectItem T="string" Value="@("PaymentFailed")">شکست خورده</MudSelectItem>
<MudSelectItem T="string" Value="@("Cancelled")">لغو شده</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList">
اعمال فیلتر
</MudButton>
<MudButton Variant="Variant.Outlined" OnClick="ClearFiltersAsync" StartIcon="@Icons.Material.Filled.Clear">
پاک کردن
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudStack Row="true" Spacing="2" Style="height: 100%;" AlignItems="AlignItems.End">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList" FullWidth="true">
اعمال فیلتر
</MudButton>
<MudButton Variant="Variant.Outlined" OnClick="ClearFiltersAsync" StartIcon="@Icons.Material.Filled.Clear" FullWidth="true">
پاک کردن
</MudButton>
</MudStack>
</MudItem>
</MudGrid>
</MudPaper>
<!-- لیست کمیسیون‌ها -->
@if (_isLoading)
{
<LoadingState Message="در حال دریافت پاداش‌ها..." />
}
else
{
<!-- آمار خلاصه -->
@if (_payouts.Any())
{
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="true">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>تعداد تعادل</MudTh>
<MudTh>مبلغ کل</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>عملیات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudChip T="string" Color="Color.Default" Size="Size.Small">@context.WeekLabel</MudChip>
</MudTd>
<MudTd>@context.BalancesEarned</MudTd>
<MudTd>
<MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText>
</MudTd>
<MudTd>
<MudChip T="string" Color="@GetStatusColor(context.StatusBadgeColor)" Size="Size.Small" Variant="Variant.Outlined">
@context.Status
</MudChip>
</MudTd>
<MudTd>@context.DatePersian</MudTd>
<MudTd>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.Info"
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekNumber}")">
جزئیات
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
<MudGrid Spacing="2">
<MudItem xs="6" sm="4">
<MudPaper Elevation="2" Class="pa-3 rounded-lg text-center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">کل پاداش</MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@GetTotalEarnings()</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="4">
<MudPaper Elevation="2" Class="pa-3 rounded-lg text-center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">تعداد</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary">@_totalCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-3 rounded-lg text-center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">میانگین هفتگی</MudText>
<MudText Typo="Typo.h6" Color="Color.Info">@GetAverageWeekly()</MudText>
</MudPaper>
</MudItem>
</MudGrid>
}
<!-- لیست پاداش‌ها -->
@if (_payouts.Any())
{
<!-- جدول - دسکتاپ -->
<MudHidden Breakpoint="Breakpoint.SmAndDown">
<MudPaper Elevation="2" Class="pa-0 rounded-lg">
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="true" FixedHeader="true" Height="400px">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>امتیاز</MudTh>
<MudTh>مبلغ (تومان)</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>جزئیات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="هفته">
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip>
</MudTd>
<MudTd DataLabel="پکیج">
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.PackageTitle) ? "-" : context.PackageTitle)
</MudChip>
</MudTd>
<MudTd DataLabel="امتیاز">@context.BalancesEarned</MudTd>
<MudTd DataLabel="مبلغ">
<MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText>
</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string" Color="@GetStatusColor(context.StatusColor)" Size="Size.Small">@context.StatusText</MudChip>
</MudTd>
<MudTd DataLabel="تاریخ">@context.DatePersian</MudTd>
<MudTd DataLabel="جزئیات">
<MudIconButton Size="Size.Small"
Icon="@Icons.Material.Filled.Visibility"
Color="Color.Info"
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekDefinitionId}")" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<!-- کارت‌ها - موبایل -->
<MudHidden Breakpoint="Breakpoint.SmAndDown" Invert="true">
<MudStack Spacing="2">
@foreach (var payout in _payouts)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudChip T="string" Color="Color.Default" Size="Size.Small">@payout.WeekLabel</MudChip>
<MudChip T="string" Color="@GetStatusColor(payout.StatusBadgeColor)" Size="Size.Small" Variant="Variant.Outlined">
@payout.Status
</MudChip>
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@payout.WeekDisplayName</MudChip>
<MudChip T="string" Color="@GetStatusColor(payout.StatusColor)" Size="Size.Small">@payout.StatusText</MudChip>
</MudStack>
@if (!string.IsNullOrEmpty(payout.PackageTitle))
{
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">@payout.PackageTitle</MudChip>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">امتیاز: @payout.BalancesEarned</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@payout.DatePersian</MudText>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Info"
EndIcon="@Icons.Material.Filled.ChevronLeft"
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={payout.WeekDefinitionId}")">
جزئیات
</MudButton>
</MudStack>
<MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">تعداد تعادل: @payout.BalancesEarned</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@payout.DatePersian</MudText>
<MudButton Size="Size.Small"
Variant="Variant.Outlined"
Color="Color.Info"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Info"
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={payout.WeekNumber}")">
مشاهده جزئیات هفته
</MudButton>
</MudStack>
</MudPaper>
}
@@ -113,37 +172,27 @@
</MudHidden>
<!-- Pagination -->
<MudPaper Elevation="0" Class="pa-3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">کل: @_totalCount کمیسیون</MudText>
<MudPagination Count="@GetTotalPages()" Selected="_pageNumber" SelectedChanged="OnPageChangedAsync" Color="Color.Primary" />
<MudPaper Elevation="0" Class="pa-2">
<MudStack Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
<MudPagination Count="@GetTotalPages()" Selected="_pageNumber" SelectedChanged="OnPageChangedAsync" Color="Color.Primary" Size="Size.Small" />
</MudStack>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
کمیسیونی برای نمایش وجود ندارد.
پاداشی برای نمایش وجود ندارد.
</MudAlert>
}
<!-- دکمه‌های بیشتر -->
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.History"
Href="@RouteConstants.Commission.History"
FullWidth="true">
تاریخچه کامل
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.BarChart"
Href="@RouteConstants.Commission.WeeklyBalance"
FullWidth="true">
تعادل هفتگی
</MudButton>
</MudStack>
<!-- دکمه گزارش هفتگی -->
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.BarChart"
Href="@RouteConstants.Commission.WeeklyBalance"
FullWidth="true">
مشاهده گزارش هفتگی
</MudButton>
}
</MudStack>
</MudContainer>
@@ -1,3 +1,4 @@
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -7,38 +8,59 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class CommissionDashboardPage : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
private List<CommissionPayoutDto> _payouts = new();
private List<PackageDto> _packages = new();
private int _totalCount;
private long _totalAmount;
private int _pageNumber = 1;
private int _pageSize = 10;
private bool _isLoading = true;
private int? _filterWeekNumber;
private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition;
private string _filterStatus = string.Empty;
private long? _filterPackageId;
protected override async Task OnInitializedAsync()
{
await LoadPackagesAsync();
await LoadPayoutsAsync();
}
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new List<PackageDto>();
}
}
private async Task LoadPayoutsAsync()
{
try
{
_isLoading = true;
var weekDefinitionId = _selectedWeekDefinition?.Id;
var result = await CommissionService.GetMyCommissionPayoutsAsync(
_filterWeekNumber,
weekDefinitionId,
_filterStatus,
_pageNumber,
_pageSize
_pageSize,
_filterPackageId
);
_payouts = result.Payouts;
_totalCount = result.TotalCount;
_totalAmount = _payouts.Sum(p => p.TotalAmount);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در دریافت کمیسیون‌ها: {ex.Message}", Severity.Error);
Snackbar.Add($"خطا در دریافت پاداش‌ها: {ex.Message}", Severity.Error);
}
finally
{
@@ -54,8 +76,9 @@ public partial class CommissionDashboardPage : ComponentBase
private async Task ClearFiltersAsync()
{
_filterWeekNumber = null;
_selectedWeekDefinition = null;
_filterStatus = string.Empty;
_filterPackageId = null;
_pageNumber = 1;
await LoadPayoutsAsync();
}
@@ -73,6 +96,19 @@ public partial class CommissionDashboardPage : ComponentBase
return (int)Math.Ceiling(_totalCount / (double)_pageSize);
}
private string GetTotalEarnings()
{
return $"{_totalAmount:N0} تومان";
}
private string GetAverageWeekly()
{
if (_totalCount == 0)
return "0 تومان";
var avg = _totalAmount / _totalCount;
return $"{avg:N0} تومان";
}
private Color GetStatusColor(string statusColor) => statusColor.ToLower() switch
{
"success" => Color.Success,
@@ -1,91 +0,0 @@
@attribute [Route(RouteConstants.Commission.History)]
@using FrontOffice.Main.Utilities
@using MudBlazor
<PageTitle>تاریخچه کمیسیون</PageTitle>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تاریخچه کامل کمیسیون</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Commission.Dashboard">بازگشت</MudButton>
</MudStack>
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_payouts.Any())
{
<!-- آمار خلاصه -->
<MudGrid Spacing="2">
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">کل کمیسیون دریافتی</MudText>
<MudText Typo="Typo.h5" Color="Color.Success">@GetTotalEarnings()</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">تعداد هفته‌ها</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_payouts.Count</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">میانگین هفتگی</MudText>
<MudText Typo="Typo.h5" Color="Color.Info">@GetAverageWeekly()</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<!-- جدول کامل -->
<MudPaper Elevation="2" Class="pa-0 rounded-lg">
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="false" FixedHeader="true" Height="600px">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>تعادل‌ها</MudTh>
<MudTh>مبلغ (تومان)</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>جزئیات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="هفته">
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekLabel</MudChip>
</MudTd>
<MudTd DataLabel="تعادل‌ها">
<MudText>@context.BalancesEarned</MudText>
</MudTd>
<MudTd DataLabel="مبلغ">
<MudText Color="Color.Success" Typo="Typo.body1"><strong>@context.AmountFormatted</strong></MudText>
</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string" Color="@GetStatusColor(context.StatusBadgeColor)" Size="Size.Small">
@context.Status
</MudChip>
</MudTd>
<MudTd DataLabel="تاریخ">
<MudText Typo="Typo.caption">@context.DatePersian</MudText>
</MudTd>
<MudTd DataLabel="جزئیات">
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.Visibility"
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekNumber}")">
مشاهده
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Filled">
هنوز کمیسیونی دریافت نکرده‌اید.
</MudAlert>
}
</MudStack>
</MudContainer>
@@ -1,65 +0,0 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Commission;
public partial class CommissionHistoryPage : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
private List<CommissionPayoutDto> _payouts = new();
private bool _isLoading = true;
protected override async Task OnInitializedAsync()
{
await LoadAllPayoutsAsync();
}
private async Task LoadAllPayoutsAsync()
{
try
{
_isLoading = true;
// Load all payouts (large page size to get everything)
var result = await CommissionService.GetMyCommissionPayoutsAsync(
null,
string.Empty,
1,
1000
);
_payouts = result.Payouts;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در دریافت تاریخچه: {ex.Message}", Severity.Error);
}
finally
{
_isLoading = false;
}
}
private string GetTotalEarnings()
{
var total = _payouts.Sum(p => p.TotalAmount);
return $"{total:N0} تومان";
}
private string GetAverageWeekly()
{
if (!_payouts.Any())
return "0 تومان";
var avg = _payouts.Average(p => p.TotalAmount);
return $"{avg:N0} تومان";
}
private Color GetStatusColor(string statusColor) => statusColor.ToLower() switch
{
"success" => Color.Success,
"warning" => Color.Warning,
"error" => Color.Error,
"info" => Color.Info,
_ => Color.Default
};
}
@@ -1,147 +1,151 @@
@attribute [Route(RouteConstants.Commission.WeeklyBalance)]
@using FrontOffice.Main.Utilities
@using FrontOffice.Main.Shared
@using MudBlazor
<PageTitle>تعادل هفتگی</PageTitle>
<PageTitle>گزارش هفتگی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تعادل هفتگی کمیسیون</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Commission.Dashboard">بازگشت</MudButton>
</MudStack>
<PageHeader Title="گزارش هفتگی پاداش" BackHref="@RouteConstants.Commission.Dashboard" />
<!-- انتخاب هفته -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.End">
<MudNumericField @bind-Value="_selectedWeekNumber"
Label="شماره هفته"
Variant="Variant.Outlined"
Min="1"
HelperText="هفته مورد نظر را انتخاب کنید (خالی = هفته جاری)"
Style="max-width: 200px;" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadWeeklyBalanceAsync" StartIcon="@Icons.Material.Filled.Search">
مشاهده
</MudButton>
<MudButton Variant="Variant.Outlined" OnClick="LoadCurrentWeekAsync" StartIcon="@Icons.Material.Filled.Today">
هفته جاری
</MudButton>
</MudStack>
<MudGrid Spacing="2">
<MudItem xs="8" sm="4" md="4">
<WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته"
Placeholder="هفته را جستجو یا انتخاب کنید"
Variant="Variant.Outlined"
Dense="false"
OnlyActive="true" />
</MudItem>
<MudItem xs="4" sm="4" md="4">
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="4" sm="3" md="3" Class="pt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="LoadWeeklyBalanceAsync"
StartIcon="@Icons.Material.Filled.Search"
Disabled="@(_selectedWeekDefinition == null)"
FullWidth="true"
Style="height: 50px;">
جستجو
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت گزارش..." />
}
else if (_weeklyBalance is not null)
{
<!-- اطلاعات هفته -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.h6">هفته @_weeklyBalance.WeekNumber - @_weeklyBalance.WeekLabel</MudText>
<MudDivider />
<MudGrid Spacing="2">
<MudItem xs="12" sm="6">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">تاریخ شروع:</MudText>
<MudText Typo="Typo.body1"><strong>@_weeklyBalance.StartDatePersian</strong></MudText>
<!-- اطلاعات هفته - کامپکت -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="1">
<MudItem xs="12" sm="4">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.DateRange" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2"><strong>@_weeklyBalance.WeekDisplayName</strong></MudText>
</MudStack>
</MudItem>
<MudItem xs="6" sm="4">
<MudText Typo="Typo.caption" Class="mud-text-secondary">شروع: <strong>@_weeklyBalance.StartDatePersian</strong></MudText>
</MudItem>
<MudItem xs="6" sm="4">
<MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText>
</MudItem>
@if (!string.IsNullOrEmpty(_weeklyBalance.PackageTitle))
{
<MudItem xs="12" sm="12">
<MudChip T="string" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.Inventory2">@_weeklyBalance.PackageTitle</MudChip>
</MudItem>
<MudItem xs="12" sm="6">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">تاریخ پایان:</MudText>
<MudText Typo="Typo.body1"><strong>@_weeklyBalance.EndDatePersian</strong></MudText>
</MudItem>
</MudGrid>
</MudStack>
}
</MudGrid>
</MudPaper>
<!-- تعادل چپ و راست -->
<!-- امتیاز فروش تیم اول و دوم - کارت‌های بزرگ -->
<MudGrid Spacing="2">
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Info">شاخه چپ</MudText>
</MudStack>
<MudText Typo="Typo.h4" Color="Color.Info">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Info"
Value="@GetLeftPercentage()"
Size="Size.Large"
Class="rounded-lg" />
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-success-soft stat-card-ds">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Color="Color.Success">تیم دوم</MudText>
<MudText Typo="Typo.h5" Color="Color.Success" Class="dash-stat-value">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Success"
Value="@GetRightPercentage()"
Size="Size.Medium"
Class="rounded-lg mt-2"
Style="width: 100%;" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@GetLeftPercentage().ToString("F1")% از کل تعادل
@GetRightPercentage().ToString("F1")% از کل
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-info-soft stat-card-ds">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" Size="Size.Large"/>
<MudText Typo="Typo.subtitle2" Color="Color.Info">تیم اول</MudText>
<MudText Typo="Typo.h5" Color="Color.Info" Class="dash-stat-value">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Info"
Value="@GetLeftPercentage()"
Size="Size.Medium"
Class="rounded-lg mt-2"
Style="width: 100%;"/>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@GetLeftPercentage().ToString("F1")% از کل
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.h6" Color="Color.Success">شاخه راست</MudText>
</MudStack>
<MudText Typo="Typo.h4" Color="Color.Success">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Success"
Value="@GetRightPercentage()"
Size="Size.Large"
Class="rounded-lg" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@GetRightPercentage().ToString("F1")% از کل تعادل
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<!-- محاسبات کمیسیون -->
<!-- محاسبات پاداش - فشرده‌تر -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">محاسبات کمیسیون</MudText>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body1">تعادل چپ:</MudText>
<MudText Typo="Typo.body1" Color="Color.Info"><strong>@_weeklyBalance.LeftBalanceFormatted</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body1">تعادل راست:</MudText>
<MudText Typo="Typo.body1" Color="Color.Success"><strong>@_weeklyBalance.RightBalanceFormatted</strong></MudText>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body1">حداقل تعادل (پایه کمیسیون):</MudText>
<MudText Typo="Typo.h6" Color="Color.Warning"><strong>@_weeklyBalance.MinBalanceFormatted</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body1">تعداد تعادل (دفعات):</MudText>
<MudText Typo="Typo.h6" Color="Color.Secondary"><strong>@_weeklyBalance.BalanceCount</strong></MudText>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6">کمیسیون محاسبه شده:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary"><strong>@_weeklyBalance.CalculatedCommissionFormatted</strong></MudText>
</MudStack>
@if (_weeklyBalance.LeftCarryover > 0 || _weeklyBalance.RightCarryover > 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
<MudText Typo="Typo.body2">
<strong>Carryover (انتقال به هفته بعد):</strong><br />
چپ: @_weeklyBalance.LeftCarryoverFormatted | راست: @_weeklyBalance.RightCarryoverFormatted
</MudText>
</MudAlert>
}
</MudStack>
</MudPaper>
<!-- نمودار مقایسه -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">نمودار مقایسه</MudText>
<MudChart ChartType="ChartType.Bar"
Width="100%"
Height="300px"
InputData="@(new double[] { _weeklyBalance.LeftBalance, _weeklyBalance.RightBalance, _weeklyBalance.MinBalance })"
InputLabels="@(new[] { "چپ", "راست", "حداقل" })"
ChartOptions="@_chartOptions" />
<MudText Typo="Typo.subtitle1" Class="mb-2 fw-bold">محاسبات پاداش</MudText>
<MudSimpleTable Dense="true" Hover="true" Style="background: transparent;">
<tbody>
<tr>
<td><MudText Typo="Typo.body2">اعضای جدید تیم اول:</MudText></td>
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Info"><strong>@_weeklyBalance.LeftNewMembersFormatted</strong></MudText></td>
</tr>
<tr>
<td><MudText Typo="Typo.body2">اعضای جدید تیم دوم:</MudText></td>
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Success"><strong>@_weeklyBalance.RightNewMembersFormatted</strong></MudText></td>
</tr>
<tr>
<td><MudText Typo="Typo.body2">انتقال از هفته قبل (تیم اول):</MudText></td>
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Info"><strong>@_weeklyBalance.LeftCarryoverFormatted</strong></MudText></td>
</tr>
<tr>
<td><MudText Typo="Typo.body2">انتقال از هفته قبل (تیم دوم):</MudText></td>
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Success"><strong>@_weeklyBalance.RightCarryoverFormatted</strong></MudText></td>
</tr>
<tr>
<td><MudText Typo="Typo.body2" Color="Color.Error"> امتیاز فروش کل (پایه):</MudText></td>
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Error"><strong>@_weeklyBalance.MinBalanceFormatted</strong></MudText></td>
</tr>
@* <tr style="background-color: var(--mud-palette-primary-lighten);"> *@
@* <td><MudText Typo="Typo.subtitle1"><strong>کمیسیون محاسبه شده:</strong></MudText></td> *@
@* <td class="text-end"><MudText Typo="Typo.h6" Color="Color.Primary"><strong>@_weeklyBalance.CalculatedCommissionFormatted</strong></MudText></td> *@
@* </tr> *@
</tbody>
</MudSimpleTable>
</MudPaper>
}
else if (_hasError)
@@ -1,4 +1,5 @@
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Shared;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -7,13 +8,18 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class WeeklyBalancePage : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private ISnackbar SnackbarService { get; set; } = default!;
[Parameter]
[SupplyParameterFromQuery(Name = "week")]
public int? QueryWeekNumber { get; set; }
public string? QueryWeekNumber { get; set; }
private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition;
private WeeklyBalanceDto? _weeklyBalance;
private int? _selectedWeekNumber;
private List<PackageDto> _packages = new();
private long? _filterPackageId;
private bool _isLoading = true;
private bool _hasError = false;
@@ -27,12 +33,55 @@ public partial class WeeklyBalancePage : ComponentBase
}
};
protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (QueryWeekNumber.HasValue)
_selectedWeekNumber = QueryWeekNumber;
if (firstRender)
{
await LoadPackagesAsync();
await LoadWeeklyBalanceAsync();
// Wait for WeekSelector to initialize and load its cached weeks
if (_weekSelector != null)
{
await _weekSelector.EnsureLoadedAsync();
// Handle query parameter ?week=7 (WeekDefinitionId)
if (!string.IsNullOrEmpty(QueryWeekNumber))
{
// Try to parse as WeekDefinitionId first (most common from dashboard links)
if (long.TryParse(QueryWeekNumber, out long id))
{
_selectedWeekDefinition = _weekSelector.FindById(id);
// Fallback: try as WeekOrder if not found by Id
if (_selectedWeekDefinition == null && int.TryParse(QueryWeekNumber, out int weekOrder))
{
_selectedWeekDefinition = _weekSelector.FindByWeekOrder(weekOrder);
}
// If still not found in cache, create a temporary definition with just the Id
// so we can still query the balance from BFF
if (_selectedWeekDefinition == null)
{
_selectedWeekDefinition = new WeekDefinitionDto { Id = id, DisplayName = $"هفته {id}" };
}
}
}
// If no week selected and no query param, select current week
if (_selectedWeekDefinition == null)
{
_selectedWeekDefinition = _weekSelector.GetCurrentWeek();
}
await LoadWeeklyBalanceAsync();
StateHasChanged();
}
}
}
protected override void OnInitialized()
{
_isLoading = true;
}
private async Task LoadWeeklyBalanceAsync()
@@ -41,12 +90,27 @@ public partial class WeeklyBalancePage : ComponentBase
{
_isLoading = true;
_hasError = false;
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(_selectedWeekNumber);
long? weekDefinitionId = _selectedWeekDefinition?.Id;
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId, _filterPackageId);
// Update week display name from selected definition if available
if (_selectedWeekDefinition != null && _weeklyBalance != null)
{
_weeklyBalance.WeekDisplayName = _selectedWeekDefinition.DisplayName;
return;
}
_weeklyBalance = new WeeklyBalanceDto()
{
};
}
catch (Exception ex)
{
_hasError = true;
Snackbar.Add($"خطا در دریافت تعادل: {ex.Message}", Severity.Error);
SnackbarService.Add($"خطا در دریافت تعادل: {ex.Message}", Severity.Error);
}
finally
{
@@ -54,9 +118,25 @@ public partial class WeeklyBalancePage : ComponentBase
}
}
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new();
}
}
private async Task LoadCurrentWeekAsync()
{
_selectedWeekNumber = null;
if (_weekSelector != null)
{
await _weekSelector.SelectCurrentWeekAsync();
}
_selectedWeekDefinition = _weekSelector?.GetCurrentWeek();
await LoadWeeklyBalanceAsync();
}
+23 -23
View File
@@ -1,4 +1,5 @@
@attribute [Route(RouteConstants.Contact.Index)]
@inject SitePageSettingsService PageSettingsService
<PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle>
@@ -8,10 +9,10 @@
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ارتباط با ما</MudChip>
<MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3">
آماده شنیدن صدای شما هستیم
@(_page?.HeroTitle ?? "آماده شنیدن صدای شما هستیم")
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" Style="max-width:600px">
سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.
@(_page?.HeroSubtitle ?? "سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.")
</MudText>
</MudStack>
</MudContainer>
@@ -25,7 +26,7 @@
<MudGrid Spacing="6">
<!-- Contact Form -->
<MudItem xs="12" lg="8">
<MudPaper Elevation="3" Class="pa-6">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-6">فرم تماس</MudText>
<MudForm @ref="_form" Model="_contactForm">
@@ -72,12 +73,12 @@
Variant="Variant.Outlined"
Required="true"
RequiredError="انتخاب موضوع الزامی است.">
<MudSelectItem Value="@("general")">عمومی</MudSelectItem>
<MudSelectItem Value="@("support")">پشتیبانی فنی</MudSelectItem>
<MudSelectItem Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem>
<MudSelectItem Value="@("partnership")">شریک تجاری</MudSelectItem>
<MudSelectItem Value="@("complaint")">شکایت</MudSelectItem>
<MudSelectItem Value="@("suggestion")">پیشنهاد</MudSelectItem>
<MudSelectItem T="string" Value="@("general")">عمومی</MudSelectItem>
<MudSelectItem T="string" Value="@("support")">پشتیبانی فنی</MudSelectItem>
<MudSelectItem T="string" Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem>
<MudSelectItem T="string" Value="@("partnership")">شریک تجاری</MudSelectItem>
<MudSelectItem T="string" Value="@("complaint")">شکایت</MudSelectItem>
<MudSelectItem T="string" Value="@("suggestion")">پیشنهاد</MudSelectItem>
</MudSelect>
</MudItem>
@@ -123,7 +124,7 @@
<MudItem xs="12" lg="4">
<MudStack Spacing="4">
<!-- Contact Details -->
<MudPaper Elevation="3" Class="pa-6">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">اطلاعات تماس</MudText>
<MudStack Spacing="4">
@@ -132,7 +133,7 @@
<div>
<MudText Typo="Typo.body2" >آدرس</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText>
</div>
</MudStack>
@@ -141,7 +142,7 @@
<MudIcon Icon="@Icons.Material.Filled.Phone" Color="Color.Success" Size="Size.Large" />
<div>
<MudText Typo="Typo.body2" >تلفن</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">026-34233563</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Phone ?? "026-34233563")</MudText>
</div>
</MudStack>
@@ -149,7 +150,7 @@
<MudIcon Icon="@Icons.Material.Filled.Email" Color="Color.Info" Size="Size.Large" />
<div>
<MudText Typo="Typo.body2" >ایمیل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">info@kbs1.co</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Email ?? "info@kbs1.co")</MudText>
</div>
</MudStack>
@@ -158,8 +159,7 @@
<div>
<MudText Typo="Typo.body2" >ساعات کاری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
شنبه تا پنج‌شنبه<br />
۹ صبح تا ۶ عصر
@((MarkupString)(_settings?.WorkingHours ?? "شنبه تا پنج‌شنبه<br />۹ صبح تا ۶ عصر"))
</MudText>
</div>
</MudStack>
@@ -167,14 +167,14 @@
</MudPaper>
<!-- Social Media -->
<MudPaper Elevation="3" Class="pa-6">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">شبکه‌های اجتماعی</MudText>
<MudStack Spacing="3">
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Telegram"
Href="https://t.me/kbs1"
Href="@(_settings?.TelegramUrl ?? "https://t.me/kbs1")"
Target="_blank"
FullWidth="true">
تلگرام
@@ -183,7 +183,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Instagram"
Href="https://instagram.com/kbs1"
Href="@(_settings?.InstagramUrl ?? "https://instagram.com/kbs1")"
Target="_blank"
FullWidth="true">
اینستاگرام
@@ -192,7 +192,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.LinkedIn"
Href="https://linkedin.com/company/kbs1"
Href="@(_settings?.LinkedinUrl ?? "https://linkedin.com/company/kbs1")"
Target="_blank"
FullWidth="true">
لینکدین
@@ -201,7 +201,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.WhatsApp"
Href="https://wa.me/989123456789"
Href="@(_settings?.WhatsappUrl ?? "https://wa.me/989123456789")"
Target="_blank"
FullWidth="true">
واتس‌اپ
@@ -219,14 +219,14 @@
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" Class="mb-8">موقعیت مکانی</MudText>
<MudPaper Elevation="3" Class="pa-4 overflow-hidden">
<MudPaper Elevation="2" Class="pa-4 overflow-hidden">
<!-- Placeholder for Map -->
<div style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center;">
<div class="contact-map-placeholder" style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; border-radius: 12px;">
<MudStack AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.Map" Size="Size.Large" Style="color: white;" />
<MudText Typo="Typo.h6" Style="color: white;">نقشه موقعیت مکانی</MudText>
<MudText Typo="Typo.body2" Style="color: rgba(255,255,255,0.8);" Align="Align.Center">
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText>
</MudStack>
</div>
+38 -4
View File
@@ -1,8 +1,10 @@
using FluentValidation;
using FrontOffice.Main.Utilities;
using MudBlazor;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages;
public partial class Contact
{
private ContactForm _contactForm = new();
@@ -10,6 +12,27 @@ public partial class Contact
private bool _isSubmitting;
private readonly ContactFormValidator _contactFormValidator = new();
// Dynamic CMS data (new simplified system)
private PageSettingsDto? _page;
private ContactSettings? _settings;
protected override async Task OnInitializedAsync()
{
try
{
_page = await PageSettingsService.GetPageAsync("contact");
if (_page != null)
{
_settings = _page.GetSettings<ContactSettings>();
}
}
catch
{
// Fallback: all values remain null → hardcoded defaults will render
}
}
private async Task SubmitContactForm()
{
if (_form is null) return;
@@ -55,14 +78,12 @@ public partial class Contact
private void CallSupport()
{
// TODO: Initiate phone call or show phone number
Snackbar.Add("شماره تماس: ۰۲۱-۱۲۳۴۵۶۷۸", Severity.Info);
Snackbar.Add($"شماره تماس: {_settings?.Phone ?? "026-34233563"}", Severity.Info);
}
private void SendEmail()
{
// TODO: Open email client or redirect to email page
Snackbar.Add("ایمیل: info@kbs1.co", Severity.Info);
Snackbar.Add($"ایمیل: {_settings?.Email ?? "info@kbs1.co"}", Severity.Info);
}
public class ContactForm
@@ -87,4 +108,17 @@ public partial class Contact
RuleFor(x => x.Message).NotEmpty().MinimumLength(10).WithMessage("پیام باید حداقل ۱۰ کاراکتر باشد");
}
}
// DTO class for SettingsJson deserialization
private class ContactSettings
{
public string? Address { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? WorkingHours { get; set; }
public string? TelegramUrl { get; set; }
public string? InstagramUrl { get; set; }
public string? LinkedinUrl { get; set; }
public string? WhatsappUrl { get; set; }
}
}
@@ -0,0 +1,204 @@
@attribute [Route(RouteConstants.DiscountStore.Cart)]
<PageTitle>سبد خرید اعتباری</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سبد خرید اعتباری" BackHref="@RouteConstants.DiscountStore.Products" />
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید اعتباری شما خالی است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
بازگشت به فروشگاه اعتباری
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="DiscountCart.Items">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>سقف اعتبار</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>اعتبار</MudTh>
<MudTh>قیمت نهایی</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText>@context.Title</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
@context.MaxDiscountPercent% اعتبار
</MudChip>
</MudTd>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Remove" Color="Color.Default"
OnClick="@(() => DecrementQty(context.ProductId))" />
<MudText>@context.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Default"
OnClick="@(() => IncrementQty(context.ProductId))" />
</MudStack>
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(context.ProductId)" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var item in DiscountCart.Items)
{
<MudPaper Class="pa-3 rounded-lg w-100-mobile" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
@item.MaxDiscountPercent% اعتبار
</MudChip>
</MudStack>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Class="mud-text-secondary">@FormatPrice(item.UnitPrice) واحد</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton
Icon="@(item.Quantity <= 1 ? Icons.Material.Filled.Delete : Icons.Material.Filled.Remove)"
Color="Color.Default"
OnClick="@(() => DecrementQty(item.ProductId))" />
<MudText>@item.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add"
Color="Color.Default"
OnClick="@(() => IncrementQty(item.ProductId))" />
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
@if (item.DiscountAmount > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">اعتبار: @FormatPrice(item.DiscountAmount)-</MudText>
}
<MudText>جمع: @FormatPrice(item.FinalPrice)</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(item.ProductId)" />
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<!-- Summary -->
@if (DeviceDetector.IsMobile())
{
<MudStack Spacing="1" Class="mobile-actions-stack px-5">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@if (DiscountCart.TotalDiscount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Color="Color.Success">سقف اعتبار قابل اعمال:</MudText>
<MudText Typo="Typo.caption" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
}
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">مالیات ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">حداقل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVat) تومان</MudText>
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" Spacing="2" Class="mobile-actions-stack">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه خرید</MudButton>
</MudStack>
}
else
{
<MudPaper Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-background-grey);">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@if (DiscountCart.TotalDiscount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">سقف اعتبار قابل اعمال:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
}
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">حداقل مبلغ پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVat) تومان</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مبلغ دقیق پرداخت در مرحله تسویه مشخص خواهد شد.
</MudText>
</MudStack>
</MudPaper>
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه فرایند خرید</MudButton>
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Cart : IDisposable
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
}
private async Task IncrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity + 1);
}
private async Task DecrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
if (item.Quantity <= 1)
{
await DiscountCart.RemoveAsync(productId);
}
else
{
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity - 1);
}
}
private async Task Remove(long productId)
{
await DiscountCart.RemoveAsync(productId);
}
private void ProceedCheckout()
{
Navigation.NavigateTo(RouteConstants.DiscountStore.Checkout);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await VAT.LoadAsync();
StateHasChanged();
}
}
private long VatAmount => VAT.CalculateVAT(DiscountCart.FinalPrice);
private long TotalWithVat => DiscountCart.FinalPrice + VatAmount;
private static string FormatPrice(long price) => $"{price:N0} ";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
@@ -0,0 +1,145 @@
@using CMSMicroservice.Protobuf.Protos.UserAddress
@attribute [Route(RouteConstants.DiscountStore.Checkout)]
<PageTitle>تسویه حساب فروشگاه اعتباری</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تسویه حساب اعتباری" BackHref="@RouteConstants.DiscountStore.Cart" />
<MudGrid Spacing="3">
<!-- Left Column: Address + Payment Settings -->
<MudItem xs="12" md="8">
<!-- Address Selection -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
<MudText Typo="Typo.h6" Class="mb-2">انتخاب آدرس</MudText>
@if (_loadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
<MudStack Spacing="1">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack>
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
</MudStack>
}
</MudPaper>
<!-- Notes -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">توضیحات سفارش</MudText>
<MudTextField T="string" @bind-Value="_notes" Lines="3"
Placeholder="توضیحات اختیاری..."
Variant="Variant.Outlined" />
</MudPaper>
</MudItem>
<!-- Right Column: Order Summary -->
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="position:sticky; top:80px;">
<MudText Typo="Typo.h6" Class="mb-2">خلاصه سفارش</MudText>
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید شما خالی است.</MudAlert>
}
else
{
<MudList T="string" Dense="true">
@foreach (var item in DiscountCart.Items)
{
<MudListItem T="string">
<MudListItemText>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="40" ImgHeight="40" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText Typo="Typo.subtitle2" Style="flex:1; margin:0 8px;">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@item.Quantity × @FormatPrice(item.UnitPrice)
</MudText>
@if (item.MaxDiscountPercent > 0)
{
<MudText Typo="Typo.caption" Color="Color.Error">@item.MaxDiscountPercent% اعتبار</MudText>
}
</MudStack>
</MudStack>
</MudListItemText>
</MudListItem>
}
</MudList>
<MudDivider Class="my-2" />
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">اعتبار از کیف اعتباری:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ پس از اعتبار:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(NetGatewayAmount) تومان</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage٪):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
</MudStack>
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(FinalGatewayAmount) تومان
</MudText>
</MudStack>
</MudStack>
<MudButton Disabled="@(!CanPlaceOrder || _placing)" Class="mt-3 w-100-mobile"
Variant="Variant.Filled" Color="Color.Primary"
OnClick="PlaceOrder" FullWidth="true"
StartIcon="@Icons.Material.Filled.CheckCircle">
@(_placing ? "در حال ثبت سفارش..." : "ثبت و پرداخت سفارش")
</MudButton>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
@@ -0,0 +1,129 @@
using Microsoft.AspNetCore.Components;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using MudBlazor;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Checkout
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _loadingAddresses;
private string? _notes;
private bool _placing;
/// <summary>مبلغ درگاه قبل از مالیات (جمع کل - اعتبار)</summary>
private long NetGatewayAmount => DiscountCart.TotalPrice - DiscountCart.TotalDiscount;
/// <summary>مالیات بر ارزش افزوده</summary>
private long VatAmount => VAT.CalculateVAT(NetGatewayAmount);
/// <summary>مبلغ نهایی قابل پرداخت (شامل VAT)</summary>
private long FinalGatewayAmount => NetGatewayAmount + VatAmount;
private bool CanPlaceOrder => DiscountCart.Items.Count > 0 && _selectedAddress is not null;
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
await VAT.LoadAsync();
await DiscountCart.EnsureInitializedAsync();
var userInfo = await AuthService.GetUserAuthInfo();
if (userInfo.HasAddress)
await LoadAddresses();
else
_addresses = new();
}
private async Task LoadAddresses()
{
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
}
finally
{
_loadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
{
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
return;
}
_placing = true;
try
{
// Always use 100% discount — send full price, server will apply max from wallet
var result = await DiscountOrderService.PlaceOrderAsync(
_selectedAddress.Id,
DiscountCart.TotalPrice,
_notes);
if (!result.Success)
{
Snackbar.Add(result.Message, Severity.Error);
return;
}
// If there's a gateway payment URL, redirect to it
if (!string.IsNullOrWhiteSpace(result.PaymentUrl) && result.GatewayAmount > 0)
{
Snackbar.Add("در حال انتقال به درگاه پرداخت...", Severity.Info);
await DiscountCart.ClearAsync();
Navigation.NavigateTo(result.PaymentUrl, forceLoad: true);
return;
}
// If fully paid via discount balance (no gateway needed)
await DiscountCart.ClearAsync();
Snackbar.Add("سفارش با موفقیت ثبت شد!", Severity.Success);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{result.OrderId}");
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
}
finally
{
_placing = false;
}
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -0,0 +1,217 @@
@attribute [Route("/discount-store/order/{Id:long}")]
<PageTitle>جزئیات سفارش اعتباری</PageTitle>
@if (!string.IsNullOrEmpty(_paymentMessage))
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="pt-4">
<MudAlert Severity="@_paymentSeverity" Variant="Variant.Filled" Class="mb-2"
CloseIconClicked="() => _paymentMessage = null" ShowCloseIcon="true">
@_paymentMessage
</MudAlert>
</MudContainer>
}
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<LoadingState />
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="سفارش مورد نظر یافت نشد."
ActionText="بازگشت به لیست سفارش‌ها"
ActionHref="@RouteConstants.DiscountStore.Orders" />
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Header -->
<PageHeader Title="@($"سفارش #{_order.OrderNumber}")" BackHref="@RouteConstants.DiscountStore.Orders" />
<MudStack Row="true" Spacing="2" Class="flex-wrap mb-4">
<MudChip T="string" Color="@DiscountOrderService.GetPaymentStatusColor(_order.PaymentStatus)"
Variant="Variant.Filled">
@DiscountOrderService.GetPaymentStatusText(_order.PaymentStatus)
</MudChip>
<MudChip T="string" Color="@DiscountOrderService.GetDeliveryStatusColor(_order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(_order.DeliveryStatus)
</MudChip>
</MudStack>
@if (_order.PaymentStatus == 2)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined" Class="mb-4">
<MudText>پرداخت این سفارش ناموفق بود و سفارش منقضی شده است. موجودی رزرو شده آزاد شد.</MudText>
<MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-2"
Href="@RouteConstants.DiscountStore.Products">
بازگشت به فروشگاه و ثبت سفارش جدید
</MudButton>
</MudAlert>
}
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
تاریخ ثبت: @FormatDate(_order.Created)
</MudText>
@* ── Address ── *@
@if (_order.Address is not null)
{
<MudText Typo="Typo.body2">
<b>آدرس:</b> @_order.Address.Title — @_order.Address.Address
</MudText>
@if (!string.IsNullOrWhiteSpace(_order.Address.PostalCode))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @_order.Address.PostalCode</MudText>
}
}
<MudDivider Class="my-2" />
@* ── Desktop: Table ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudTable Items="_order.Items" Dense="true">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>سقف اعتبار</MudTh>
<MudTh>اعتبار</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Title</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">@context.MaxDiscountPercent%</MudChip>
}
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@* ── Mobile: Cards ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudStack Spacing="2">
@foreach (var item in _order.Items)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت واحد:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(item.UnitPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">تعداد:</MudText>
<MudText Typo="Typo.body2">@item.Count</MudText>
</MudStack>
@if (item.DiscountAmount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">اعتبار (@item.MaxDiscountPercent%):</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(item.DiscountAmount)-</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle2">قیمت نهایی:</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<MudDivider Class="my-2" />
@* ── Financial Summary ── *@
<MudStack Spacing="1" Class="pa-3" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudText Typo="Typo.h6" Class="mb-1">خلاصه مالی</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_order.TotalPrice)</MudText>
</MudStack>
@if (_order.DiscountBalanceUsed > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">از کیف اعتباری:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_order.DiscountBalanceUsed)-</MudText>
</MudStack>
}
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(OrderVatAmount)+</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(_order.GatewayAmount)
</MudText>
</MudStack>
</MudStack>
@* ── Tracking / Transaction ── *@
@if (!string.IsNullOrWhiteSpace(_order.TrackingCode))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">کد رهگیری:</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@_order.TrackingCode</MudText>
</MudStack>
}
@if (!string.IsNullOrWhiteSpace(_order.TransactionId))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه تراکنش:</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_order.TransactionId</MudText>
</MudStack>
}
@* ── Notes ── *@
@if (!string.IsNullOrWhiteSpace(_order.Notes))
{
<MudDivider Class="my-1" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
<b>توضیحات:</b> @_order.Notes
</MudText>
}
@if (!string.IsNullOrWhiteSpace(_order.AdminNotes))
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
<b>پیام مدیر:</b> @_order.AdminNotes
</MudAlert>
}
</MudStack>
</MudPaper>
</MudContainer>
}
@@ -0,0 +1,92 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class OrderDetail
{
[Parameter] public long Id { get; set; }
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentResult { get; set; }
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
private DiscountOrderDetail? _order;
private bool _loading = true;
private string? _paymentMessage;
private MudBlazor.Severity _paymentSeverity;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await VAT.LoadAsync();
StateHasChanged();
}
}
/// <summary>مبلغ قبل از مالیات (جمع کل - اعتبار)</summary>
private long PreVatGateway => _order is null ? 0 : (_order.TotalPrice - _order.DiscountBalanceUsed);
/// <summary>مالیات بر ارزش افزوده</summary>
private long OrderVatAmount => VAT.CalculateVAT(PreVatGateway);
protected override async Task OnInitializedAsync()
{
// نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه)
if (!string.IsNullOrEmpty(PaymentResult))
{
switch (PaymentResult.ToLower())
{
case "success":
_paymentMessage = "پرداخت با موفقیت انجام شد! سفارش شما ثبت نهایی شد.";
_paymentSeverity = MudBlazor.Severity.Success;
break;
case "failed":
_paymentMessage = "پرداخت ناموفق بود. مبلغ رزرو شده آزاد خواهد شد.";
_paymentSeverity = MudBlazor.Severity.Error;
break;
case "error":
_paymentMessage = "خطایی در پردازش پرداخت رخ داد. لطفاً با پشتیبانی تماس بگیرید.";
_paymentSeverity = MudBlazor.Severity.Warning;
break;
}
}
await LoadOrderAsync();
}
private async Task LoadOrderAsync()
{
_loading = true;
try
{
_order = await DiscountOrderService.GetOrderByIdAsync(Id);
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,130 @@
@attribute [Route(RouteConstants.DiscountStore.Orders)]
<PageTitle>سفارش‌های فروشگاه اعتباری</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سفارش‌های اعتباری" BackHref="@RouteConstants.DiscountStore.Products" />
@if (_loading)
{
<LoadingState />
}
else if (_orders.Count == 0)
{
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
Href="@RouteConstants.DiscountStore.Products">
مشاهده فروشگاه اعتباری
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="_orders">
<HeaderContent>
<MudTh>شماره سفارش</MudTh>
<MudTh>تعداد اقلام</MudTh>
<MudTh>مبلغ کل</MudTh>
<MudTh>از کیف اعتباری</MudTh>
<MudTh>درگاه</MudTh>
<MudTh>وضعیت پرداخت</MudTh>
<MudTh>وضعیت ارسال</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.OrderNumber</MudTd>
<MudTd>@context.ItemsCount</MudTd>
<MudTd>@FormatPrice(context.TotalPrice)</MudTd>
<MudTd>
@if (context.DiscountBalanceUsed > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountBalanceUsed)</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.GatewayAmount)</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetPaymentStatusColor(context.PaymentStatus)"
Variant="Variant.Filled">
@DiscountOrderService.GetPaymentStatusText(context.PaymentStatus)
</MudChip>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(context.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(context.DeliveryStatus)
</MudChip>
</MudTd>
<MudTd>@FormatDate(context.Created)</MudTd>
<MudTd>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => ViewOrder(context.Id)">
جزئیات
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var order in _orders)
{
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
@onclick="() => ViewOrder(order.Id)">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2">سفارش #@order.OrderNumber</MudText>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetPaymentStatusColor(order.PaymentStatus)"
Variant="Variant.Filled">
@DiscountOrderService.GetPaymentStatusText(order.PaymentStatus)
</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order.Created)</MudText>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(order.DeliveryStatus)
</MudChip>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">@order.ItemsCount قلم</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(order.TotalPrice) تومان</MudText>
</MudStack>
@if (order.DiscountBalanceUsed > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">
از کیف اعتباری: @FormatPrice(order.DiscountBalanceUsed) تومان
</MudText>
}
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
@if (_totalPages > 1)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-4">
<MudPagination Count="@_totalPages" Selected="@_currentPage"
SelectedChanged="OnPageChanged" Color="Color.Primary"
Variant="Variant.Outlined" />
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Orders
{
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private List<DiscountOrderSummary> _orders = new();
private int _currentPage = 1;
private int _totalPages;
private bool _loading = true;
private const int PageSize = 10;
protected override async Task OnInitializedAsync()
{
await LoadOrdersAsync();
}
private async Task LoadOrdersAsync()
{
_loading = true;
try
{
var result = await DiscountOrderService.GetUserOrdersAsync(_currentPage, PageSize);
_orders = result.Orders;
_totalPages = result.TotalPages;
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش‌ها", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadOrdersAsync();
}
private void ViewOrder(long orderId)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{orderId}");
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,173 @@
@attribute [Route("/discount-store/product/{Id:long}")]
<PageTitle>@(_product?.Title ?? "محصول اعتباری")</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4 py-md-6">
@if (_loading)
{
<LoadingState />
}
else if (_product is null)
{
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="محصول مورد نظر یافت نشد."
ActionText="بازگشت به فروشگاه اعتباری"
ActionHref="@RouteConstants.DiscountStore.Products" />
}
else
{
<!-- Breadcrumb -->
<PageHeader Title="جزئیات محصول اعتباری" BackHref="@RouteConstants.DiscountStore.Products" />
<MudGrid Spacing="4">
<!-- Image Gallery -->
<MudItem xs="12" md="5">
<MudPaper Class="pa-2 rounded-xl" Elevation="1">
<AppImage Path="@_selectedImage" Alt="@_product.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; aspect-ratio:1/1; border-radius:12px;" />
@if (_product.Images.Count > 1)
{
<MudStack Row="true" Spacing="1" Class="mt-2 flex-wrap" Justify="Justify.Center">
@foreach (var img in _product.Images)
{
<span @onclick="() => _selectedImage = img.ImageUrl" style="cursor:pointer">
<AppImage Path="@img.ThumbnailUrl" Alt="@(img.Title ?? "")"
ImgWidth="60" ImgHeight="60"
ObjectFit="ObjectFit.Cover"
Class="rounded-lg"
Style="@($"border: 2px solid {(img.ImageUrl == _selectedImage ? "var(--mud-palette-primary)" : "transparent")};")"
/>
</span>
}
</MudStack>
}
</MudPaper>
</MudItem>
<!-- Product Info -->
<MudItem xs="12" md="7">
<MudStack Spacing="3">
<MudText Typo="Typo.h5" Class="fw-bold">@_product.Title</MudText>
<!-- Categories -->
@if (_product.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var cat in _product.Categories)
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">@cat.Title</MudChip>
}
</MudStack>
}
<!-- Price & Discount -->
@if (_isAuthenticated)
{
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
<MudText Typo="Typo.body1" Class="fw-bold">
@($"{_product.Price:N0}") تومان
</MudText>
</MudStack>
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@($"{VAT.CalculateVAT(_product.Price):N0}") تومان
</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت با مالیات:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
@($"{VAT.AddVAT(_product.Price):N0}") تومان
</MudText>
</MudStack>
}
else
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت نهایی:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
@($"{_product.Price:N0}") تومان
</MudText>
</MudStack>
}
@if (_product.MaxDiscountPercent > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار از کیف اعتباری:</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
@_product.MaxDiscountPercent% (@($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
</MudChip>
</MudStack>
}
</MudStack>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Lock">
برای مشاهده قیمت ابتدا وارد شوید
</MudAlert>
}
<!-- Stock & Stats -->
<MudStack Row="true" Spacing="3" Class="flex-wrap">
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small"
Color="@(_product.RemainingCount > 0 ? Color.Success : Color.Error)"
Icon="@Icons.Material.Filled.Inventory">
@(_product.RemainingCount > 0 ? $"موجود ({_product.RemainingCount} عدد)" : "ناموجود")
</MudChip>
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.Visibility">
@_product.ViewCount بازدید
</MudChip>
</MudStack>
<!-- Short Info -->
@if (!string.IsNullOrWhiteSpace(_product.ShortInfo))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.ShortInfo</MudText>
}
<!-- Add to Cart -->
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudNumericField T="int" @bind-Value="_quantity" Min="1"
Max="@(_product.RemainingCount > 0 ? _product.RemainingCount : 1)"
Label="تعداد" Variant="Variant.Outlined" Margin="Margin.Dense"
Style="width:100px;" />
<MudButton Variant="Variant.Filled" Color="Color.Success"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
OnClick="AddToCart"
Disabled="@(_product.RemainingCount <= 0 || _addingToCart)"
Size="Size.Large">
@(_addingToCart ? "در حال افزودن..." : "افزودن به سبد")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
مشاهده سبد
</MudButton>
</MudStack>
</MudStack>
</MudItem>
</MudGrid>
<!-- Full Information -->
@if (!string.IsNullOrWhiteSpace(_product.FullInfo))
{
<MudPaper Elevation="1" Class="pa-4 pa-md-6 mt-6 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">توضیحات محصول</MudText>
<MudDivider Class="mb-4" />
<div class="blog-content">
@((MarkupString)_product.FullInfo)
</div>
</MudPaper>
}
}
</MudContainer>
@@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class ProductDetail
{
[Parameter] public long Id { get; set; }
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private bool _isAuthenticated;
private DiscountProductDetail? _product;
private string _selectedImage = string.Empty;
private int _quantity = 1;
private bool _loading = true;
private bool _addingToCart;
protected override async Task OnInitializedAsync()
{
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
await LoadProductAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await VAT.LoadAsync();
StateHasChanged();
}
}
private async Task LoadProductAsync()
{
_loading = true;
try
{
_product = await DiscountProductService.GetByIdAsync(Id);
if (_product is not null)
{
_selectedImage = _product.Images.FirstOrDefault()?.ImageUrl
?? _product.ThumbnailUrl;
}
}
catch
{
Snackbar.Add("خطا در بارگذاری محصول", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task AddToCart()
{
if (_product is null) return;
_addingToCart = true;
try
{
var productId = _product.Id;
var qty = _quantity;
var title = _product.Title;
await GuestGate.RunAsync(async () =>
{
await DiscountCartService.AddAsync(productId, qty);
Snackbar.Add($"{title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
});
}
catch
{
Snackbar.Add("خطا در افزودن به سبد خرید", MudBlazor.Severity.Error);
}
finally
{
_addingToCart = false;
}
}
}
@@ -0,0 +1,161 @@
@attribute [Route(RouteConstants.DiscountStore.Products)]
<PageTitle>فروشگاه اعتباری</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Hero -->
<MudPaper Class="discount-hero pa-6 pa-md-8 mb-4 rounded-xl" Elevation="0">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="dash-hero-name" />
<MudText Typo="Typo.h5" Align="Align.Center" Class="dash-hero-name">فروشگاه اعتباری</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="dash-hero-sub">
خرید با استفاده از موجودی کیف پول اعتباری
</MudText>
</MudStack>
</MudPaper>
<!-- Filters -->
<MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="5">
<MudTextField @bind-Value="_search"
Placeholder="جستجو در محصولات..."
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Immediate="true"
OnKeyUp="OnSearchKeyUp"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
<MudItem xs="8" sm="5">
<MudSelect T="long?" Value="_selectedCategoryId" ValueChanged="OnCategoryChanged"
Label="دسته‌بندی" Variant="Variant.Outlined" Margin="Margin.Dense"
Clearable="true" AnchorOrigin="Origin.BottomCenter">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@((long?)cat.Id)">@cat.Title (@cat.ProductCount)</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="4" sm="2" Class="d-flex gap-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
StartIcon="@Icons.Material.Filled.Search" OnClick="SearchProducts"
Style="height:40px;">جستجو</MudButton>
</MudItem>
<MudItem xs="12" Class="d-flex justify-start justify-md-end gap-2">
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
سبد خرید
@if (DiscountCart.Count > 0)
{
<MudBadge Content="@DiscountCart.Count" Color="Color.Error" Overlap="true" Class="ms-2" />
}
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
<!-- Loading -->
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_products.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
}
else
{
<!-- Products Grid -->
<MudGrid Spacing="1">
@foreach (var p in _products)
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<div id="@($"shop-product-{p.Id}")" class="h-100">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="aspect-ratio:1/1;width:100%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
@if (p.MaxDiscountPercent > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;left:8px;">
@p.MaxDiscountPercent% اعتبار
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
@if (_isAuthenticated)
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
</div>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
</MudText>
}
</div>
</MudCardContent>
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => AddToCart(p))"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن")
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
}
</MudGrid>
@* Lazy Load — بارگذاری بیشتر *@
@if (_loadingMore)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
<MudText Typo="Typo.caption" Class="mud-text-secondary">بارگذاری بیشتر...</MudText>
</MudStack>
}
else if (_hasMore)
{
<div class="d-flex justify-center py-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ExpandMore"
OnClick="LoadMore">
نمایش محصولات بیشتر
</MudButton>
</div>
}
else if (_products.Count > 0)
{
<MudText Typo="Typo.caption" Align="Align.Center" Class="py-4 mud-text-secondary">
همه @(_products.Count) محصول نمایش داده شد
</MudText>
}
}
</MudContainer>
@@ -0,0 +1,212 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Products : ComponentBase, IDisposable
{
[Inject] private DiscountProductService ProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private IJSRuntime Js { get; set; } = default!;
private bool _isAuthenticated;
private string _search = string.Empty;
private long? _selectedCategoryId;
private int _currentPage = 1;
private bool _loading;
private bool _loadingMore;
private bool _hasMore = true;
private int _totalCount;
private const int PageSize = 12;
private const string DefaultSortBy = "price desc";
private List<DiscountProductCard> _products = new();
private List<DiscountCategoryNode> _categories = new();
private bool _ignoreNextLocationChange;
private bool _pendingScrollRestore;
protected override async Task OnInitializedAsync()
{
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
_categories = await ProductService.GetCategoriesAsync();
ApplyStateFromUri();
_loading = true;
await LoadPages(_currentPage);
_loading = false;
_pendingScrollRestore = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_pendingScrollRestore && !_loading && _products.Count > 0)
{
_pendingScrollRestore = false;
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
if (payload is not null)
await ShopListScrollRestore.RestoreAsync(Js, payload);
}
await base.OnAfterRenderAsync(firstRender);
}
private void ApplyStateFromUri()
{
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
_search = state.Query;
_selectedCategoryId = state.CategoryId;
_currentPage = state.Pages;
}
private ShopListQueryState CaptureState() => new()
{
Query = _search,
CategoryId = _selectedCategoryId,
Pages = Math.Max(1, _currentPage)
};
private void SyncUrl()
{
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
return;
_ignoreNextLocationChange = true;
Navigation.NavigateTo(target, replace: true);
}
private async Task LoadPages(int pagesToLoad)
{
_products.Clear();
_hasMore = true;
pagesToLoad = Math.Max(1, pagesToLoad);
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
for (var page = 1; page <= pagesToLoad; page++)
{
var result = await ProductService.GetProductsAsync(
page: page,
pageSize: PageSize,
search: search,
categoryId: _selectedCategoryId,
sortBy: DefaultSortBy);
if (page == 1)
{
_products = result.Products;
_totalCount = result.TotalCount;
}
else
{
_products.AddRange(result.Products);
}
_currentPage = page;
_hasMore = result.CurrentPage < result.TotalPages;
if (!_hasMore)
break;
}
}
private async Task ReloadFromFilters()
{
_loading = true;
_currentPage = 1;
StateHasChanged();
await LoadPages(1);
SyncUrl();
_loading = false;
StateHasChanged();
}
private async Task LoadMore()
{
if (_loadingMore || !_hasMore) return;
_loadingMore = true;
StateHasChanged();
_currentPage++;
var result = await ProductService.GetProductsAsync(
page: _currentPage,
pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId,
sortBy: DefaultSortBy);
_products.AddRange(result.Products);
_hasMore = result.CurrentPage < result.TotalPages;
SyncUrl();
_loadingMore = false;
StateHasChanged();
}
private async Task SearchProducts() => await ReloadFromFilters();
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
await ReloadFromFilters();
}
private async Task OnCategoryChanged(long? value)
{
_selectedCategoryId = value;
await ReloadFromFilters();
}
private async Task AddToCart(DiscountProductCard p)
{
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
}
private async Task NavigateToProduct(long id)
{
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
}
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
{
if (_ignoreNextLocationChange)
{
_ignoreNextLocationChange = false;
return;
}
var uri = Navigation.ToAbsoluteUri(args.Location);
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
return;
var incoming = ShopListQueryState.Parse(uri);
if (incoming.Matches(CaptureState()))
return;
_ = InvokeAsync(async () =>
{
ApplyStateFromUri();
_loading = true;
await LoadPages(_currentPage);
_loading = false;
_pendingScrollRestore = true;
StateHasChanged();
});
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
Navigation.LocationChanged -= HandleLocationChanged;
}
}
+1 -1
View File
@@ -116,7 +116,7 @@
<!-- Contact Support -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudPaper Elevation="3" Class="pa-8 text-center">
<MudPaper Elevation="2" Class="pa-8 text-center">
<MudIcon Icon="@Icons.Material.Filled.ContactSupport" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h4" Class="mb-3">سوال شما پاسخ داده نشد؟</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
+1 -1
View File
@@ -55,7 +55,7 @@ public partial class FAQ
Icon = Icons.Material.Filled.Build,
Questions = new List<FAQQuestion>
{
new() { Question = "شجره‌نامه چگونه کار می‌کند؟", Answer = "شجره‌نامه بصری نمایش سلسله مراتبی تیم شما را نشان می‌دهد و امکان ردیابی روابط ارجاعی را فراهم می‌کند." },
new() { Question = "سازمان فروش چگونه کار می‌کند؟", Answer = "سازمان فروش بصری نمایش سلسله مراتبی تیم شما را نشان می‌دهد و امکان ردیابی روابط ارجاعی را فراهم می‌کند." },
new() { Question = "گزارش‌گیری به چه صورت است؟", Answer = "سیستم گزارش‌های جامع مالی، عملکردی و آماری ارائه می‌دهد که قابل فیلتر و دانلود به فرمت Excel است." },
new() { Question = "آیا از موبایل قابل استفاده است؟", Answer = "بله، اپلیکیشن کاملاً responsive است و تجربه کاربری عالی در موبایل و تبلت ارائه می‌دهد." }
}
@@ -0,0 +1,62 @@
@attribute [Route(RouteConstants.Gateway.CartChooser)]
<PageTitle>سبد خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="سبد خرید" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
سبد خرید کدام فروشگاه را می‌خواهید مشاهده کنید؟
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Cart" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید فروشگاه</MudText>
@if (_cartCount > 0)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
@_cartCount محصول
</MudChip>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سبد خالی</MudText>
}
</MudPaper>
</MudLink>
</MudItem>
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Cart" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-discount">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید اعتباری</MudText>
@if (_discountCartCount > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
@_discountCartCount محصول
</MudChip>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سبد خالی</MudText>
}
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack" Class="mt-4">
بازگشت به داشبورد
</MudButton>
</MudStack>
</MudContainer>
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Gateway;
public partial class CartChooser : IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
private int _cartCount;
private int _discountCartCount;
protected override async Task OnInitializedAsync()
{
await CartService.EnsureInitializedAsync();
await DiscountCartService.EnsureInitializedAsync();
_cartCount = CartService.Count;
_discountCartCount = DiscountCartService.Count;
CartService.OnChange += OnCartChanged;
DiscountCartService.OnChange += OnDiscountCartChanged;
}
private void OnCartChanged()
{
_cartCount = CartService.Count;
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -0,0 +1,49 @@
@attribute [Route(RouteConstants.Gateway.OrdersChooser)]
@inject IJSRuntime JS
<PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.Receipt" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
سفارشات کدام فروشگاه را می‌خواهید مشاهده کنید؟
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Orders" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید معمولی</MudText>
</MudPaper>
</MudLink>
</MudItem>
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Orders" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-discount">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه اعتباری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید اعتباری</MudText>
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack" Class="mt-4">
بازگشت به داشبورد
</MudButton>
</MudStack>
</MudContainer>
@code {
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -0,0 +1,56 @@
@attribute [Route(RouteConstants.Gateway.StoreChooser)]
<PageTitle>فروشگاه‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="انتخاب فروشگاه" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary" Style="max-width:500px;">
برای خرید، یکی از فروشگاه‌ها را انتخاب کنید. هر فروشگاه سبد خرید و سفارشات مجزای خود را دارد.
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<!-- Regular Store -->
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
خرید محصولات با پرداخت از کیف پول اعتباری
</MudText>
@if (_cartCount > 0)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small" Class="mt-3">
@_cartCount محصول در سبد خرید
</MudChip>
}
</MudPaper>
</MudLink>
</MudItem>
<!-- Discount Store -->
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Products" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-discount">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
</MudAvatar>
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه اعتباری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
خرید با استفاده از موجودی کیف اعتباری + درگاه پرداخت
</MudText>
@if (_discountCartCount > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-3">
@_discountCartCount محصول در سبد خرید
</MudChip>
}
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
</MudStack>
</MudContainer>
@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Gateway;
public partial class StoreChooser : IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
private int _cartCount;
private int _discountCartCount;
protected override async Task OnInitializedAsync()
{
await CartService.EnsureInitializedAsync();
await DiscountCartService.EnsureInitializedAsync();
_cartCount = CartService.Count;
_discountCartCount = DiscountCartService.Count;
CartService.OnChange += OnCartChanged;
DiscountCartService.OnChange += OnDiscountCartChanged;
}
private void OnCartChanged()
{
_cartCount = CartService.Count;
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
}
}
+480 -315
View File
@@ -1,354 +1,519 @@
@attribute [Route(RouteConstants.Main.MainPage)]
@inject NavigationManager Navigation
@inject IJSRuntime JS
<PageTitle>صفحه اصلی</PageTitle>
<PageTitle>صفحه اصلی | کارا بازار سلامت</PageTitle>
<!-- HERO -->
<section id="hero" class="hero-section">
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<MudGrid Justify="Justify.Center" Spacing="3">
<MudItem xs="12" md="6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-3">پیشنهاد ویژه برای شروع</MudChip>
<MudStack Spacing="2">
<MudText Typo="Typo.h1">
با دعوت از دوستان خود، از خریدهای واقعی پاداش بگیرید
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
ثبت‌نام آسان، تجربهٔ سریع و شفاف. همین امروز حساب بساز و با دعوت دوستانت، امتیاز و پاداش دریافت کن.
</MudText>
@* ═══════════════════════════════════════════════
1. HERO — Bold, minimal, centered
═══════════════════════════════════════════════ *@
<section class="hero-gradient py-12 py-md-16">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudStack AlignItems="AlignItems.Center" Spacing="4" Class="text-center">
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled"
Class="pulse-chip hero-chip-glass" Size="Size.Small">
باشگاه مشتریان KBS کارا بازار سلامت
</MudChip>
<MudStack Row="true" Spacing="3" Class="mb-2">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
Class="mud-ripple rounded-pill"
OnClick="@(() => Navigation.NavigateTo("/pricing"))">مشاهده قیمت‌ها</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Class="mud-ripple rounded-pill"
OnClick="@(() => NavigateToRegistrationWizard())">شروع کن</MudButton>
</MudStack>
<MudDivider Class="my-2" Style="width: 90%;" />
<MudStack Row="true" Spacing="1">
<MudChip T="string"
Color="Color.Info"
Variant="Variant.Outlined"
Class="mb-3">ثبت‌نام زیر ۲ دقیقه</MudChip>
<MudChip T="string"
Color="Color.Info"
Variant="Variant.Outlined"
Class="mb-3">پشتیبانی ۷×۲۴</MudChip>
<MudChip T="string"
Color="Color.Info"
Variant="Variant.Outlined"
Class="mb-3">پرداخت ایمن</MudChip>
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
@(_pageData?.HeroTitle ?? "رشد تیم، فروش واقعی، پاداش شفاف")
</MudText>
<MudText Typo="Typo.body1" Class="hero-subtitle" Style="max-width:520px;">
@(_pageData?.HeroSubtitle ?? "با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای.")
</MudText>
<MudStack Row="true" Spacing="2" Class="mt-2 flex-wrap" Justify="Justify.Center">
<MudButton Variant="Variant.Filled"
Class="rounded-pill hero-cta-primary"
Size="Size.Large"
OnClick="NavigateToRegistrationWizard">
@(_settings?.HeroButtonPrimaryText ?? "شروع رایگان")
</MudButton>
<MudButton Variant="Variant.Outlined"
Class="rounded-pill hero-cta-outline"
Size="Size.Large"
OnClick="@(() => Navigation.NavigateTo("/blog"))">
@(_settings?.HeroButtonSecondaryText ?? "آخرین اخبار")
</MudButton>
</MudStack>
@* ── Trust badges ── *@
<MudStack Row="true" Spacing="4" Class="mt-4 flex-wrap" Justify="Justify.Center" AlignItems="AlignItems.Center">
@foreach (var badge in _trustBadges)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@badge.Icon" Size="Size.Small" Style="color:rgba(255,255,255,.7);" />
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,.8);">@badge.Text</MudText>
</MudStack>
}
</MudStack>
</MudStack>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
1b. FEATURED POST BANNER — below hero
═══════════════════════════════════════════════ *@
@if (_latestPosts.Any())
{
var featured = _latestPosts.First();
<section class="py-4" style="background:var(--mud-palette-surface);">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="1" Class="landing-blog-banner rounded-xl cursor-pointer pa-3"
@onclick="() => NavigateToPost(featured.Slug)">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(featured.ThumbnailUrl))
{
<AppImage Path="@featured.ThumbnailUrl"
Alt="@featured.Title"
ObjectFit="ObjectFit.Cover"
Class="landing-blog-thumb"
Fallback="@Icons.Material.Filled.Article" />
}
else
{
<MudAvatar Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" Class="rounded-lg" Style="min-width:64px;">
<MudIcon Icon="@Icons.Material.Filled.Article" />
</MudAvatar>
}
<MudStack Spacing="1" Style="flex:1; min-width:0;">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary">
🔥 جدیدترین مطلب
</MudChip>
<MudText Typo="Typo.subtitle1" Class="landing-blog-title">@featured.Title</MudText>
@if (!string.IsNullOrWhiteSpace(featured.Summary))
{
<MudText Typo="Typo.body2" Class="landing-blog-summary mud-text-secondary">@featured.Summary</MudText>
}
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary d-none d-sm-flex" />
</MudStack>
</MudItem>
</MudPaper>
</MudContainer>
</section>
}
<MudItem xs="12" md="6">
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="images/team-meeting.jpg"
Alt="جلسه تیم فروش و شبکه‌سازی"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
@* ═══════════════════════════════════════════════
2. HOW IT WORKS — 3 numbered steps
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">@(_settings?.StepsTitle ?? "سه گام تا شروع")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بدون پیچیدگی، سریع و آسان
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var (step, i) in _steps.Select((s, i) => (s, i)))
{
<MudItem xs="12" sm="4">
<MudPaper Elevation="0" Class="pa-5 rounded-xl feature-card-v2 text-center fade-in-up" data-delay="@(i * 150)">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled" Class="mx-auto mb-3 landing-step-num">
<MudText Typo="Typo.h5" Style="color:#fff;font-weight:800;">@(i + 1)</MudText>
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">@step.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@step.Desc</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<MudStack Spacing="4">
<!-- WHYUS -->
<section class="whyus-section">
<MudContainer MaxWidth="MaxWidth.Large" Class="whyus-section">
<MudText Typo="Typo.h4" GutterBottom="true">
چرا کاربران ما را انتخاب می‌کنند؟
</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8">
سه دلیل روشن برای شروع امروز.
@* ═══════════════════════════════════════════════
3. FEATURES — 6 cards with icons
═══════════════════════════════════════════════ *@
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">@(_settings?.FeaturesTitle ?? "چرا کارا بازار سلامت؟")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2" Style="max-width:540px;margin:0 auto;">
ابزارهایی ساده و قدرتمند برای رشد کسب‌وکار شما
</MudText>
</div>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var (icon, title, desc, delay) in _features)
{
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.VerifiedUser"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">ثبت‌نام سریع و ساده</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
در چند گام کوتاه، حسابت را بساز و شروع کن.
</MudText>
<MudPaper Elevation="0" Class="pa-5 rounded-xl feature-card-v2 fade-in-up" data-delay="@delay">
<MudStack Spacing="2">
<MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Medium" Class="mb-1">
<MudIcon Icon="@icon" />
</MudAvatar>
<MudText Typo="Typo.h6">@title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@desc</MudText>
</MudStack>
</MudCard>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.StarBorder"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">مزایا و پاداش‌های شفاف</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.
</MudText>
</MudStack>
</MudCard>
@* ═══════════════════════════════════════════════
4. STATS — Counter cards
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var stat in _stats)
{
<MudItem xs="6" sm="3">
<MudPaper Elevation="2" Class="pa-4 rounded-xl stat-card fade-in-up text-center">
<MudText Class="stat-number" Color="@stat.Color" id="@stat.ElementId">@stat.Display</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">@stat.Label</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.Devices"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">اپلیکیشن واکنش‌گرا</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
تجربه‌ای روان در موبایل و دسکتاپ.
</MudText>
</MudStack>
</MudCard>
</MudItem>
</MudGrid>
@* ═══════════════════════════════════════════════
1c. TOP-SELLING REGULAR PRODUCTS
═══════════════════════════════════════════════ *@
@if (_loadingTopProducts)
{
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
</MudStack>
</MudContainer>
</section>
}
else
{
@if (_topRegularProducts.Any())
{
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-6 fade-in-up">
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
محبوب‌ترین محصولات کارا بازار سلامت
</MudText>
</div>
@* <!-- FEATURES: معرفی پکیج‌ها -->
<section id="features" class="py-20">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" GutterBottom="true">
پکیج‌های سرمایه گذاری
</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8">
بر اساس اندازهٔ تیم خود انتخاب کنید.
</MudText>
@if (_isLoadingPackages)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری پکیج‌ها...</MudText>
</MudStack>
}
else if (_packs.Any())
{
<MudGrid Spacing="4" Justify="Justify.Center" Class="stretch-grid">
@foreach (var p in _packs)
<MudGrid Spacing="2" Justify="Justify.FlexStart">
@foreach (var p in _topRegularProducts)
{
<MudItem xs="12" md="4" Class="d-flex">
<MudCard>
<MudCardMedia Image="@p.Image"
Title="@p.Title"
Height="300" />
<MudCardContent>
<MudText Typo="Typo.h5" Class="mb-3">@(p.Title)</MudText>
@((MarkupString)p.Body)
<MudItem xs="6" sm="6" md="4">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
Style="cursor:pointer;"
@onclick="() => NavigateToRegularProduct(p.Id)">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="aspect-ratio:1/1;width:100%;background-image:url('@p.ImageUrl');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
@if (_isAuthenticated)
{
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
</MudText>
}
</div>
</MudCardContent>
<MudCardActions Class="ma-2">
<MudText Typo="Typo.h5"
Color="Color.Success"
Align="Align.End">@(p.Price.ToThousands().ToCurrencyUnitIRT())</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="@(() => NavigateToPackage(p.Id))">مشاهده جزئیات</MudButton>
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)"
OnClick="@(() => AddRegularToCart(p))">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudText Typo="Typo.body1" Class="mud-text-secondary">هیچ پکیجی یافت نشد.</MudText>
</MudStack>
}
</MudContainer>
</section>
*@
<!-- STATS -->
<section class="stats-strip">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
کارا بازار سلامت چطور کار می‌کند؟
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
سه گام روشن تا شروع یک ماجراجویی جدید
</MudText>
<MudGrid>
<MudItem xs="12" lg="6">
<MudStack AlignItems="AlignItems.Start">
<MudPaper Outlined="true" Class="pa-6 rounded-xl">
<MudTimeline>
<MudTimelineItem Color="Color.Info" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Info" Typo="Typo.h5">ثبت‌نام</MudText>
</ItemOpposite>
<ItemContent>
<MudText Color="Color.Info" Typo="Typo.h6" GutterBottom="true">مرحله اول</MudText>
<MudText>یک حساب بساز و وارد شو.</MudText>
</ItemContent>
</MudTimelineItem>
<MudTimelineItem Color="Color.Success" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Success" Typo="Typo.h5">دعوت دوستان</MudText>
</ItemOpposite>
<ItemContent>
<MudText Align="Align.End" Color="Color.Success" Typo="Typo.h6" GutterBottom="true">مرحله دوم</MudText>
<MudText Align="Align.End">لینک دعوتت را به اشتراک بگذار.</MudText>
</ItemContent>
</MudTimelineItem>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
Href="@RouteConstants.Store.Products"
EndIcon="@Icons.Material.Filled.ArrowBack">
مشاهده همه محصولات
</MudButton>
</div>
</MudContainer>
</section>
}
<MudTimelineItem Color="Color.Error" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Error" Typo="Typo.h5">دریافت پاداش</MudText>
</ItemOpposite>
<ItemContent>
<MudText Color="Color.Error" Typo="Typo.h6" GutterBottom="true">مرحله سوم</MudText>
<MudText>از خریدهای واقعی دوستانت پاداش بگیر.</MudText>
</ItemContent>
</MudTimelineItem>
</MudTimeline>
</MudPaper>
</MudStack>
</MudItem>
<MudItem xs="1">
<MudDivider Vertical="true" />
</MudItem>
<MudItem xs="12" lg="5" Class="d-flex align-center">
<MudStack>
<MudGrid Spacing="4" Justify="Justify.SpaceAround">
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl ">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Success">+۵۰٪</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">رشد میانگین شبکه</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Primary">۹۹٫۹٪</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">آپ‌تایم سرویس</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Warning">۳ روز</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">میانگین زمان استقرار</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Secondary">+۲۰ کشور</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">پوشش ارسال کد</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudStack>
</MudItem>
@* ═══════════════════════════════════════════════
1d. TOP-SELLING DISCOUNT STORE PRODUCTS
═══════════════════════════════════════════════ *@
@if (_topDiscountProducts.Any())
{
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-6 fade-in-up">
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه اعتباری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
محصولات ویژه اعضای باشگاه با پرداخت اعتباری
</MudText>
</div>
</MudGrid>
</MudContainer>
</section>
<!-- TESTIMONIALS -->
<section id="testimonials" class="py-20 testimonials">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
اعتماد مشتریان
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
بخشی از تجربهٔ استفاده از «کارا بازار سلامت».
</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" md="6">
<MudPaper Elevation="4" Class=" pa-5 ">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar1.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">شرکت سینا نت</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">مدیر عملیات</MudText>
</div>
</MudStack>
<MudText Typo="Typo.body2">
«با کارا بازار سلامت، محاسبهٔ کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.»
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="4" Class=" pa-5 ">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar2.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">هولدینگ آریانا</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">مدیر فروش</MudText>
</div>
</MudStack>
<MudText Typo="Typo.body2">
«شجره‌نامهٔ بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.»
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- FAQ -->
<section id="faq" class="mb-6">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
سوالات متداول
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
پاسخ به سوالات رایج شما.
</MudText>
<div class="mx-auto max-w-800">
<MudExpansionPanels Square="false" Class=" ">
@foreach (var q in _faqs)
<MudGrid Spacing="2" Justify="Justify.FlexStart">
@foreach (var p in _topDiscountProducts)
{
<MudExpansionPanel Text="@q.Q">
<MudText Typo="Typo.body2">@q.A</MudText>
</MudExpansionPanel>
<MudItem xs="6" sm="6" md="4">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
Style="cursor:pointer;"
@onclick="() => NavigateToDiscountProduct(p.Id)">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="aspect-ratio:1/1;width:100%;background-image:url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? p.ImageUrl : p.ThumbnailUrl)');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
@if (p.MaxDiscountPercent > 0)
{
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;left:8px;">
@p.MaxDiscountPercent% اعتبار
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
@if (_isAuthenticated)
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatDiscountPrice(p.Price)</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
</div>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
</MudText>
}
</div>
</MudCardContent>
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
<MudButton Variant="Variant.Filled" Color="Color.Secondary" FullWidth="true"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)"
OnClick="@(() => AddDiscountToCart(p))">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudExpansionPanels>
</MudGrid>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" Class="rounded-pill"
Href="@RouteConstants.DiscountStore.Products"
EndIcon="@Icons.Material.Filled.ArrowBack">
مشاهده همه محصولات اعتباری
</MudButton>
</div>
</MudContainer>
</section>
}
}
@* ═══════════════════════════════════════════════
5. LATEST BLOG POSTS
═══════════════════════════════════════════════ *@
@if (_latestPosts.Any())
{
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">آخرین مقالات</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
جدیدترین مطالب و اخبار کارا بازار سلامت
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var post in _latestPosts.Take(2))
{
<MudItem xs="12" sm="6" md="6">
<MudPaper Elevation="1" Class="rounded-xl blog-card-v2 cursor-pointer fade-in-up" Style="overflow:hidden;"
@onclick="() => NavigateToPost(post.Slug)">
<div class="blog-thumb" style="height:180px; background:#f0f0f0;">
@if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl))
{
<AppImage Path="@post.ThumbnailUrl"
Alt="@post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:180px;" />
}
else
{
<div style="height:180px; display:flex; align-items:center; justify-content:center; background:linear-gradient(135deg,#6366f1,#a78bfa);">
<MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Large" Style="color:rgba(255,255,255,.5);" />
</div>
}
</div>
<div class="pa-4">
@if (post.Categories.Any())
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary" Class="mb-2">
@post.Categories.First().Title
</MudChip>
}
<MudText Typo="Typo.h6" Class="mb-1" Style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
@post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(post.Summary))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2" Style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
@post.Summary
</MudText>
}
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mt-2">
@if (post.PublishedAt.HasValue)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="ml-1" />
@post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
}
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Class="ml-1" />
@post.ViewCount
</MudText>
</MudStack>
</div>
</MudPaper>
</MudItem>
}
</MudGrid>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
Href="/blog">
مشاهده همه مقالات
</MudButton>
</div>
</MudContainer>
</section>
}
</MudStack>
@* ═══════════════════════════════════════════════
6. TESTIMONIALS
═══════════════════════════════════════════════ *@
@if (_testimonials.Count > 0)
{
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">@(_settings?.TestimonialsTitle ?? "اعتماد مشتریان")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بخشی از تجربه استفاده از «کارا بازار سلامت»
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var t in _testimonials)
{
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-5 rounded-xl testimonial-card fade-in-up" data-delay="@t.Delay">
<MudStack Spacing="2">
<MudRating ReadOnly="true" SelectedValue="5" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
«@t.Quote»
</MudText>
<MudDivider Class="my-1" />
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" />
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">@t.Name</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">@t.Role</MudText>
</div>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
}
@* ═══════════════════════════════════════════════
7. FAQ
═══════════════════════════════════════════════ *@
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Medium">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">@(_settings?.FaqTitle ?? "سوالات متداول")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
پاسخ به سوالات رایج شما
</MudText>
</div>
<div class="fade-in-up">
<MudExpansionPanels Elevation="1" Class="rounded-xl">
@foreach (var q in _faqs)
{
<MudExpansionPanel Text="@q.Q">
<MudText Typo="Typo.body2" Class="mud-text-secondary">@q.A</MudText>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
8. CTA BANNER
═══════════════════════════════════════════════ *@
<section class="section-landing-lg">
<MudContainer MaxWidth="MaxWidth.Medium">
<div class="cta-banner pa-8 pa-md-12 fade-in-up">
<MudStack AlignItems="AlignItems.Center" Spacing="3">
<MudText Typo="Typo.h3" Align="Align.Center" Class="dash-hero-name">
@(_settings?.CtaTitle ?? "آماده شروع هستید؟")
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="hero-subtitle">
@(_settings?.CtaDescription ?? "همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید.")
</MudText>
<MudButton Variant="Variant.Filled"
Size="Size.Large"
Class="rounded-pill mt-2 hero-cta-primary"
OnClick="NavigateToRegistrationWizard">
@(_settings?.CtaButtonText ?? "شروع رایگان")
</MudButton>
</MudStack>
</div>
</MudContainer>
</section>
+323 -87
View File
@@ -1,44 +1,286 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using System.Security.Cryptography;
namespace FrontOffice.Main.Pages;
public partial class Index:IDisposable
public partial class Index : IDisposable
{
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
[Inject] private MobileNumberEncryptor Encryptor { get; set; }
private string? _email;
private bool _isLoadingPackages;
private List<Pack> _packs = new();
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private SitePageSettingsService PageSettingsService { get; set; } = default!;
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
private bool _isAuthenticated;
// ── CMS page data ──
private PageSettingsDto? _pageData;
private LandingSettings? _settings;
// ── Top-selling product sections ──
private const int LandingTopProductCount = 6;
private List<Product> _topRegularProducts = new();
private List<DiscountProductCard> _topDiscountProducts = new();
private bool _loadingTopProducts = true;
// ── Latest blog posts (loaded from CMS) ──
private List<BlogPostCardDto> _latestPosts = new();
// ── Data lists (populated from DB or fallback) ──
private List<(string Icon, string Text)> _trustBadges = new();
private List<(string Title, string Desc)> _steps = new();
private List<(string Icon, string Title, string Desc, int Delay)> _features = new();
private List<StatItem> _stats = new();
private List<TestimonialItem> _testimonials = new();
private List<QA> _faqs = new();
// Track whether animations need re-initialization after data load
private bool _dataLoaded;
private bool _animationsInitialized;
protected override async Task OnInitializedAsync()
{
MainService.OnChangeHandler +=async () =>
MainService.OnChangeHandler += OnStateChanged;
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
// Load landing page settings from CMS
try
{
await InvokeAsync(StateHasChanged);
};
await LoadPackagesAsync();
//string mobileNumber = "09387342688";
_pageData = await PageSettingsService.GetPageAsync("landing");
if (_pageData != null)
{
_settings = _pageData.GetSettings<LandingSettings>();
}
}
catch
{
// Fallback: settings remain null → defaults below
}
//// انکریپت کردن
//string encrypted = Encryptor.EncryptMobileNumber(mobileNumber);
//Console.WriteLine($"Encrypted: {encrypted}");
PopulateFromSettings();
_dataLoaded = true;
//// دیکریپت کردن برای تست
//string decrypted = Encryptor.DecryptMobileNumber(encrypted);
//Console.WriteLine($"Decrypted: {decrypted}");
// Load top-selling in-stock products and blog posts in parallel
var topRegTask = ProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
var topDiscTask = DiscountProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
try
{
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
_topRegularProducts = topRegTask.Result.Products;
_topDiscountProducts = topDiscTask.Result.Products;
}
catch
{
// Fallback: sections remain empty
}
_loadingTopProducts = false;
// Load latest published blog posts
try
{
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
? featuredPostsTask.Result
: await BlogPostService.GetFeaturedPostsAsync(2);
if (_latestPosts.Count < 2)
{
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2);
var existing = _latestPosts.Select(p => p.Id).ToHashSet();
foreach (var post in result.Posts)
{
if (!existing.Contains(post.Id))
{
_latestPosts.Add(post);
if (_latestPosts.Count >= 2) break;
}
}
}
}
catch
{
_latestPosts = new();
}
}
private void PopulateFromSettings()
{
// ── Trust badges ──
if (_settings?.TrustBadges?.Any() == true)
{
_trustBadges = _settings.TrustBadges
.Select(b => (ResolveIcon(b.IconName), b.Text ?? ""))
.ToList();
}
else
{
_trustBadges = new()
{
(Icons.Material.Outlined.Timer, "ثبت‌نام زیر ۲ دقیقه"),
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
};
}
// ── Steps ──
if (_settings?.Steps?.Any() == true)
{
_steps = _settings.Steps
.Select(s => (s.Title ?? "", s.Description ?? ""))
.ToList();
}
else
{
_steps = new()
{
("ثبت‌نام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظه‌ای دریافت کنید."),
};
}
// ── Features ──
if (_settings?.Features?.Any() == true)
{
_features = _settings.Features
.Select((f, i) => (ResolveIcon(f.IconName), f.Title ?? "", f.Description ?? "", i * 100))
.ToList();
}
else
{
_features = new()
{
(Icons.Material.Outlined.VerifiedUser, "ثبت‌نام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
(Icons.Material.Outlined.StarBorder, "پاداش‌های شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.", 100),
(Icons.Material.Outlined.Devices, "طراحی واکنش‌گرا", "تجربه‌ای روان در موبایل و دسکتاپ.", 200),
(Icons.Material.Outlined.Groups, "تیم‌سازی هوشمند", "ساختار درختی شبکه و مدیریت تیم‌های فروش.", 300),
(Icons.Material.Outlined.Insights, "گزارش‌های لحظه‌ای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحله‌ای.", 500),
};
}
// ── Stats ──
if (_settings?.Stats?.Any() == true)
{
_stats = _settings.Stats
.Select((s, i) => new StatItem(
$"stat-{i}",
$"{s.Value}{s.Suffix}",
s.Value,
s.Suffix ?? "",
ResolveColor(s.Color),
s.Label ?? ""))
.ToList();
}
else
{
_stats = new()
{
new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"),
new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپ‌تایم سرویس"),
new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"),
new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"),
};
}
// ── Testimonials ──
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
if (_settings is not null)
{
_testimonials = (_settings.Testimonials ?? [])
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
.ToList();
}
else
{
_testimonials = new()
{
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.", "شرکت سینا نت", "مدیر عملیات", 0),
new("سازمان فروش بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
new("سادگی ثبت‌نام و شفافیت پاداش‌ها مهم‌ترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
};
}
// ── FAQs ──
if (_settings?.Faqs?.Any() == true)
{
_faqs = _settings.Faqs
.Select(f => new QA(f.Question ?? "", f.Answer ?? ""))
.ToList();
}
else
{
_faqs = new()
{
new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),
new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."),
new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."),
new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
};
}
}
/// <summary>
/// Resolve icon name from SettingsJson to MudBlazor icon string.
/// Falls back to a generic icon.
/// </summary>
private static string ResolveIcon(string? iconName)
{
if (string.IsNullOrWhiteSpace(iconName)) return Icons.Material.Outlined.Info;
// Try to get from MudBlazor Icons via reflection
var field = typeof(Icons.Material.Outlined).GetField(iconName,
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
// Also try Filled
field = typeof(Icons.Material.Filled).GetField(iconName,
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
return Icons.Material.Outlined.Info;
}
private static Color ResolveColor(string? colorName) => colorName?.ToLowerInvariant() switch
{
"primary" => Color.Primary,
"secondary" => Color.Secondary,
"success" => Color.Success,
"warning" => Color.Warning,
"error" => Color.Error,
"info" => Color.Info,
_ => Color.Default
};
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Init/re-init scroll animations after data is loaded and rendered
if ((firstRender || (_dataLoaded && !_animationsInitialized)) && _steps.Any())
{
_animationsInitialized = true;
// Init scroll-triggered fade-in animations
await JS.InvokeVoidAsync("initScrollAnimations");
// Animate stat counters
foreach (var stat in _stats)
{
await JS.InvokeVoidAsync("animateCounter", stat.ElementId, stat.Target, 2200, stat.Suffix);
}
}
if (await AuthService.IsAuthenticatedAsync())
{
if ((await AuthService.IsCompleteRegisterAsync()))
if (await AuthService.IsCompleteRegisterAsync())
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
}
@@ -47,56 +289,8 @@ public partial class Index:IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
}
await base.OnAfterRenderAsync(firstRender);
}
private async Task LoadPackagesAsync()
{
_isLoadingPackages = true;
try
{
var response = await PackageClient.GetAllPackageByFilterAsync(request: new());
if (response?.Models?.Any() == true)
{
_packs = response.Models.Select(p => new Pack(
Id: p.Id,
Title: p.Title,
Body: p.Description,
Image: UrlUtility.DownloadUrl + p.ImagePath,
Price: p.Price
)).ToList();
}
else
_packs = new List<Pack>();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error);
// Fallback to empty list
_packs = new List<Pack>();
}
finally
{
_isLoadingPackages = false;
await InvokeAsync(StateHasChanged);
}
}
private void JoinWaitlist()
{
if (string.IsNullOrWhiteSpace(_email))
{
Snackbar.Add("لطفاً ایمیل معتبر وارد کنید.", Severity.Warning);
return;
}
Snackbar.Add("به لیست انتظار «کارا بازار سلامت» اضافه شدید.", Severity.Success);
_email = string.Empty;
}
private void NavigateToPackage(long packageId)
{
Navigation.NavigateTo($"{RouteConstants.Package.Detail}{packageId}");
await base.OnAfterRenderAsync(firstRender);
}
private void NavigateToRegistrationWizard()
@@ -104,25 +298,67 @@ public partial class Index:IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
private record Pack(long Id, string Title, string Body, string Image, long Price);
private record Plan(string Name, string Price, bool Highlight, IEnumerable<string> Features);
private record QA(string Q, string A);
private readonly List<QA> _faqs = new()
private void NavigateToPost(string slug)
{
new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),
new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."),
new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."),
new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
};
Navigation.NavigateTo($"/blog/{slug}");
}
private void NavigateToRegularProduct(long id)
=> Navigation.NavigateTo(RouteConstants.Store.ProductDetail + id);
private void NavigateToDiscountProduct(long id)
=> Navigation.NavigateTo(RouteConstants.DiscountStore.ProductDetail + id);
private async Task AddRegularToCart(Product p)
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
private async Task AddDiscountToCart(DiscountProductCard p)
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
private string FormatDiscountPrice(long price) => $"{price:N0} تومان";
private async void OnStateChanged()
{
await InvokeAsync(StateHasChanged);
}
// ── Records ──
private record QA(string Q, string A);
private record StatItem(string ElementId, string Display, double Target, string Suffix, Color Color, string Label);
private record TestimonialItem(string Quote, string Name, string Role, int Delay);
// ── SettingsJson DTO ──
private class LandingSettings
{
public string? HeroButtonPrimaryText { get; set; }
public string? HeroButtonSecondaryText { get; set; }
public List<TrustBadgeItem>? TrustBadges { get; set; }
public string? StepsTitle { get; set; }
public List<StepItem>? Steps { get; set; }
public string? FeaturesTitle { get; set; }
public List<FeatureItem>? Features { get; set; }
public string? StatsTitle { get; set; }
public List<StatSettingItem>? Stats { get; set; }
public string? TestimonialsTitle { get; set; }
public List<TestimonialSettingItem>? Testimonials { get; set; }
public string? FaqTitle { get; set; }
public List<FaqItem>? Faqs { get; set; }
public string? CtaTitle { get; set; }
public string? CtaDescription { get; set; }
public string? CtaButtonText { get; set; }
public bool? FeaturedBlogEnabled { get; set; }
}
private class TrustBadgeItem { public string? IconName { get; set; } public string? Text { get; set; } }
private class StepItem { public string? Title { get; set; } public string? Description { get; set; } }
private class FeatureItem { public string? IconName { get; set; } public string? Title { get; set; } public string? Description { get; set; } }
private class StatSettingItem { public string? Label { get; set; } public double Value { get; set; } public string? Suffix { get; set; } public string? Color { get; set; } }
private class TestimonialSettingItem { public string? Quote { get; set; } public string? Name { get; set; } public string? Role { get; set; } }
private class FaqItem { public string? Question { get; set; } public string? Answer { get; set; } }
public void Dispose()
{
MainService.OnChangeHandler -=async () =>
{
await InvokeAsync(StateHasChanged);
};
MainService.OnChangeHandler -= OnStateChanged;
}
}
+99
View File
@@ -0,0 +1,99 @@
@attribute [Route(RouteConstants.Licenses.Index)]
@inject SitePageSettingsService PageSettingsService
<PageTitle>مجوزها و گواهینامه‌ها | کارا بازار سلامت</PageTitle>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<LoadingState />
</MudContainer>
}
else
{
<!-- Hero Section -->
<section class="licenses-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled">مجوزها و گواهینامه‌ها</MudChip>
<MudText Typo="Typo.h2" Class="mb-3">
@(_pageData?.HeroTitle ?? "مجوزها و گواهینامه‌های رسمی")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 700px;">
@(_pageData?.HeroSubtitle ?? "ما با دریافت مجوزهای رسمی و تأییدیه‌های معتبر، اطمینان شما را در استفاده از خدمات خود جلب کرده‌ایم.")
</MudText>
</MudStack>
</MudContainer>
</section>
<MudStack Spacing="8" Class="pb-12">
<!-- Licenses Gallery -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">مجوزهای ما</MudText>
@if (_licenseImages.Any())
{
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var license in _licenseImages)
{
<MudItem xs="6" sm="6" md="4">
<MudCard Elevation="2" Class="h-100">
<MudCardMedia Image="@license.ImagePath" Height="250" />
<MudCardContent>
@if (!string.IsNullOrWhiteSpace(license.Title))
{
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-1">@license.Title</MudText>
}
@if (!string.IsNullOrWhiteSpace(license.Description))
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">@license.Description</MudText>
}
</MudCardContent>
@if (!string.IsNullOrWhiteSpace(license.LinkUrl))
{
<MudCardActions Class="justify-center">
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Href="@license.LinkUrl"
Target="_blank"
StartIcon="@Icons.Material.Filled.OpenInNew">
مشاهده جزئیات
</MudButton>
</MudCardActions>
}
</MudCard>
</MudItem>
}
</MudGrid>
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Class="my-8" Dense="true">
اطلاعات مجوزها به‌زودی بارگذاری خواهد شد.
</MudAlert>
}
</MudContainer>
</section>
<!-- CTA Section -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="3" Class="pa-8 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h4" Class="mb-3">نیاز به اطلاعات بیشتر دارید؟</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
برای دریافت اطلاعات بیشتر درباره مجوزها و گواهینامه‌های ما، با ما تماس بگیرید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
تماس با ما
</MudButton>
</MudPaper>
</MudContainer>
</section>
</MudStack>
}
@@ -0,0 +1,31 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages;
public partial class Licenses
{
private bool _loading = true;
private PageSettingsDto? _pageData;
private List<PageSettingsImageDto> _licenseImages = new();
protected override async Task OnInitializedAsync()
{
try
{
_pageData = await PageSettingsService.GetPageAsync("licenses");
if (_pageData != null)
{
_licenseImages = _pageData.GetImages("licenses");
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
}
}
@@ -2,18 +2,15 @@
@using FrontOffice.Main.Utilities
@using MudBlazor
<PageTitle>آمار شبکه</PageTitle>
<PageTitle>آمار تیم</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">آمار شبکه من</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="آمار باشگاه توسعه دهندگان" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت آمار..." />
}
else if (_statistics is not null)
{
@@ -22,7 +19,7 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">کل اعضای شبکه</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">اعضای باشگاه توسعه دهندگان</MudText>
<MudText Typo="Typo.h4" Color="Color.Primary">@_statistics.TotalMembers</MudText>
<MudIcon Icon="@Icons.Material.Filled.Groups" Color="Color.Primary" />
</MudStack>
@@ -31,7 +28,7 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">شاخه چپ</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">بخش (A)</MudText>
<MudText Typo="Typo.h4" Color="Color.Info">@_statistics.LeftLegCount</MudText>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" />
</MudStack>
@@ -40,7 +37,7 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">شاخه راست</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">بخش (B)</MudText>
<MudText Typo="Typo.h4" Color="Color.Success">@_statistics.RightLegCount</MudText>
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" />
</MudStack>
@@ -49,7 +46,7 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">عمق درخت</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">فاصله شعب</MudText>
<MudText Typo="Typo.h4" Color="Color.Secondary">@_statistics.TreeDepth</MudText>
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Color="Color.Secondary" />
</MudStack>
@@ -57,12 +54,12 @@
</MudItem>
</MudGrid>
<!-- تعادل شاخه‌ها -->
<!-- آمار فروش تیم‌ها -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">تعادل شاخه‌ها</MudText>
<MudText Typo="Typo.h6" Class="mb-3">آمار جمعی فعالیت</MudText>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body1">شاخه چپ: <strong>@_statistics.LeftLegCount</strong> عضو</MudText>
<MudText Typo="Typo.body1">بخش (A): <strong>@_statistics.LeftLegCount</strong> عضو</MudText>
<MudProgressLinear Color="Color.Info"
Value="@GetLeftPercentage()"
Size="Size.Medium"
@@ -70,7 +67,7 @@
Style="width: 200px;" />
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body1">شاخه راست: <strong>@_statistics.RightLegCount</strong> عضو</MudText>
<MudText Typo="Typo.body1">بخش (B): <strong>@_statistics.RightLegCount</strong> عضو</MudText>
<MudProgressLinear Color="Color.Success"
Value="@GetRightPercentage()"
Size="Size.Medium"
@@ -80,7 +77,7 @@
@if (!string.IsNullOrEmpty(_statistics.WeakerLeg))
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
شاخه ضعیف‌تر: <strong>@GetLegText(_statistics.WeakerLeg)</strong>
بخش کوچک تر: <strong>@GetLegText(_statistics.WeakerLeg)</strong>
</MudAlert>
}
</MudStack>
@@ -88,12 +85,12 @@
<!-- نمودار دایره‌ای -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">توزیع اعضا</MudText>
<MudText Typo="Typo.h6" Class="mb-3">توزیع توسعه دهندگان</MudText>
<MudChart ChartType="ChartType.Donut"
Width="300px"
Height="300px"
InputData="@(new double[] { _statistics.LeftLegCount, _statistics.RightLegCount })"
InputLabels="@(new[] { "شاخه چپ", "شاخه راست" })"
InputLabels="@(new[] { "بخش (A)", "بخش (B)" })"
ChartOptions="@_chartOptions" />
</MudPaper>
@@ -126,7 +123,7 @@
StartIcon="@Icons.Material.Filled.AccountTree"
Href="@RouteConstants.Profile.Tree"
FullWidth="true">
مشاهده درخت کامل
مشاهده کل توسعه دهندگان
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
@@ -140,7 +137,7 @@
else if (_hasError)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
خطا در دریافت آمار شبکه. لطفا دوباره تلاش کنید.
خطا در دریافت آمار توسعه دهندگان. لطفا دوباره تلاش کنید.
</MudAlert>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadStatisticsAsync">تلاش مجدد</MudButton>
}
@@ -61,15 +61,15 @@ public partial class NetworkStatisticsPage : ComponentBase
private string GetLegText(string leg) => leg.ToLower() switch
{
"left" => "شاخه چپ",
"right" => "شاخه راست",
"left" => "بخش (A)",
"right" => "بخش (B)",
_ => leg
};
private string GetPositionText(string position) => position.ToLower() switch
{
"left" => "چپ",
"right" => "راست",
"left" => "(A)",
"right" => "(B)",
_ => position
};
@@ -3,16 +3,7 @@
<PageTitle>پکیج‌های من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Breadcrumb -->
<MudBreadcrumbs Items="_breadcrumbItems" Class="mb-4" />
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های من</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
وضعیت پکیج‌های خریداری شده و عضویت باشگاه مشتریان
</MudText>
</MudStack>
<PageHeader Title="پکیج‌های من" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
@@ -29,7 +20,7 @@
<MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" />
<MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکرده‌اید</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند کمیسیون هفتگی و شبکه‌سازی بهره‌مند شوید.
با خرید پکیج، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
@@ -43,7 +34,76 @@
}
else
{
@* ── G3: راهنمای پکیج‌های من (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-2" Icon="@Icons.Material.Filled.Info">
بعد از تکمیل چرخه کیف‌پول جادویی، می‌توانید مجدداً پکیج خریداری کرده و دور جدیدی را آغاز کنید.
</MudAlert>
<MudGrid Spacing="4">
@* Re-Purchase / Cycle Progress Section *@
@if (_canRepurchase)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Success" Icon="@Icons.Material.Filled.Celebration" Class="mb-0">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width:100%">
<MudText Typo="Typo.body1">
🎉 چرخه جادویی تکمیل شد! می‌توانید پکیج جدید بخرید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Package.List))">
خرید پکیج جدید
</MudButton>
</MudStack>
</MudAlert>
</MudItem>
}
else if (_magicStatus != null && _magicStatus.WalletMode == 1)
{
<MudItem xs="12">
<MudPaper Elevation="2" Class="pa-6">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Color="Color.Warning" />
<MudText Typo="Typo.h6">پیشرفت چرخه جادویی</MudText>
@if (_magicStatus.PurchaseCycleCount > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info">دور @(_magicStatus.PurchaseCycleCount + 1)</MudChip>
}
</MudStack>
<MudDivider />
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">واریز انجام‌شده:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicTotalDeposited)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">سقف واریز:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicMaxDeposit)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">باقیمانده:</MudText>
<MudText Typo="Typo.body2" Color="Color.Warning">@FormatPrice(_magicStatus.MagicRemainingDeposit)</MudText>
</MudStack>
<MudProgressLinear Color="Color.Primary" Value="@GetDepositProgress()" Class="my-2" Rounded="true" Size="Size.Large">
<MudText Typo="Typo.caption"><b>@GetDepositProgress()%</b></MudText>
</MudProgressLinear>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_magicStatus.MagicTotalCredited)</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
<!-- Package Status Card -->
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 h-100">
@@ -58,7 +118,7 @@
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText>
<MudText Typo="Typo.body2">پکیج طلایی</MudText>
<MudText Typo="Typo.body2">@_userStatus.PackageTitle</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -108,7 +168,7 @@
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountTree"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Network.Statistics))">
مشاهده شبکه
مشاهده تیم
</MudButton>
<MudButton Variant="Variant.Outlined"
@@ -116,7 +176,7 @@
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payments"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Commission.Dashboard))">
کمیسیون‌های من
پاداش‌های من
</MudButton>
</MudStack>
}
@@ -155,7 +215,7 @@
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" />
<MudText Typo="Typo.h6">مزایای پکیج طلایی</MudText>
<MudText Typo="Typo.h6">مزایای پکیج</MudText>
</MudStack>
<MudDivider />
@@ -164,7 +224,7 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Color="Color.Primary" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">شبکه‌سازی نامحدود</MudText>
<MudText Typo="Typo.subtitle2" Class="mt-2">تیم‌سازی نامحدود</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">دعوت دوستان و ساخت تیم</MudText>
</MudPaper>
</MudItem>
@@ -172,15 +232,15 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.Payments" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">کمیسیون هفتگی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">دریافت سهم از فروش شبکه</MudText>
<MudText Typo="Typo.subtitle2" Class="mt-2">پاداش هفتگی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">دریافت سهم از فروش تیم</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.ShoppingBag" Color="Color.Secondary" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه اعتباری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">خرید با تخفیف ویژه</MudText>
</MudPaper>
</MudItem>
@@ -9,7 +9,9 @@ public partial class MyPackages : ComponentBase
[Inject] private PackageService PackageService { get; set; } = default!;
private UserPackageStatusDto? _userStatus;
private MagicWalletStatus? _magicStatus;
private bool _isLoading = true;
private bool _canRepurchase = false;
private List<BreadcrumbItem> _breadcrumbItems = new()
{
@@ -30,6 +32,18 @@ public partial class MyPackages : ComponentBase
try
{
_userStatus = await PackageService.GetUserPackageStatusAsync();
if (_userStatus?.HasPurchasedPackage == true)
{
_magicStatus = await WalletService.GetMagicWalletStatusAsync();
// Cycle complete: wallet back to Normal mode, at least one cycle done,
// and deposit ceiling fully used (remaining = 0)
_canRepurchase = _magicStatus != null
&& _magicStatus.WalletMode == 0 // Normal mode
&& _magicStatus.PurchaseCycleCount >= 1 // At least one cycle completed
&& _magicStatus.MagicRemainingDeposit == 0; // Deposit ceiling used up
}
}
catch (Exception ex)
{
@@ -67,4 +81,11 @@ public partial class MyPackages : ComponentBase
{
return string.Format("{0:N0} تومان", price);
}
private int GetDepositProgress()
{
if (_magicStatus == null || _magicStatus.MagicMaxDeposit <= 0) return 0;
var pct = (int)((_magicStatus.MagicTotalDeposited * 100) / _magicStatus.MagicMaxDeposit);
return Math.Min(pct, 100);
}
}
@@ -3,13 +3,12 @@
<PageTitle>پکیج‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های سرمایه‌گذاری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه بهره‌مند شوید.
</MudText>
</MudStack>
<PageHeader Title="پکیج‌های سرمایه‌گذاری" BackHref="@RouteConstants.Profile.Index" />
@* ── G1: راهنمای سیستم پکیج‌بیس (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-4" Icon="@Icons.Material.Filled.Info">
هر پکیج ویژگی‌ها و مزایای منحصربه‌فردی دارد. با انتخاب پکیج مناسب، از سقف پاداش و ضریب کیف‌پول جادویی متفاوتی بهره‌مند می‌شوید.
</MudAlert>
@if (_isLoading)
{
@@ -33,7 +32,7 @@
{
<MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle">
<MudText>
شما قبلاً پکیج طلایی را خریداری کرده‌اید.
شما قبلاً پکیج را خریداری کرده‌اید.
@if (_userStatus.IsClubMemberActive)
{
<span>عضویت باشگاه شما فعال است.</span>
@@ -70,20 +69,43 @@
<MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText>
</MudStack>
<!-- Features Preview -->
<!-- Package Highlights -->
<MudStack Spacing="1" Class="mt-2">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">عضویت در باشگاه مشتریان</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">دریافت کمیسیون هفتگی</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">شبکه‌سازی نامحدود</MudText>
</MudStack>
@if (package.SupportsDirectPurchase)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">پرداخت مستقیم</MudText>
</MudStack>
}
@if (package.SupportsDayaPurchase)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">پرداخت با اعتبار دایا</MudText>
</MudStack>
}
@if (package.DiscountMultiplier > 0)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">ضریب اعتبار: @package.DiscountMultiplier.ToString("F1")x</MudText>
</MudStack>
}
@if (package.MagicWalletMultiplier > 0)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">کیف پول جادویی: @package.MagicWalletMultiplier.ToString("F1")x</MudText>
</MudStack>
}
@if (package.IsBasePackage)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.caption">پکیج پایه</MudText>
</MudStack>
}
</MudStack>
</MudStack>
</MudCardContent>
@@ -106,10 +128,10 @@
<MudGrid Spacing="4">
<MudItem xs="12" md="8">
<MudStack Spacing="2">
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج طلایی؟</MudText>
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج بخریم؟</MudText>
<MudText Typo="Typo.body1" Style="color: rgba(255,255,255,0.9);">
با خرید پکیج طلایی، علاوه بر دسترسی به محصولات ویژه، می‌توانید در شبکه فروش شرکت کنید
و از کمیسیون‌های هفتگی بهره‌مند شوید. هر چه شبکه شما گسترده‌تر، درآمد شما بیشتر!
با خرید پکیج، علاوه بر دسترسی به محصولات ویژه، می‌توانید تیم فروش خود را بسازید
و از پاداش‌های هفتگی بهره‌مند شوید. هر چه تیم شما گسترده‌تر، درآمد شما بیشتر!
</MudText>
</MudStack>
</MudItem>
+15 -16
View File
@@ -4,26 +4,25 @@
@if (_isLoading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری...</MudText>
</MudStack>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<LoadingState Message="در حال بارگذاری..." />
</MudContainer>
}
else if (_package == null)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudIcon Icon="@Icons.Material.Filled.Error" Size="Size.Large" Color="Color.Error" />
<MudText Typo="Typo.h5" Class="mt-2">پکیج یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">پکیج مورد نظر وجود ندارد یا حذف شده است.</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4" OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
بازگشت به صفحه اصلی
</MudButton>
</MudStack>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.Error"
Title="پکیج یافت نشد"
Description="پکیج مورد نظر وجود ندارد یا حذف شده است."
ActionText="بازگشت به صفحه اصلی"
ActionHref="@RouteConstants.Main.MainPage" />
</MudContainer>
}
else
{
<!-- Breadcrumb -->
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4">
<PageHeader Title="جزئیات پکیج" BackHref="@RouteConstants.Package.List" />
<MudBreadcrumbs Items="_breadcrumbItems" />
</MudContainer>
@@ -36,9 +35,9 @@ else
<MudStack Class="mb-4">
<!-- Package Image -->
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_package.Image"
<AppImage Path="@_package.Image"
Alt="@_package.Title"
Height="250"
ImgHeight="250"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
@@ -228,9 +227,9 @@ else
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 d-flex flex-column h-100 cursor-pointer"
OnClick="() => NavigateToPackage(relatedPackage.Id)">
<MudImage Src="@relatedPackage.Image"
<AppImage Path="@relatedPackage.Image"
Alt="@relatedPackage.Title"
Height="200"
ImgHeight="200"
ObjectFit="ObjectFit.Cover"
Class="rounded-xl mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">@relatedPackage.Title</MudText>
@@ -1,5 +1,5 @@
using Blazored.LocalStorage;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Package;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Grpc.Core;
@@ -64,36 +64,38 @@ public partial class PackageDetail : IDisposable
try
{
// Load package details
var packageResponse = await PackageClient.GetPackageAsync(request: new() { Id = Id}, cancellationToken: _loadCts.Token);
// Load package details via Customer RPC
var response = await PackageClient.GetCustomerPackageDetailsAsync(
new GetCustomerPackageDetailsRequest { PackageId = Id },
cancellationToken: _loadCts.Token);
if (packageResponse != null)
if (response != null)
{
// Build features from API response
var features = response.Features?.Select(f => f.Title).ToList() ?? new List<string>();
// Build specifications from API features (highlighted ones)
var specs = response.Features?
.Where(f => f.IsHighlighted)
.Select(f => new Specification
{
Name = f.Title,
Value = f.Description,
Icon = string.IsNullOrEmpty(f.Icon) ? Icons.Material.Filled.Star : f.Icon
}).ToList() ?? new List<Specification>();
_package = new PackageDetailDto
{
Id = packageResponse.Id,
Title = packageResponse.Title,
Body = packageResponse.Description,
Image = UrlUtility.DownloadUrl + packageResponse.ImagePath,
Specifications = new List<Specification>
{
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
new() { Name = "شجره‌نامه", Value = "پیشرفته", Icon = Icons.Material.Filled.AccountTree },
new() { Name = "گزارش‌گیری", Value = "جامع", Icon = Icons.Material.Filled.Analytics },
new() { Name = "پشتیبانی", Value = "۲۴ ساعته", Icon = Icons.Material.Filled.Support }
},
Features = new List<string>
{
"مدیریت تیم نامحدود",
"شجره‌نامه بصری",
"محاسبه کارمزد خودکار",
"گزارش‌های مالی",
"پشتیبانی اولویت‌دار"
},
Id = response.Id,
Title = response.Title,
Body = response.Description,
Image = response.ImagePath ?? string.Empty,
Specifications = specs,
Features = features,
Pricing = new PricingInfo
{
OriginalPrice = packageResponse.Price,
FinalPrice = packageResponse.Price,
OriginalPrice = response.Price,
FinalPrice = response.Price,
HasDiscount = false,
DiscountPercent = 0
}
@@ -123,23 +125,14 @@ public partial class PackageDetail : IDisposable
private async Task LoadReviewsAsync()
{
// TODO: Load reviews from API
_reviews = new List<Review>
{
new() { UserName = "علی احمدی", Rating = 5, Comment = "عالی! کارمزد رو دقیق حساب می‌کنه و گزارش‌ها کامل هستن.", Date = "۱۴۰۲/۱۰/۰۵" },
new() { UserName = "مریم رضایی", Rating = 4, Comment = "رابط کاربری خوبی داره، فقط سرعت بارگذاری می‌تونه بهتر بشه.", Date = "۱۴۰۲/۰۹/۲۲" },
new() { UserName = "حسن کریمی", Rating = 5, Comment = "پشتیبانی فوق‌العاده سریع و حرفه‌ای داشتن. پیشنهاد می‌کنم.", Date = "۱۴۰۲/۰۹/۱۵" }
};
// TODO: Load reviews from API when endpoint is available
_reviews = new List<Review>();
}
private async Task LoadRelatedPackagesAsync()
{
// TODO: Load related packages from API
_relatedPackages = new List<RelatedPackage>
{
new() { Id = "2", Title = "پکیج رشد", ShortDescription = "مناسب برای تیم‌های در حال توسعه", Image = "images/package1.jpg", Pricing = new PricingInfo { FinalPrice = 750000 } },
new() { Id = "3", Title = "پکیج حرفه‌ای", ShortDescription = "برای کسب‌وکارهای بزرگ", Image = "images/package2.jpg", Pricing = new PricingInfo { FinalPrice = 1200000 } }
};
// TODO: Load related packages from API when endpoint is available
_relatedPackages = new List<RelatedPackage>();
}
private async Task PurchasePackage()
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">مدیریت آدرس‌ها</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="مدیریت آدرس‌ها" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
@@ -1,4 +1,5 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -8,8 +9,9 @@ namespace FrontOffice.Main.Pages.Profile;
public partial class Addresses : ComponentBase
{
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private List<CustomerAddressModel> _addresses = new();
private bool _isLoadingAddresses;
protected override async Task OnInitializedAsync()
@@ -22,7 +24,7 @@ public partial class Addresses : ComponentBase
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new());
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
_addresses = response?.Models?.ToList() ?? new();
}
catch (Exception ex)
@@ -40,18 +42,21 @@ public partial class Addresses : ComponentBase
{
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (!result.Canceled)
if (result is not null && !result.Canceled)
{
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
}
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
{ x => x.Model, address }
});
var result = await dialog.Result;
if (!result.Canceled)
if (result is not null && !result.Canceled)
await LoadAddresses();
}
@@ -59,7 +64,7 @@ public partial class Addresses : ComponentBase
{
try
{
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = id });
await UserAddressContract.SetCustomerDefaultAddressAsync(new() { Id = id });
Snackbar.Add("آدرس پیش‌فرض تغییر کرد.", Severity.Success);
await LoadAddresses();
}
@@ -76,9 +81,10 @@ public partial class Addresses : ComponentBase
{
try
{
await UserAddressContract.DeleteUserAddressAsync(new() { Id = id });
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
Snackbar.Add("آدرس حذف شد.", Severity.Success);
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
catch (Exception ex)
{
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تغییر رمز عبور</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Settings">بازگشت</MudButton>
</MudStack>
<PageHeader Title="تغییر رمز عبور" BackHref="@RouteConstants.Profile.Settings" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3">
@@ -0,0 +1,212 @@
@attribute [Route(RouteConstants.Profile.ChargeCreditWallet)]
@attribute [Authorize]
<PageTitle>شارژ کیف‌پول اصلی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="💳 شارژ کیف‌پول اصلی" BackHref="@RouteConstants.Profile.Wallet" />
@if (_paymentResult != null)
{
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
Class="rounded-lg" Variant="Variant.Filled">
@if (_paymentResult == "success")
{
<MudStack Spacing="2">
<span>شارژ کیف پول اصلی با موفقیت انجام شد! ✅</span>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="ContinueShoppingAsync">
ادامه خرید
</MudButton>
</MudStack>
}
else if (_paymentResult == "cancelled")
{
<span>پرداخت توسط شما لغو شد.</span>
}
else
{
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
}
</MudAlert>
}
@if (_authLoaded && _redirectingToMagic)
{
<MudAlert Severity="MudBlazor.Severity.Info" Class="rounded-lg" Variant="Variant.Outlined">
<MudStack Spacing="2" AlignItems="AlignItems.Center">
<MudProgressCircular Size="Size.Small" Indeterminate="true" Color="Color.Primary" />
<MudText>
کیف پول شما در حالت جادویی است. در حال انتقال به صفحه شارژ کیف پول جادویی...
</MudText>
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
Href="@RouteConstants.Profile.MagicWallet"
StartIcon="@Icons.Material.Filled.AutoAwesome">
رفتن به شارژ کیف جادویی
</MudButton>
</MudStack>
</MudAlert>
}
else if (_authLoaded && !_isClubMemberActive)
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
<MudStack Spacing="2">
<MudText>
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
</MudText>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@if (!_hasPurchasedPackage)
{
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
Href="@RouteConstants.Package.List"
StartIcon="@Icons.Material.Filled.CardGiftcard">
مشاهده پکیج‌ها
</MudButton>
}
else
{
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
Href="@RouteConstants.Club.Membership"
StartIcon="@Icons.Material.Filled.Handshake">
امضای قرارداد باشگاه
</MudButton>
}
<MudButton Size="Size.Small" Variant="Variant.Text"
Href="@RouteConstants.Profile.Wallet">
بازگشت به کیف پول
</MudButton>
</MudStack>
</MudStack>
</MudAlert>
}
else if (_authLoaded && _needsMagicCeilingConsent && !_allowNormalDespiteMagic && !_redirectingToMagic)
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
<MudStack Spacing="3">
<MudText Typo="Typo.subtitle1">
سقف شارژ کیف پول جادویی در این دور پر شده است.
</MudText>
<MudText Typo="Typo.body2">
شارژ از این صفحه بدون ضریب (۱:۱) به موجودی اضافه می‌شود و ضریب جادویی اعمال نمی‌شود.
برای ادامه، تأیید کنید.
</MudText>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="AcceptMagicCeilingConsent">
ادامه شارژ بدون ضریب
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.AutoAwesome"
OnClick="DeclineMagicCeilingConsent">
بازگشت به کیف جادویی
</MudButton>
</MudStack>
</MudStack>
</MudAlert>
}
else if (ShowChargeForm)
{
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
شارژ موجودی اصلی
</MudText>
@if (_allowNormalDespiteMagic)
{
<MudAlert Severity="MudBlazor.Severity.Warning" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
این شارژ بدون ضریب جادویی است (۱:۱). سقف واریز جادویی شما در این دور پر شده است.
</MudAlert>
}
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه می‌شود.
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده می‌شود.
</MudAlert>
<MudStack Spacing="2">
<MudNumericField @bind-Value="_chargeAmount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Min="10_000"
Max="1_000_000_000"
Format="N0"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts)
{
<MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
OnClick="() => _chargeAmount = preset"
Class="rounded-pill">
@FormatToman(preset)
</MudButton>
}
</MudStack>
@if (_chargeAmount > 0)
{
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #eff6ff;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ پرداخت:</MudText>
<MudText Typo="Typo.body2"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">موجودی دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Primary"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
OnClick="StartCreditCharge"
StartIcon="@Icons.Material.Filled.Payment"
Class="rounded-lg mt-2">
@if (_isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
</MudStack>
</MudPaper>
<MudExpansionPanels Elevation="1" Class="rounded-lg">
<MudExpansionPanel Text="قوانین شارژ کیف پول اصلی" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش قابل استفاده است
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
مبلغ واریزی بدون ضریب به موجودی اضافه می‌شود (۱:۱)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
پرداخت از طریق درگاه بانکی انجام می‌شود
</MudListItem>
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
}
</MudStack>
</MudContainer>
@@ -0,0 +1,199 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class ChargeCreditWallet : ComponentBase
{
[Inject] private AuthService AuthService { get; set; } = default!;
private bool _isProcessing;
private long _chargeAmount;
private string? _paymentResult;
private string? _pendingReturnUrl;
private bool _isClubMemberActive;
private bool _hasPurchasedPackage;
private bool _authLoaded;
private bool _redirectingToMagic;
private bool _allowNormalDespiteMagic;
private bool _needsMagicCeilingConsent;
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentQueryParam { get; set; }
[SupplyParameterFromQuery(Name = "amount")]
public long? AmountQueryParam { get; set; }
[SupplyParameterFromQuery(Name = "returnUrl")]
public string? ReturnUrlQueryParam { get; set; }
private bool ShowChargeForm =>
_authLoaded
&& _isClubMemberActive
&& !_redirectingToMagic
&& (!_needsMagicCeilingConsent || _allowNormalDespiteMagic);
protected override async Task OnInitializedAsync()
{
_paymentResult = PaymentQueryParam;
if (AmountQueryParam is > 0)
_chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(AmountQueryParam.Value);
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
_pendingReturnUrl = ReturnUrlQueryParam;
try
{
var userInfo = await AuthService.GetUserAuthInfo();
_isClubMemberActive = userInfo.IsClubMemberActive;
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
}
catch
{
_isClubMemberActive = false;
_hasPurchasedPackage = false;
}
finally
{
_authLoaded = true;
}
if (_isClubMemberActive)
await EvaluateMagicWalletGateAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl) && _isClubMemberActive && !_redirectingToMagic)
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
await base.OnAfterRenderAsync(firstRender);
}
private void AcceptMagicCeilingConsent()
{
_allowNormalDespiteMagic = true;
_redirectingToMagic = false;
}
private void DeclineMagicCeilingConsent()
{
_redirectingToMagic = true;
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
}
private async Task StartCreditCharge()
{
if (!_isClubMemberActive)
{
Snackbar.Add(
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.",
Severity.Warning);
return;
}
if (await EvaluateMagicWalletGateAsync())
return;
if (_needsMagicCeilingConsent && !_allowNormalDespiteMagic)
return;
if (_chargeAmount <= 0 || _isProcessing) return;
_isProcessing = true;
StateHasChanged();
try
{
if (!string.IsNullOrEmpty(_pendingReturnUrl))
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
else
{
var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
if (CreditChargeNavigation.IsValidReturnUrl(storedReturnUrl))
_pendingReturnUrl = storedReturnUrl;
}
var (success, gatewayUrl, error) = await WalletService.InitiateCreditChargeAsync(_chargeAmount);
if (success && !string.IsNullOrEmpty(gatewayUrl))
{
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
StateHasChanged();
}
}
/// <summary>
/// Magic با ظرفیت باقی‌مانده → ریدایرکت به MagicWallet.
/// Magic با سقف پر → نمایش تأیید روی صفحه برای شارژ ۱:۱.
/// true یعنی مسیر شارژ فعلی باید متوقف شود.
/// </summary>
private async Task<bool> EvaluateMagicWalletGateAsync()
{
if (_allowNormalDespiteMagic)
return false;
try
{
var status = await WalletService.GetMagicWalletStatusAsync();
if (status.WalletMode != 1)
{
_needsMagicCeilingConsent = false;
return false;
}
if (status.MagicRemainingDeposit > 0)
{
var multiplier = status.MagicMultiplier > 0 ? status.MagicMultiplier : 2.5;
var multiplierText = multiplier.ToString("0.#");
_redirectingToMagic = true;
_needsMagicCeilingConsent = false;
Snackbar.Add(
$"کیف پول شما در حالت جادویی است. شارژ از این صفحه موجودی را چندبرابر نمی‌کند. برای دریافت ضریب جادویی (×{multiplierText}) به صفحه شارژ کیف پول جادویی هدایت می‌شوید.",
Severity.Info);
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
return true;
}
_needsMagicCeilingConsent = true;
return !_allowNormalDespiteMagic;
}
catch
{
// در صورت خطا در خواندن وضعیت، مانع شارژ عادی نشو
return false;
}
}
private async Task ContinueShoppingAsync()
{
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
await CreditChargeNavigation.ClearReturnUrlAsync(JSRuntime);
if (CreditChargeNavigation.IsValidReturnUrl(returnUrl))
Navigation.NavigateTo(returnUrl!);
}
private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman);
}
@@ -0,0 +1,121 @@
@attribute [Route(RouteConstants.Profile.ChargeDiscountWallet)]
@attribute [Authorize]
<PageTitle>شارژ کیف‌پول اعتباری</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="💳 شارژ کیف‌پول اعتباری" BackHref="@RouteConstants.Profile.Wallet" />
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
@if (_paymentResult != null)
{
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
Class="rounded-lg" Variant="Variant.Filled">
@if (_paymentResult == "success")
{
<span>شارژ اعتباری با موفقیت انجام شد! ✅</span>
}
else if (_paymentResult == "cancelled")
{
<span>پرداخت توسط شما لغو شد.</span>
}
else
{
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
}
</MudAlert>
}
@* ─── فرم شارژ ─── *@
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Class="ml-1" />
شارژ موجودی اعتباری
</MudText>
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اعتباری شما اضافه می‌شود.
از موجودی اعتباری فقط برای خرید از فروشگاه اعتباری می‌توانید استفاده کنید.
</MudAlert>
<MudStack Spacing="2">
<MudNumericField @bind-Value="_chargeAmount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Min="10_000"
Max="1_000_000_000"
Format="N0"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
@* دکمه‌های مبلغ سریع *@
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts)
{
<MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
OnClick="() => _chargeAmount = preset"
Class="rounded-pill">
@FormatToman(preset)
</MudButton>
}
</MudStack>
@if (_chargeAmount > 0)
{
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #f0fdf4;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ پرداخت:</MudText>
<MudText Typo="Typo.body2"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">اعتبار دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
OnClick="StartDiscountCharge"
StartIcon="@Icons.Material.Filled.Payment"
Class="rounded-lg mt-2">
@if (_isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
</MudStack>
</MudPaper>
@* ─── قوانین ─── *@
<MudExpansionPanels Elevation="1" Class="rounded-lg">
<MudExpansionPanel Text="قوانین شارژ اعتباری" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
موجودی اعتباری فقط قابل استفاده در فروشگاه اعتباری است
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
مبلغ واریزی بدون ضریب به موجودی اضافه می‌شود (۱:۱)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
پرداخت از طریق درگاه بانکی انجام می‌شود
</MudListItem>
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
</MudStack>
</MudContainer>
@@ -0,0 +1,57 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class ChargeDiscountWallet : ComponentBase
{
private bool _isProcessing;
private long _chargeAmount;
private string? _paymentResult;
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentQueryParam { get; set; }
protected override void OnInitialized()
{
_paymentResult = PaymentQueryParam;
}
private async Task StartDiscountCharge()
{
if (_chargeAmount <= 0 || _isProcessing) return;
_isProcessing = true;
StateHasChanged();
try
{
// مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ می‌کنه
var (success, gatewayUrl, error) = await WalletService.InitiateDiscountChargeAsync(_chargeAmount);
if (success && !string.IsNullOrEmpty(gatewayUrl))
{
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
StateHasChanged();
}
}
private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman);
}
@@ -1,10 +1,11 @@
@using CMSMicroservice.Protobuf.Protos.City
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">افزودن آدرس جدید</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" Model="_request" Validation="@(_validator.ValidateValue)">
<MudForm @ref="_form" Model="_request">
<MudStack Spacing="3">
<MudTextField @bind-Value="_request.Title"
For="@(() => _request.Title)"
@@ -29,13 +30,34 @@
Required="true"
RequiredError="کد پستی الزامی است." />
<MudTextField @bind-Value="_request.CityId"
For="@(() => _request.CityId)"
Label="شناسه شهر"
Variant="Variant.Outlined"
InputType="InputType.Number"
Required="true"
RequiredError="شهر الزامی است." />
<MudAutocomplete T="object"
@bind-Value="_selectedCity"
Label="شهر"
Variant="Variant.Outlined"
SearchFunc="SearchCities"
ToStringFunc="@(city => city != null ? $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" : string.Empty)"
Required="true"
RequiredError="شهر الزامی است."
Clearable="true"
ResetValueOnEmptyText="true"
CoerceText="false"
CoerceValue="false"
Dense="true"
DebounceInterval="300"
MaxItems="20"
MinCharacters="2"
ProgressIndicatorColor="Color.Primary">
<ItemTemplate Context="city">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
<MudText>@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName))</MudText>
</MudStack>
</ItemTemplate>
<NoItemsTemplate>
<MudText Typo="Typo.body2" Class="mud-text-secondary pa-2">شهری یافت نشد</MudText>
</NoItemsTemplate>
</MudAutocomplete>
</MudStack>
</MudForm>
</DialogContent>
@@ -1,32 +1,62 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.City;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages.Profile.Components;
public record CityModel(long Id, string Name);
public partial class AddAddressDialog : ComponentBase
{
[CascadingParameter] private IDialogReference MudDialog { get; set; } = default!;
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private CityContract.CityContractClient CityContract { get; set; } = default!;
private MudForm? _form;
private readonly CreateNewUserAddressRequestValidator _validator = new();
private bool _isSaving;
private CreateNewUserAddressRequest _request = new();
private CreateCustomerAddressRequest _request = new();
private object? _selectedCity;
private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
return Enumerable.Empty<object>();
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new GetAllCitiesByFilterFilter { Name = value }
});
return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
}
catch
{
return Enumerable.Empty<object>();
}
}
private async Task SaveAddress()
{
if (_form == null) return;
await _form.Validate();
if (!_form.IsValid) return;
if (!_form.IsValid || _selectedCity == null)
{
if (_selectedCity == null)
Snackbar.Add("لطفاً شهر را انتخاب کنید.", Severity.Warning);
return;
}
_request.CityId = (long)((dynamic)_selectedCity).Id;
_isSaving = true;
try
{
var response = await UserAddressContract.CreateNewUserAddressAsync(_request);
var response = await UserAddressContract.CreateCustomerAddressAsync(_request);
Snackbar.Add("آدرس با موفقیت اضافه شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
@@ -41,5 +71,6 @@ public partial class AddAddressDialog : ComponentBase
}
}
private void Cancel() => MudDialog.Close(DialogResult.Cancel());
}
@@ -1,9 +1,10 @@
@using CMSMicroservice.Protobuf.Protos.City
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">ویرایش آدرس</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" Model="_request" Validation="@(_validator.ValidateValue)">
<MudForm @ref="_form" Model="_request">
<MudStack Spacing="3">
<MudTextField @bind-Value="_request.Title"
For="@(() => _request.Title)"
@@ -28,13 +29,34 @@
Required="true"
RequiredError="کد پستی الزامی است." />
<MudTextField @bind-Value="_request.CityId"
For="@(() => _request.CityId)"
Label="شناسه شهر"
Variant="Variant.Outlined"
InputType="InputType.Number"
Required="true"
RequiredError="شهر الزامی است." />
<MudAutocomplete T="object"
@bind-Value="_selectedCity"
Label="شهر"
Variant="Variant.Outlined"
SearchFunc="SearchCities"
ToStringFunc="@(city => city != null ? $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" : string.Empty)"
Required="true"
RequiredError="شهر الزامی است."
Clearable="true"
ResetValueOnEmptyText="true"
CoerceText="false"
CoerceValue="false"
Dense="true"
DebounceInterval="300"
MaxItems="20"
MinCharacters="2"
ProgressIndicatorColor="Color.Primary">
<ItemTemplate Context="city">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
<MudText>@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName))</MudText>
</MudStack>
</ItemTemplate>
<NoItemsTemplate>
<MudText Typo="Typo.body2" Class="mud-text-secondary pa-2">شهری یافت نشد</MudText>
</NoItemsTemplate>
</MudAutocomplete>
</MudStack>
</MudForm>
</DialogContent>
@@ -1,5 +1,5 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.City;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -9,22 +9,69 @@ namespace FrontOffice.Main.Pages.Profile.Components;
public partial class EditAddressDialog : ComponentBase
{
[CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; // updated type
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
// removed duplicate Snackbar injection; provided by Razor partial via _Imports
[Inject] private CityContract.CityContractClient CityContract { get; set; } = default!;
[Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; }
[Parameter] public CustomerAddressModel? Model { get; set; }
private MudForm? _form;
private readonly UpdateUserAddressRequestValidator _validator = new();
private bool _isSaving;
private UpdateUserAddressRequest _request = new();
private UpdateCustomerAddressRequest _request = new();
private object? _selectedCity;
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
base.OnInitialized();
await base.OnInitializedAsync();
if (Model != null)
_request = Model.Adapt<UpdateUserAddressRequest>();
{
_request = Model.Adapt<UpdateCustomerAddressRequest>();
// Load selected city if CityId exists
if (Model.CityId > 0)
{
await LoadCurrentCity(Model.CityId);
}
}
}
private async Task LoadCurrentCity(long cityId)
{
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 1 },
Filter = new GetAllCitiesByFilterFilter { Id = cityId }
});
_selectedCity = response?.Cities?.FirstOrDefault();
}
catch
{
// Ignore error loading city
}
}
private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
return Enumerable.Empty<object>();
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new GetAllCitiesByFilterFilter { Name = value }
});
return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
}
catch
{
return Enumerable.Empty<object>();
}
}
private async Task SaveAddress()
@@ -32,12 +79,18 @@ public partial class EditAddressDialog : ComponentBase
if (_form == null) return;
await _form.Validate();
if (!_form.IsValid) return;
if (!_form.IsValid || _selectedCity == null)
{
if (_selectedCity == null)
Snackbar.Add("لطفاً شهر را انتخاب کنید.", Severity.Warning);
return;
}
_request.CityId = (long)((dynamic)_selectedCity).Id;
_isSaving = true;
try
{
await UserAddressContract.UpdateUserAddressAsync(_request);
await UserAddressContract.UpdateCustomerAddressAsync(_request);
Snackbar.Add("آدرس با موفقیت ویرایش شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
@@ -1,62 +1,82 @@
@if (_currentUser != null)
{
<div class="org-chart-container">
<div class="org-tree">
<div class="org-node root-node" id="root-node">
<div class="node-card">
<div class="node-avatar">
@if (!string.IsNullOrWhiteSpace(_currentUser.Avatar))
{
<MudAvatar Size="Size.Large">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="@_currentUser.Avatar" />
</MudAvatar>
}
else
{
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Outlined">
@(string.IsNullOrWhiteSpace(_currentUser.FirstName) ? "N" : _currentUser.FirstName.Substring(0, 1))
</MudAvatar>
}
@implements IAsyncDisposable
@if (_currentUser?.Children?.Any() == true)
{
<button class="expand-btn" @onclick="ToggleExpand" data-user-id="@_currentUser.Id">
<MudIcon Icon="@(_isExpanded ? Icons.Material.Filled.Remove : Icons.Material.Filled.Add)" Size="Size.Small" />
</button>
}
</div>
<div class="node-info">
@if (!string.IsNullOrWhiteSpace(_currentUser.FirstName) || !string.IsNullOrWhiteSpace(_currentUser.LastName))
{
<div class="node-name">@_currentUser.FirstName @_currentUser.LastName</div>
}
else
{
<div class="node-name">@_currentUser.Mobile</div>
}
@* <div class="node-amounts">
<div class="personal-amount">
<span class="label">خرید شخصی:</span>
<span class="amount">@(_currentUser?.PersonalPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
<div class="team-amount">
<span class="label">خرید تیمی:</span>
<span class="amount">@(_currentUser?.TeamPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
</div>
</div> *@
</div>
</div>
</div>
@if (_isExpanded && _currentUser?.Children?.Any() == true)
{
<CascadingValue Value="this">
<OrganizationChartLevel Nodes="_currentUser.Children" Level="1" />
</CascadingValue>
}
</div>
<div class="org-chart-wrapper">
@* Toolbar *@
<div class="org-chart-toolbar">
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق" Variant="Variant.Outlined"
Dense="true" Margin="Margin.Dense" Style="width: 90px; min-width: 90px;">
<MudSelectItem T="int" Value="2">2 سطح</MudSelectItem>
<MudSelectItem T="int" Value="3">3 سطح</MudSelectItem>
<MudSelectItem T="int" Value="4">4 سطح</MudSelectItem>
<MudSelectItem T="int" Value="5">5 سطح</MudSelectItem>
<MudSelectItem T="int" Value="6">6 سطح</MudSelectItem>
<MudSelectItem T="int" Value="15">همه</MudSelectItem>
</MudSelect>
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
<MudIconButton Icon="@Icons.Material.Filled.UnfoldMore" OnClick="ExpandAll" Title="باز کردن همه" Size="Size.Small" />
<MudIconButton Icon="@Icons.Material.Filled.UnfoldLess" OnClick="CollapseAll" Title="بستن همه" Size="Size.Small" />
<MudIconButton Icon="@Icons.Material.Filled.CenterFocusStrong" OnClick="CenterChart" Title="مرکز" Size="Size.Small" />
<MudIconButton Icon="@Icons.Material.Filled.ZoomOutMap" OnClick="FitToScreen" Title="نمایش کامل" Size="Size.Small" />
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="RefreshData" Color="Color.Primary" Title="بروزرسانی" Size="Size.Small" />
</MudButtonGroup>
@if (_currentViewUserId.HasValue)
{
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
<MudIconButton Icon="@Icons.Material.Filled.ArrowForward" OnClick="GoBack" Title="بازگشت" Size="Size.Small" Color="Color.Secondary" />
<MudIconButton Icon="@Icons.Material.Filled.Home" OnClick="GoToMyTree" Title="نمودار من" Size="Size.Small" Color="Color.Secondary" />
</MudButtonGroup>
}
</div>
}
@* Chart Container *@
@if (_isLoading)
{
<div class="chart-loading">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Typo="Typo.body2" Class="mt-2">در حال بارگذاری...</MudText>
</div>
}
else if (_hasError)
{
<div class="error-message">
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mt-2">خطا در بارگذاری داده‌ها</MudText>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="RefreshData">تلاش مجدد</MudButton>
</div>
}
else
{
<div id="org-chart-container" class="org-chart-container"></div>
}
@* Stats Bar *@
@if (_statistics != null && !_isLoading)
{
<div class="org-chart-stats">
<div class="stat-item">
<span class="stat-label">کل اعضا:</span>
<span class="stat-value">@_statistics.TotalMembers</span>
</div>
<div class="stat-item">
<span class="stat-label">سازمان چپ:</span>
<span class="stat-value left">@_statistics.LeftLegCount</span>
</div>
<div class="stat-item">
<span class="stat-label">سازمان راست:</span>
<span class="stat-value right">@_statistics.RightLegCount</span>
</div>
<div class="stat-item">
<span class="stat-label">عمق:</span>
<span class="stat-value">@_statistics.TreeDepth</span>
</div>
<div class="stat-item">
<span class="stat-label">پای ضعیف:</span>
<span class="stat-value">@(_statistics.WeakerLeg == "Left" ? "چپ" : "راست")</span>
</div>
</div>
}
</div>
@@ -1,120 +1,278 @@
using FrontOffice.BFF.User.Protobuf.Protos.User;
using Mapster;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using static FrontOffice.Main.Pages.Profile.Components.OrganizationChartLevel;
using Microsoft.JSInterop;
namespace FrontOffice.Main.Pages.Profile.Components;
public partial class OrganizationChart
{
private UserNode? _currentUser;
private bool _isExpanded;
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
public partial class OrganizationChart : IAsyncDisposable
{
private NetworkTreeDto? _networkTree;
private NetworkStatisticsDto? _statistics;
private bool _isLoading = true;
private bool _hasError = false;
private int _selectedDepthValue = 3;
private DotNetObjectReference<OrganizationChart>? _dotNetHelper;
private bool _chartNeedsInit = false;
private bool _jsReady = false;
// برای نگهداری شناسه کاربر فعلی که درختش نمایش داده شده
private long? _currentViewUserId = null;
// Stack برای بازگشت به عقب
private Stack<long> _navigationHistory = new();
private int _selectedDepth
{
get => _selectedDepthValue;
set
{
if (_selectedDepthValue != value)
{
_selectedDepthValue = value;
_ = OnDepthChanged();
}
}
}
[Inject] private NetworkMembershipService NetworkService { get; set; } = default!;
// JSRuntime is injected globally via _Imports.razor;
protected override async Task OnInitializedAsync()
{
await LoadCurrentUser();
await LoadData();
}
private async Task LoadUserProfile()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsReady = true;
}
// Initialize chart after JS is ready and data is loaded
if (_jsReady && _chartNeedsInit && _networkTree?.RootNode != null)
{
_chartNeedsInit = false;
await InitializeChart();
}
}
private async Task LoadData()
{
_isLoading = true;
_hasError = false;
StateHasChanged();
try
{
_userProfile = await UserContract.GetUserAsync(request: new());
// Load tree and statistics in parallel
var treeTask = NetworkService.GetMyNetworkTreeAsync(_selectedDepth);
var statsTask = NetworkService.GetMyNetworkStatisticsAsync();
await Task.WhenAll(treeTask, statsTask);
_networkTree = await treeTask;
_statistics = await statsTask;
// Mark that chart needs to be initialized
_chartNeedsInit = true;
}
catch (Exception ex)
{
// Handle the case when user is not authenticated or API fails
_userProfile = new GetUserResponse();
Console.WriteLine($"Error loading network data: {ex.Message}");
_hasError = true;
}
StateHasChanged();
}
private async Task LoadCurrentUser()
{
await LoadUserProfile();
// Mock data - replace with actual API call
_currentUser = new UserNode
finally
{
Id = _userProfile.Id,
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Mobile = _userProfile.Mobile,
Avatar = _userProfile.AvatarPath,
PersonalPurchase = 0,
TeamPurchase = 0,
Children = await GetUserChildren(userId: _userProfile.Id)
};
}
private async Task<List<UserNode>?> GetUserChildren(long userId)
{
Console.WriteLine("OK0");
if (userId != 0)
{
Console.WriteLine("OK1");
var children = await UserContract.GetAllUserByFilterAsync(request: new()
{
Filter = new()
{
ParentId = userId,
}
});
if (children?.Models?.Any() == true)
{
Console.WriteLine("OK2");
var result = new List<UserNode>();
foreach (var item in children.Models)
{
var node = new UserNode
{
Id = item.Id,
FirstName = item.FirstName,
LastName = item.LastName,
Mobile = item.Mobile,
Avatar = item.AvatarPath,
ReferralCode = item.ReferralCode,
PersonalPurchase = 0,
TeamPurchase = 0,
Children = await GetUserChildren(userId: item.Id)
};
result.Add(node);
}
return result;
}
}
return null;
}
private void ToggleExpand()
{
_isExpanded = !_isExpanded;
StateHasChanged();
}
public void ToggleNodeExpand(long userId)
{
var node = FindNode(_currentUser, userId);
if (node != null)
{
node.IsExpanded = !node.IsExpanded;
_isLoading = false;
StateHasChanged();
}
}
private UserNode? FindNode(UserNode? node, long userId)
private async Task InitializeChart()
{
if (node == null) return null;
if (node.Id == userId) return node;
if (_networkTree?.RootNode == null) return;
if (node.Children != null)
try
{
foreach (var child in node.Children)
_dotNetHelper = DotNetObjectReference.Create(this);
// Explicit camelCase mapping (same pattern as BackOffice admin chart)
var flatData = _networkTree.ToFlatArray().Select(n => new
{
var found = FindNode(child, userId);
if (found != null) return found;
id = n.Id,
parentId = n.ParentId,
fullName = n.FullName,
mobile = n.Mobile,
avatar = n.Avatar,
position = n.Position,
level = n.Level,
isActive = n.IsActive,
isClubActive = n.IsClubActive,
activationWeekNumber = n.ActivationWeekNumber,
joinedAt = n.JoinedAt,
referralCode = n.ReferralCode,
packageName = n.PackageName ?? string.Empty
}).ToArray();
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
}
catch (Exception ex)
{
Console.WriteLine($"Error initializing chart: {ex.Message}");
}
}
private async Task RefreshData()
{
await LoadData();
if (_networkTree?.RootNode != null)
{
await InitializeChart();
}
}
private async Task OnDepthChanged()
{
await RefreshData();
}
private async Task ExpandAll()
{
try
{
await JSRuntime.InvokeVoidAsync("OrgChart.expandAll");
}
catch { }
}
private async Task CollapseAll()
{
try
{
await JSRuntime.InvokeVoidAsync("OrgChart.collapseAll");
}
catch { }
}
private async Task CenterChart()
{
try
{
await JSRuntime.InvokeVoidAsync("OrgChart.center");
}
catch { }
}
private async Task FitToScreen()
{
try
{
await JSRuntime.InvokeVoidAsync("OrgChart.fitToScreen");
}
catch { }
}
private async Task ExportPng()
{
try
{
await JSRuntime.InvokeVoidAsync("OrgChart.exportPng");
}
catch { }
}
/// <summary>
/// Called from JavaScript when a node is clicked
/// </summary>
[JSInvokable]
public async Task OnNodeClicked(long userId)
{
// اگر روی همان نود کلیک شده، کاری نکن
if (_currentViewUserId == userId) return;
// ذخیره نود فعلی در history برای بازگشت
if (_currentViewUserId.HasValue)
{
_navigationHistory.Push(_currentViewUserId.Value);
}
await LoadSubordinateTree(userId);
}
/// <summary>
/// بارگذاری درخت یک زیرمجموعه
/// </summary>
private async Task LoadSubordinateTree(long targetUserId)
{
_isLoading = true;
StateHasChanged();
try
{
_networkTree = await NetworkService.GetSubordinateTreeAsync(targetUserId, _selectedDepth);
_currentViewUserId = targetUserId;
_chartNeedsInit = true;
}
catch (UnauthorizedAccessException)
{
// نمایش پیام خطا - این کاربر زیرمجموعه نیست
Console.WriteLine("Unauthorized access to subordinate tree");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading subordinate tree: {ex.Message}");
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
/// <summary>
/// بازگشت به درخت قبلی
/// </summary>
private async Task GoBack()
{
if (_navigationHistory.Count > 0)
{
var previousUserId = _navigationHistory.Pop();
await LoadSubordinateTree(previousUserId);
}
else
{
// بازگشت به درخت خود کاربر
_currentViewUserId = null;
await LoadData();
if (_networkTree?.RootNode != null)
{
await InitializeChart();
}
}
}
return null;
/// <summary>
/// بازگشت به درخت اصلی (خود کاربر)
/// </summary>
private async Task GoToMyTree()
{
_navigationHistory.Clear();
_currentViewUserId = null;
await LoadData();
if (_networkTree?.RootNode != null)
{
await InitializeChart();
}
}
public async ValueTask DisposeAsync()
{
try
{
await JSRuntime.InvokeVoidAsync("OrgChart.dispose");
}
catch { }
_dotNetHelper?.Dispose();
}
}
@@ -9,9 +9,9 @@
@if (!string.IsNullOrWhiteSpace(node.Avatar))
{
<MudAvatar Size="@GetAvatarSize(Level)">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="@node.Avatar" />
<AppImage Path="@node.Avatar"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" />
</MudAvatar>
}
else
@@ -4,6 +4,12 @@ using MudBlazor;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Profile.Components;
/// <summary>
/// DEPRECATED: This component is no longer used.
/// d3-org-chart is now used for tree visualization.
/// Kept for backward compatibility.
/// </summary>
public partial class OrganizationChartLevel
{
[Parameter] public List<UserNode>? Nodes { get; set; }
@@ -25,18 +31,12 @@ public partial class OrganizationChartLevel
private void ToggleNodeExpand(long userId)
{
if (ParentChart != null)
{
ParentChart.ToggleNodeExpand(userId);
}
// No longer needed - d3-org-chart handles this internally
}
private void SafeToggleNodeExpand(long userId)
{
if (ParentChart != null)
{
ParentChart.ToggleNodeExpand(userId);
}
// No longer needed - d3-org-chart handles this internally
}
private async Task CopyReferralCode(string referralCode)
@@ -0,0 +1,64 @@
@attribute [Route(RouteConstants.Profile.Hub)]
<PageTitle>پروفایل</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack AlignItems="AlignItems.Center" Spacing="3" Class="mb-6">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled" Class="profile-hub-avatar">
<MudIcon Icon="@Icons.Material.Outlined.Person" Size="Size.Large" />
</MudAvatar>
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">
@($"{_userProfile.FirstName} {_userProfile.LastName}")
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(_userProfile.Mobile)</MudText>
</MudStack>
</MudStack>
<MudStack Spacing="2">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.Person" Color="Color.Primary" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">اطلاعات شخصی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">نام، کد ملی و تاریخ تولد</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.LocationOn" Color="Color.Success" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">آدرس‌ها</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های تحویل</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.Settings" Color="Color.Warning" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">تنظیمات</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">تغییر رمز و تنظیمات حساب</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
</MudStack>
</MudContainer>
@@ -0,0 +1,34 @@
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Profile;
public partial class Hub
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await LoadUserProfile();
}
}
private async Task LoadUserProfile()
{
try
{
_userProfile = await UserContract.GetUserAsync(request: new());
}
catch
{
_userProfile = new GetUserResponse();
}
StateHasChanged();
}
}
+176 -179
View File
@@ -5,197 +5,194 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudGrid Spacing="4">
<!-- Profile Header -->
@* ═══════════════════════════════════════════════
1. PROFILE HEADER — Modern gradient card
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-6">
<MudStack Spacing="4">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Large" Color="Color.Primary">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" />
</MudAvatar>
<div>
<MudText Typo="Typo.h5">@($"{_userProfile.FirstName} {_userProfile.LastName}")</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(_userProfile.Mobile)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali())</MudText>
</div>
</MudStack>
<MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Financing">تامین اعتبار خرید</MudButton>
</MudStack>
<!-- Referral Code Section -->
<MudDivider />
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
<MudPaper Elevation="0" Class="dash-header-card pa-5 pa-md-6 rounded-xl">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudAvatar Size="Size.Large" Class="dash-avatar">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" />
</MudAvatar>
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.h5" Class="dash-hero-name">
@($"{_userProfile.FirstName} {_userProfile.LastName}")
</MudText>
<MudText Typo="Typo.body2" Class="dash-hero-sub">
@(_userProfile.Mobile)
</MudText>
<MudText Typo="Typo.caption" Class="dash-hero-hint">
عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali())
</MudText>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Class="flex-grow-1"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode" />
</MudStack>
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success">@(_copyMessage)</MudText>
}
</MudStack>
<MudButton Variant="Variant.Filled"
Class="rounded-pill dash-hero-btn"
Href="https://dayadiamond.ir/"
StartIcon="@Icons.Material.Filled.Diamond">
سامانه دایا
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
<!-- Wallet Highlight Row -->
@* ═══════════════════════════════════════════════
2. WALLET STRIP — Compact inline stats
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudPaper Elevation="3" Class="pa-4 rounded-lg gradient-border">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletCredit</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی تخفیف باشگاه</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletDiscount</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی شبکه</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletNetwork</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" md="4" Class="d-flex justify-end">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AccountBalanceWallet" Href="@RouteConstants.Profile.Wallet">جزئیات کیف پول</MudButton>
</MudItem>
</MudGrid>
<MudPaper Elevation="1" Class="pa-2 pa-md-4 rounded-xl">
<MudStack Row="true" Justify="Justify.SpaceAround" AlignItems="AlignItems.Center" Class="flex-wrap wallet-strip">
<div class="wallet-strip-item">
<div class="wallet-strip-label">
<MudIcon Icon="@Icons.Material.Outlined.CreditCard" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">اصلی</MudText>
</div>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@_walletCredit</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<div class="wallet-strip-item">
<div class="wallet-strip-label">
<MudIcon Icon="@Icons.Material.Outlined.Discount" Size="Size.Small" Color="Color.Success" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">کیف اعتباری</MudText>
</div>
<MudText Typo="Typo.subtitle2" Color="Color.Success">@_walletDiscount</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<div class="wallet-strip-item">
<div class="wallet-strip-label">
<MudIcon Icon="@Icons.Material.Outlined.Groups" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">پاداش های دریافتی</MudText>
</div>
<MudText Typo="Typo.subtitle2" Color="Color.Warning">@_walletNetwork</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
Href="@RouteConstants.Profile.Wallet" EndIcon="@Icons.Material.Filled.ArrowBack">
کیف پول
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
<!-- Profile Content as tiles -->
@* ═══════════════════════════════════════════════
3. REFERRAL SECTION
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-4">
<MudGrid Spacing="3">
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">اطلاعات شخصی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">نمایش و ویرایش اطلاعات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">آدرس‌ها</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های شما</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Tree" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">شجره‌نامه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">ساختار شبکه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Settings" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">تنظیمات حساب</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">اعلان‌ها و تنظیمات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">فروشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مشاهده و خرید محصولات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Wallet" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">کیف پول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Club.Membership" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Size="Size.Large" Color="Color.Secondary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">باشگاه مشتریان</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">عضویت و مزایا</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Network.Statistics" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Groups" Size="Size.Large" Color="Color.Info" />
<MudText Typo="Typo.subtitle1" Class="mt-2">شبکه من</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">آمار و درخت شبکه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Commission.Dashboard" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Payments" Size="Size.Large" Color="Color.Success" />
<MudText Typo="Typo.subtitle1" Class="mt-2">کمیسیون</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">درآمد و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
</MudGrid>
</MudPaper>
@if (CanShowReferralLink)
{
<MudPaper Elevation="1" Class="pa-5 rounded-xl">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Share" Color="Color.Primary" />
<MudText Typo="Typo.h6">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
Class="rounded-pill"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
</MudText>
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode"
Class="rounded-lg" />
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText>
}
</MudPaper>
}
else
{
<MudPaper Elevation="0" Class="pa-6 rounded-xl text-center" Style="border:2px dashed var(--mud-palette-primary); background:rgba(99,102,241,.04);">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Large" Color="Color.Primary" Class="mb-2" Style="font-size:2.5rem;" />
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-2">
لینک دعوت شما هنوز فعال نشده است
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
برای فعال‌سازی لینک دعوت، ابتدا یکی از <strong>پکیج‌ها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Class="rounded-pill"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions">
تهیه پکیج
</MudButton>
</MudPaper>
}
</MudItem>
@* ═══════════════════════════════════════════════
3.5 LATEST BLOG BANNER
═══════════════════════════════════════════════ *@
@if (_latestPost is not null)
{
<MudItem xs="12">
<MudLink Href="@($"/blog/{_latestPost.Slug}")" Underline="Underline.None">
<MudPaper Elevation="1" Class="rounded-xl dash-blog-banner overflow-hidden">
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2" Spacing="2">
@if (!string.IsNullOrWhiteSpace(_latestPost.ThumbnailUrl))
{
<div style="width:72px;height:72px;min-width:72px;border-radius:10px;overflow:hidden;">
<AppImage Path="@_latestPost.ThumbnailUrl" Alt="@_latestPost.Title"
ObjectFit="ObjectFit.Cover" Style="width:100%;height:100%;" />
</div>
}
else
{
<MudAvatar Rounded="true" Size="Size.Large" Color="Color.Primary" Variant="Variant.Outlined">
<MudIcon Icon="@Icons.Material.Outlined.Article" />
</MudAvatar>
}
<MudStack Spacing="0" Class="flex-grow-1" Style="min-width:0;">
<MudText Typo="Typo.caption" Color="Color.Primary" Style="font-size:0.7rem;">جدیدترین مطلب</MudText>
<MudText Typo="Typo.body2" Class="dash-blog-title" Style="font-size:0.85rem;">@_latestPost.Title</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
</MudStack>
</MudPaper>
</MudLink>
</MudItem>
}
@* ═══════════════════════════════════════════════
4. QUICK ACCESS TILES — Modern grid
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudText Typo="Typo.h6" Class="mb-3">دسترسی سریع</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var tile in _dashTiles)
{
<MudItem xs="6" sm="4" md="3">
<MudLink Href="@tile.Href" Underline="Underline.None" Class="tile-link">
<MudPaper Elevation="0" Class="pa-4 rounded-xl dash-tile text-center">
<MudAvatar Size="Size.Medium" Class="mx-auto mb-2" Style="@tile.AvatarStyle">
<MudIcon Icon="@tile.Icon" Size="Size.Medium" />
</MudAvatar>
<MudText Typo="Typo.subtitle2">@tile.Title</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tile.Subtitle</MudText>
</MudPaper>
</MudLink>
</MudItem>
}
</MudGrid>
</MudItem>
</MudGrid>
</MudContainer>
+221 -26
View File
@@ -1,8 +1,5 @@
using FluentValidation;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using FrontOffice.Main.Pages.Profile.Components;
using FrontOffice.Main.Utilities;
using Mapster;
@@ -10,6 +7,11 @@ using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using System.ComponentModel.DataAnnotations;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Validator.User;
using CMSMicroservice.Protobuf.Validator.UserAddress;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages.Profile;
@@ -18,10 +20,13 @@ public partial class Index
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private GetUserForCustomerResponse _userProfile = new();
private BlogPostCardDto? _latestPost;
private UpdateCustomerProfileRequest _updateUserRequest = new();
private MudForm? _personalForm;
@@ -32,8 +37,34 @@ public partial class Index
private DateTime? _date;
// Referral Link Control
private bool _hasPurchasedPackage;
private bool _isClubMemberActive;
private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive;
// Purchase flow
private bool _isProcessingPayment;
private int _purchaseCycleCount; // تعداد دورهای خرید — اگر > 0 فقط IPG مجاز
// Dashboard Tiles
private readonly List<DashTile> _dashTiles = new()
{
new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Profile.Addresses, Icons.Material.Filled.LocationOn, "آدرس‌ها", "مدیریت آدرس‌ها", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "سازمان فروش", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاه‌ها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیف‌پول جادویی", "شارژ چند‌برابری", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"),
new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"),
new(RouteConstants.Commission.Dashboard, Icons.Material.Filled.Payments, "پاداش", "درآمد و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
};
private record DashTile(string Href, string Icon, string Title, string Subtitle, string AvatarStyle);
// Address management
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private List<CustomerAddressModel> _addresses = new();
private bool _isLoadingAddresses;
private CreateNewUserAddressRequest _newAddressRequest = new();
private UpdateUserAddressRequest _editAddressRequest = new();
@@ -44,9 +75,66 @@ public partial class Index
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// Refresh token to get latest user claims
await AuthService.RefreshTokenAsync();
await LoadUserAuthInfo();
await LoadUserProfile();
await LoadAddresses();
await LoadWallet();
await LoadLatestPost();
// نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده
await CheckAndShowClubContractModal();
}
}
private async Task LoadUserAuthInfo()
{
try
{
var userInfo = await AuthService.GetUserAuthInfo();
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
_isClubMemberActive = userInfo.IsClubMemberActive;
}
catch
{
_hasPurchasedPackage = false;
_isClubMemberActive = false;
}
StateHasChanged();
}
/// <summary>
/// بررسی و نمایش Modal قرارداد باشگاه مشتریان
/// اگر کاربر پکیج خریده ولی هنوز قرارداد باشگاه را امضا نکرده
/// </summary>
private async Task CheckAndShowClubContractModal()
{
// اگر پکیج خریده ولی باشگاه فعال نیست = باید قرارداد امضا کند
if (_hasPurchasedPackage && !_isClubMemberActive)
{
var options = new DialogOptions
{
BackdropClick = false, // غیرقابل بسته شدن با کلیک بیرون
CloseOnEscapeKey = false, // غیرقابل بسته شدن با Escape
CloseButton = false, // بدون دکمه بستن
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<Shared.ClubMembershipContractDialog>(
"قرارداد باشگاه مشتریان",
options);
var result = await dialog.Result;
// اگر موفق بود، صفحه را refresh کنیم
if (!result.Canceled)
{
await LoadUserAuthInfo();
StateHasChanged();
}
}
}
@@ -60,24 +148,57 @@ public partial class Index
var discount = FormatPrice(b.DiscountBalance);
_walletNetwork = FormatPrice(b.NetworkBalance);
_walletDiscount = discount;
// بارگذاری تعداد دورهای خرید برای کنترل روش‌های پرداخت
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
_purchaseCycleCount = magicStatus.PurchaseCycleCount;
StateHasChanged();
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task LoadLatestPost()
{
try
{
var posts = await BlogPostService.GetFeaturedPostsAsync(1);
if (!posts.Any())
{
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 1);
posts = result.Posts;
}
_latestPost = posts.FirstOrDefault();
}
catch
{
_latestPost = null;
}
StateHasChanged();
}
private async Task LoadUserProfile()
{
try
{
_userProfile = await UserContract.GetUserAsync(request: new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
_userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
if (_userProfile.BirthDate != null)
{
_date = _userProfile.BirthDate.ToDateTime();
_updateUserRequest.BirthDate = _userProfile.BirthDate;
}
}
catch (Exception ex)
{
// Handle the case when user is not authenticated or API fails
_userProfile = new GetUserResponse();
_userProfile = new GetUserForCustomerResponse();
}
StateHasChanged();
}
@@ -99,7 +220,7 @@ public partial class Index
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(request: _updateUserRequest);
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
await LoadUserProfile();
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
@@ -182,20 +303,20 @@ public partial class Index
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(request: new());
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
}
else
{
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
_addresses = new List<CustomerAddressModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
_addresses = new List<CustomerAddressModel>();
}
finally
{
@@ -212,10 +333,11 @@ public partial class Index
if (!result.Canceled)
{
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
}
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
@@ -264,6 +386,7 @@ public partial class Index
});
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
catch (Exception ex)
{
@@ -272,33 +395,105 @@ public partial class Index
}
}
private async Task Financing()
private async Task OpenPurchaseOptions()
{
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true,
BackdropClick = true
};
var parameters = new DialogParameters<Shared.PackagePurchaseDialog>
{
{ x => x.PurchaseCycleCount, _purchaseCycleCount }
};
var dialog = await DialogService.ShowAsync<Shared.PackagePurchaseDialog>(
string.Empty, parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
if (result.Data is Shared.PackagePurchaseDialog.PackagePurchaseResult purchase)
{
switch (purchase.Method)
{
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
await ProcessDirectPayment(purchase.Package.Id);
break;
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
await ProcessDayaLoanPayment();
break;
}
}
}
private async Task ProcessDirectPayment(long packageId)
{
if (_isProcessingPayment) return;
_isProcessingPayment = true;
StateHasChanged();
try
{
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
{
PackageId = packageId,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
});
if (!response.Success)
{
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
return;
}
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در فرآیند پرداخت: {ex.Message}", Severity.Error);
}
finally
{
_isProcessingPayment = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task ProcessDayaLoanPayment()
{
if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName))
{
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
return;
}
if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName))
{
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
return;
}
if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
{
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
return;
}
if (_updateUserRequest.BirthDate == null)
{
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
return;
}
if (!_addresses.Any())
{
Snackbar.Add($"آدرس محل سکونت شما الزمای است!", Severity.Error);
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
return;
}
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
@@ -0,0 +1,229 @@
@attribute [Route(RouteConstants.Profile.MagicWallet)]
@attribute [Authorize]
<PageTitle>کیف‌پول جادویی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="🪄 کیف‌پول جادویی" BackHref="@RouteConstants.Profile.Wallet" />
@if (_isLoading)
{
<MudPaper Elevation="2" Class="pa-6 rounded-lg text-center">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-3">در حال بارگذاری...</MudText>
</MudPaper>
}
else if (_status.WalletMode != 1)
{
@* ═══ حالت عادی — Magic فعال نیست ═══ *@
<MudPaper Elevation="2" Class="pa-6 rounded-lg text-center">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Default" Size="Size.Large" Style="font-size: 64px;" />
<MudText Typo="Typo.h5" Class="mt-4">کیف‌پول جادویی فعال نیست</MudText>
<MudText Typo="Typo.body1" Class="mt-2 mud-text-secondary">
برای فعال شدن کیف‌پول جادویی، ابتدا پکیج پایه را خریداری کنید و سپس
تمام موجودی اصلی خود را از فروشگاه خرج کنید. وقتی موجودی اصلی به صفر برسد،
کیف‌پول جادویی فعال می‌شود.
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4 rounded-pill"
Href="@RouteConstants.Profile.Wallet"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
بازگشت به کیف پول
</MudButton>
</MudPaper>
}
else
{
@* ═══ حالت جادویی فعال ═══ *@
@* ── G4: هشدار تنظیمات پکیج‌بیس (Q28) ── *@
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Warning">
سقف واریز و ضریب اعتبار کیف‌پول جادویی بر اساس پکیج شما تنظیم شده است. پکیج‌های بالاتر سقف و ضریب بیشتری دارند.
</MudAlert>
@* ─── وضعیت و پیشرفت ─── *@
<MudPaper Elevation="2" Class="pa-5 rounded-xl" Style="background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); color: white;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Large" />
<MudText Typo="Typo.h5">حالت جادویی فعال</MudText>
</MudStack>
<MudGrid Spacing="2">
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">موجودی فعلی</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.Balance)</MudText>
</MudItem>
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">مجموع واریزی</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicTotalDeposited)</MudText>
</MudItem>
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">مجموع اعتبار</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicTotalCredited)</MudText>
</MudItem>
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">سقف باقیمانده</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicRemainingDeposit)</MudText>
</MudItem>
</MudGrid>
@* نوار پیشرفت *@
<MudStack Spacing="0" Class="mt-1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Style="opacity:0.8">پیشرفت سقف واریز</MudText>
<MudText Typo="Typo.caption" Style="opacity:0.8">@(_progressPercent.ToString("0"))%</MudText>
</MudStack>
<MudProgressLinear Value="@_progressPercent" Color="Color.Warning" Size="Size.Medium"
Rounded="true" Class="mt-1" Style="background: rgba(255,255,255,0.3);" />
</MudStack>
@if (_status.MagicActivatedAt.HasValue)
{
<MudText Typo="Typo.caption" Style="opacity:0.7">
فعال از: @_status.MagicActivatedAt.Value.MiladiToJalaliWithTime()
</MudText>
}
</MudStack>
</MudPaper>
@* ─── فرم شارژ ─── *@
@if (_status.MagicRemainingDeposit > 0)
{
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Class="ml-1" />
شارژ کیف‌پول جادویی
</MudText>
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ واریزی شما <strong>×@_status.MagicMultiplier.ToString("0.#")</strong> به موجودی اضافه می‌شود.
مثلاً ۱۰ میلیون واریز = @FormatToman((long)(10_000_000 * (decimal)_status.MagicMultiplier)) اعتبار.
</MudAlert>
<MudStack Spacing="2">
<MudNumericField @bind-Value="_chargeAmount"
Label="مبلغ واریز (تومان)"
Variant="Variant.Outlined"
Min="10_000"
Max="@_status.MagicRemainingDeposit"
Format="N0"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="@($"حداکثر: {FormatPrice(_status.MagicRemainingDeposit)} (تومان)")" />
@if (_chargeAmount > 0)
{
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #f0fdf4;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ واریز:</MudText>
<MudText Typo="Typo.body2">@FormatToman(_chargeAmount)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success"><strong>اعتبار دریافتی (×@_status.MagicMultiplier.ToString("0.#")):</strong></MudText>
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman((long)(_chargeAmount * (decimal)_status.MagicMultiplier))</strong></MudText>
</MudStack>
</MudPaper>
}
@* دکمه‌های مبلغ سریع *@
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts)
{
if (preset <= _status.MagicRemainingDeposit)
{
<MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
OnClick="() => _chargeAmount = preset"
Class="rounded-pill">
@FormatToman(preset)
</MudButton>
}
}
</MudStack>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
OnClick="StartMagicCharge"
StartIcon="@Icons.Material.Filled.Payment"
Class="rounded-lg mt-2">
@if (_isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
</MudStack>
</MudPaper>
}
else
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
<MudStack Spacing="2">
<MudText>
سقف شارژ جادویی در این دور پر شده است.
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست می‌شود.
</MudText>
<MudText Typo="Typo.body2">
اگر برای تکمیل خرید به موجودی بیشتری نیاز دارید، می‌توانید به‌صورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg align-self-start">
شارژ عادی بدون ضریب
</MudButton>
</MudStack>
</MudAlert>
}
@* ─── قوانین ─── *@
<MudExpansionPanels Elevation="1" Class="rounded-lg">
<MudExpansionPanel Text="قوانین کیف‌پول جادویی" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
هر مبلغ واریزی ×@_status.MagicMultiplier.ToString("0.#") به موجودی اضافه می‌شود
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
سقف واریز در هر دور: @FormatPrice(_status.MagicMaxDeposit)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداکثر اعتبار: @FormatPrice(_status.MagicMaxCredit)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning">
در حالت جادویی، کمیسیون و پاداش های دریافتی غیرفعال است
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
بعد از اتمام سقف و خرج موجودی، با خرید مجدد پکیج دور جدید شروع می‌شود
</MudListItem>
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
}
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
@if (_paymentResult != null)
{
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
Class="rounded-lg" Variant="Variant.Filled">
@if (_paymentResult == "success")
{
<span>شارژ جادویی با موفقیت انجام شد! ✅</span>
}
else
{
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
}
</MudAlert>
}
</MudStack>
</MudContainer>
@@ -0,0 +1,77 @@
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class MagicWallet : ComponentBase
{
private MagicWalletStatus _status = new(0, 0, 0, 0, 0, 0, null, 0);
private bool _isLoading = true;
private bool _isProcessing;
private long _chargeAmount;
private double _progressPercent;
private string? _paymentResult;
private readonly long[] _presetAmounts = { 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000, 100_000_000 };
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentQueryParam { get; set; }
protected override async Task OnInitializedAsync()
{
_paymentResult = PaymentQueryParam;
await LoadStatus();
}
private async Task LoadStatus()
{
_isLoading = true;
_status = await WalletService.GetMagicWalletStatusAsync();
_progressPercent = _status.MagicMaxDeposit > 0
? (double)_status.MagicTotalDeposited / _status.MagicMaxDeposit * 100
: 0;
_isLoading = false;
}
private async Task StartMagicCharge()
{
if (_chargeAmount <= 0 || _isProcessing) return;
_isProcessing = true;
StateHasChanged();
try
{
// مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ می‌کنه
var (success, gatewayUrl, error) = await WalletService.InitiateMagicChargeAsync(_chargeAmount);
if (success && !string.IsNullOrEmpty(gatewayUrl))
{
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
StateHasChanged();
}
}
private static string FormatPrice(long price)
=> string.Format("{0:N0} تومان", price);
private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman);
}
@@ -0,0 +1,375 @@
@page "/profile/payment-callback"
@attribute [Authorize]
@using Blazored.LocalStorage
@using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.User
@using FrontOffice.Main.Utilities
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" Class="py-8">
<MudPaper Class="pa-6 text-center">
@if (_isLoading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.h6" Class="mt-4">در حال بررسی وضعیت پرداخت...</MudText>
}
else if (_isSuccess)
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" Style="font-size: 80px" />
<MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">پرداخت موفق</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
@if (_transactionId > 0)
{
<MudText Typo="Typo.body2" Class="mt-3">
<strong>کد ارجاعی:</strong> @_transactionId
</MudText>
}
<MudDivider Class="my-4" />
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی کیف پول اصلی: @FormatPrice(_walletBalance)
</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="@_successReturnUrl">
@_successReturnText
</MudButton>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" Style="font-size: 80px" />
<MudText Typo="Typo.h5" Color="Color.Error" Class="mt-4">پرداخت ناموفق</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="@_failReturnUrl">
@_failReturnText
</MudButton>
<MudButton Color="Color.Secondary" Variant="Variant.Outlined" Class="mt-3 mr-2"
OnClick="RetryPayment">
تلاش مجدد
</MudButton>
}
</MudPaper>
</MudContainer>
@code {
[Inject] private PackageContract.PackageContractClient PackageContractClient { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserContractClient { get; set; } = default!;
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private NavigationManager NavManager { get; set; } = default!;
private const string TokenStorageKey = "auth:token";
/// <summary>
/// نوع پرداخت: package (پیش‌فرض) | magic-wallet | discount-wallet | discount-order
/// </summary>
[SupplyParameterFromQuery(Name = "type")]
private string? PaymentType { get; set; }
[SupplyParameterFromQuery(Name = "orderId")]
private long OrderId { get; set; }
[SupplyParameterFromQuery(Name = "transactionId")]
private long TransactionId { get; set; }
[SupplyParameterFromQuery(Name = "Authority")]
private string? Authority { get; set; }
[SupplyParameterFromQuery(Name = "Status")]
private string? Status { get; set; }
private bool _isLoading = true;
private bool _isSuccess;
private string _message = string.Empty;
private long _transactionId;
private long _walletBalance;
private bool _verifyCompleted;
private readonly SemaphoreSlim _verifyGate = new(1, 1);
// دکمه‌های بازگشت بسته به نوع پرداخت
private string _successReturnUrl = "/profile";
private string _successReturnText = "بازگشت به پروفایل";
private string _failReturnUrl = "/profile";
private string _failReturnText = "بازگشت به پروفایل";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !_verifyCompleted)
{
await VerifyPaymentOnceAsync();
}
}
/// <summary>
/// Blazor Server may invoke render/verify twice in quick succession — gate with semaphore.
/// </summary>
private async Task VerifyPaymentOnceAsync()
{
if (_verifyCompleted) return;
await _verifyGate.WaitAsync();
try
{
if (_verifyCompleted) return;
await VerifyPayment();
_verifyCompleted = true;
}
finally
{
_verifyGate.Release();
}
}
private async Task VerifyPayment()
{
try
{
// تعیین نوع پرداخت — اگر type نباشد، پیش‌فرض خرید پکیج
var type = PaymentType?.ToLowerInvariant() ?? "package";
switch (type)
{
case "magic-wallet":
await VerifyMagicWalletCharge();
break;
case "discount-wallet":
await VerifyDiscountWalletCharge();
break;
case "credit-wallet":
await VerifyCreditWalletCharge();
break;
case "discount-order":
await VerifyDiscountOrderPayment();
break;
default: // "package" or missing
await VerifyPackagePurchase();
break;
}
}
catch (Exception ex)
{
_isSuccess = false;
_message = $"خطا در بررسی وضعیت پرداخت: {ex.Message}";
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
/// <summary>
/// تأیید خرید پکیج — رفتار فعلی
/// </summary>
private async Task VerifyPackagePurchase()
{
_successReturnUrl = "/profile";
_successReturnText = "بازگشت به پروفایل";
_failReturnUrl = "/profile";
_failReturnText = "بازگشت به پروفایل";
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
return;
}
var response = await PackageContractClient.CustomerVerifyPackagePurchaseAsync(new CustomerVerifyPackagePurchaseRequest
{
OrderId = OrderId,
Authority = Authority ?? string.Empty,
Status = Status ?? string.Empty
});
_isSuccess = response.Success;
_message = response.Message;
_transactionId = response.TransactionId;
if (_isSuccess)
{
await UpdateWalletBalanceAndRefreshToken();
}
}
/// <summary>
/// تأیید شارژ کیف‌پول جادویی
/// </summary>
private async Task VerifyMagicWalletCharge()
{
_successReturnUrl = "/profile/magic-wallet";
_successReturnText = "بازگشت به کیف پول جادویی";
_failReturnUrl = "/profile/magic-wallet";
_failReturnText = "بازگشت به کیف پول جادویی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyMagicChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید شارژ کیف پول تخفیفی
/// </summary>
private async Task VerifyDiscountWalletCharge()
{
_successReturnUrl = "/profile/charge-discount-wallet";
_successReturnText = "بازگشت به کیف پول تخفیفی";
_failReturnUrl = "/profile/charge-discount-wallet";
_failReturnText = "بازگشت به کیف پول تخفیفی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyDiscountChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید شارژ کیف پول اصلی
/// </summary>
private async Task VerifyCreditWalletCharge()
{
_successReturnUrl = "/profile/charge-credit-wallet?payment=success";
_successReturnText = "بازگشت به شارژ کیف پول اصلی";
_failReturnUrl = "/profile/charge-credit-wallet?payment=failed";
_failReturnText = "بازگشت به شارژ کیف پول اصلی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyCreditChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید پرداخت سفارش فروشگاه تخفیفی
/// </summary>
private async Task VerifyDiscountOrderPayment()
{
_successReturnUrl = $"/discount-store/order/{OrderId}";
_successReturnText = "مشاهده سفارش";
_failReturnUrl = $"/discount-store/order/{OrderId}";
_failReturnText = "مشاهده سفارش";
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
return;
}
var (success, message, _) = await DiscountOrderService.VerifyDiscountOrderPaymentAsync(
OrderId, Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
}
/// <summary>
/// بروزرسانی موجودی کیف پول
/// </summary>
private async Task UpdateWalletBalance()
{
try
{
var balances = await WalletService.GetBalancesAsync();
_walletBalance = balances.CreditBalance;
}
catch { /* اگر خطا شد، صفر نمایش بده */ }
}
/// <summary>
/// بروزرسانی موجودی + refresh توکن (فقط برای خرید پکیج)
/// </summary>
private async Task UpdateWalletBalanceAndRefreshToken()
{
await UpdateWalletBalance();
await RefreshTokenAsync();
}
private void RetryPayment()
{
var type = PaymentType?.ToLowerInvariant() ?? "package";
var url = type switch
{
"magic-wallet" => "/profile/magic-wallet",
"discount-wallet" => "/profile/charge-discount-wallet",
"credit-wallet" => "/profile/charge-credit-wallet",
"discount-order" => "/discount-store",
_ => "/profile"
};
NavManager.NavigateTo(url, forceLoad: true);
}
/// <summary>
/// Refresh token after successful payment to update PackagePurchaseMethod claim
/// </summary>
private async Task RefreshTokenAsync()
{
try
{
var userResponse = await UserContractClient.GetUserForCustomerAsync(new CMSMicroservice.Protobuf.Protos.User.GetUserForCustomerRequest());
if (!string.IsNullOrWhiteSpace(userResponse.Token))
{
// ذخیره توکن جدید در localStorage
await LocalStorage.SetItemAsync(TokenStorageKey, userResponse.Token);
// به‌روزرسانی UserAuthInfo با claims جدید
await AuthService.InitUserAuthInfo();
}
}
catch
{
// اگر refresh ناموفق بود، پرداخت همچنان موفق است
// کاربر می‌تواند با logout/login دوباره token جدید بگیرد
}
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
}
@@ -4,13 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">اطلاعات شخصی</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="اطلاعات شخصی" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudForm @ref="_personalForm" Model="_updateUserRequest" Validation="@(_personalValidator.ValidateValue)">
<MudForm @ref="_personalForm" Model="_updateUserRequest">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.FirstName"
@@ -1,8 +1,6 @@
using FluentValidation;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Utilities;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
@@ -13,9 +11,8 @@ public partial class Personal : ComponentBase
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private GetUserForCustomerResponse _userProfile = new();
private UpdateCustomerProfileRequest _updateUserRequest = new();
private MudForm? _personalForm;
private DateTime? _date;
@@ -30,8 +27,14 @@ public partial class Personal : ComponentBase
{
try
{
_userProfile = await UserContract.GetUserAsync(new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
_userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
if (_userProfile.BirthDate != null)
_date = _userProfile.BirthDate.ToDateTime();
}
@@ -56,7 +59,7 @@ public partial class Personal : ComponentBase
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(_updateUserRequest);
await UserContract.UpdateCustomerProfileAsync(_updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
catch (Exception ex)
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تنظیمات حساب</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="تنظیمات حساب" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
@@ -1,5 +1,4 @@
using FrontOffice.BFF.User.Protobuf.Protos.User;
using Mapster;
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -16,8 +15,18 @@ public partial class Settings : ComponentBase
{
try
{
var user = await UserContract.GetUserAsync(new());
_request = user.Adapt<UpdateUserRequest>();
var user = await UserContract.GetUserForCustomerAsync(new());
_request = new UpdateUserRequest
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
NationalCode = user.NationalCode,
EmailNotifications = user.EmailNotifications,
SmsNotifications = user.SmsNotifications,
PushNotifications = user.PushNotifications,
};
}
catch
{
@@ -1,14 +1,11 @@
@attribute [Route(RouteConstants.Profile.Tree)]
@using FrontOffice.Main.Pages.Profile.Components
<PageTitle>شجره‌نامه</PageTitle>
<PageTitle>سازمان فروش</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">شجره‌نامه</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="سازمان فروش" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<OrganizationChart />
+215 -121
View File
@@ -4,106 +4,207 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">کیف پول</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="کیف پول" BackHref="@RouteConstants.Profile.Index" />
<MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اصلی</MudText>
<MudText Typo="Typo.h4" Color="Color.Primary">@_balances.Credit</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-warning-lighten);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Discount" Color="Color.Warning" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2" Style="color: var(--mud-palette-warning-darken);">موجودی تخفیف باشگاه</MudText>
<MudText Typo="Typo.h4" Style="color: var(--mud-palette-warning-darken);"><strong>@_balances.Discount</strong></MudText>
</MudStack>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">پاداش های دریافتی</MudText>
<MudText Typo="Typo.h4" Color="Color.Success">@_balances.Network</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی شبکه</MudText>
<MudText Typo="Typo.h4" Color="Color.Success">@_balances.Network</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
<MudText Typo="Typo.h4" Color="Color.Warning">@_balances.Discount</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">درخواست برداشت از موجودی شبکه</MudText>
<MudStack Spacing="2">
<MudTextField @bind-Value="_withdrawPayoutId"
Label="شناسه واریز (PayoutId)"
Variant="Variant.Outlined"
Type="MudBlazor.InputType.Number"
Required="true" />
<MudRadioGroup T="WithdrawalMethodClient" @bind-SelectedOption="_withdrawMethod" Row="true">
<MudRadio T="WithdrawalMethodClient" Option="@WithdrawalMethodClient.Cash" Label="برداشت نقدی (نیاز به شبا)" />
<MudRadio T="WithdrawalMethodClient" Option="@WithdrawalMethodClient.Diamond" Label="الماس/غیرنقدی" />
</MudRadioGroup>
<MudTextField @bind-Value="_withdrawIban"
Label="شماره شبا"
Variant="Variant.Outlined"
Disabled="_withdrawMethod == WithdrawalMethodClient.Diamond"
Placeholder="IRxxxxxxxxxxxx"
Adornment="Adornment.Start"
AdornmentText="IR" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
حداقل مبلغ برداشت: @FormatPrice(_minWithdrawalAmount)
</MudText>
<MudButton Disabled="_isSubmittingWithdrawal"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Outbound"
OnClick="SubmitWithdrawal">
@(_isSubmittingWithdrawal ? "در حال ثبت..." : "ثبت درخواست برداشت")
<!-- دکمه دسترسی سریع به درخواست برداشت -->
<MudButton Variant="Variant.Filled"
Color="Color.Warning"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.RequestPage"
Href="@RouteConstants.Profile.WithdrawalRequests"
Class="rounded-lg">
درخواست‌های برداشت
</MudButton>
<!-- دکمه شارژ کیف‌پول اصلی / جادویی -->
@if (_isClubMemberActive)
{
@if (_isMagicWallet)
{
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AutoAwesome"
Href="@RouteConstants.Profile.MagicWallet"
Class="rounded-lg">
شارژ کیف‌پول جادویی
</MudButton>
</MudStack>
</MudPaper>
@if (_magicCeilingFull)
{
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
سقف شارژ جادویی این دور پر است. در صورت نیاز می‌توانید به‌صورت استثنایی بدون ضریب شارژ کنید.
</MudAlert>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ عادی بدون ضریب
</MudButton>
}
else
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
کیف پول شما در حالت جادویی است؛ شارژ از مسیر جادویی با ضریب چندبرابر اعمال می‌شود.
</MudAlert>
}
}
else
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
}
}
else
{
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Disabled="true"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
</MudAlert>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@if (!_hasPurchasedPackage)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
Href="@RouteConstants.Package.List"
StartIcon="@Icons.Material.Filled.CardGiftcard">
مشاهده پکیج‌ها
</MudButton>
}
else
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
Href="@RouteConstants.Club.Membership"
StartIcon="@Icons.Material.Filled.Handshake">
امضای قرارداد باشگاه
</MudButton>
}
</MudStack>
</MudStack>
</MudPaper>
}
<!-- دکمه شارژ کیف‌پول اعتباری -->
<MudButton Variant="Variant.Filled"
Color="Color.Info"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.CreditCard"
Href="@RouteConstants.Profile.ChargeDiscountWallet"
Class="rounded-lg">
شارژ کیف‌پول اعتباری
</MudButton>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">تراکنش‌ها و مسیرهای شارژ</MudText>
<MudStack Row="true" Spacing="2" Class="mb-3" AlignItems="AlignItems.End">
<MudTextField @bind-Value="_filterReferenceId"
Label="شناسه ارجاع"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" />
<MudSelect T="string" @bind-Value="_filterType" Label="نوع تراکنش" Variant="Variant.Outlined">
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("in")">ورودی (شارژ)</MudSelectItem>
<MudSelectItem T="string" Value="@("out")">خروجی (برداشت/خرید)</MudSelectItem>
</MudSelect>
<MudSelect T="string" @bind-Value="_withdrawStatusFilter" Label="وضعیت برداشت" Variant="Variant.Outlined">
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("pending")">Pending</MudSelectItem>
<MudSelectItem T="string" Value="@("requested")">Requested</MudSelectItem>
<MudSelectItem T="string" Value="@("withdrawn")">Withdrawn</MudSelectItem>
<MudSelectItem T="string" Value="@("cancelled")">Cancelled</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Outlined" OnClick="ApplyFilters" StartIcon="@Icons.Material.Filled.FilterList">اعمال فیلتر</MudButton>
</MudStack>
<MudGrid Spacing="2" Class="mb-3">
<MudItem xs="6" md="4">
<MudTextField @bind-Value="_filterReferenceId"
Label="شناسه ارجاع"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
FullWidth="true" />
</MudItem>
<MudItem xs="6" md="4">
<MudSelect T="string" @bind-Value="_filterType" Label="نوع تراکنش" Variant="Variant.Outlined" FullWidth="true">
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("in")">ورودی (شارژ)</MudSelectItem>
<MudSelectItem T="string" Value="@("out")">خروجی (برداشت/خرید)</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="4" Class="d-flex align-end">
<MudButton Variant="Variant.Outlined"
OnClick="ApplyFilters"
StartIcon="@Icons.Material.Filled.FilterList"
FullWidth="true">
اعمال فیلتر
</MudButton>
</MudItem>
</MudGrid>
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_txs" Dense="true">
<MudTable Items="_txs" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>تاریخ</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>مسیر</MudTh>
<MudTh>اصلی (تغییرات / مانده)</MudTh>
<MudTh>پاداش های دریافتی (تغییرات / مانده)</MudTh>
<MudTh>اعتباری (تغییرات / مانده)</MudTh>
<MudTh>شناسه ارجاع</MudTh>
<MudTh>توضیحات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Date</MudTd>
<MudTd Color="@(context.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(context.Amount)</MudTd>
<MudTd>@context.Channel</MudTd>
<MudTd>@context.Description</MudTd>
<MudTd DataLabel="تاریخ">
<MudText Typo="Typo.caption">@context.Date</MudText>
</MudTd>
<MudTd DataLabel="اصلی">
<MudStack Spacing="0">
<MudText Color="@(context.CreditChange > 0 ? Color.Success : context.CreditChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
@(context.CreditChange != 0 ? (context.CreditChange > 0 ? "+" : "") + FormatPrice(context.CreditChange) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.CreditBalance)</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="پاداش های دریافتی">
<MudStack Spacing="0">
<MudText Color="@(context.NetworkChange > 0 ? Color.Success : context.NetworkChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
@(context.NetworkChange != 0 ? (context.NetworkChange > 0 ? "+" : "") + FormatPrice(context.NetworkChange) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.NetworkBalance)</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="اعتباری">
<MudStack Spacing="0">
<MudText Color="@(context.DiscountChange > 0 ? Color.Success : context.DiscountChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
@(context.DiscountChange != 0 ? (context.DiscountChange > 0 ? "+" : "") + FormatPrice(context.DiscountChange) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.DiscountBalance)</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="شناسه">@context.Channel</MudTd>
<MudTd DataLabel="توضیحات">@context.Description</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@@ -112,13 +213,50 @@
<MudStack Spacing="2">
@foreach (var tx in _txs)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudPaper Class="pa-3 rounded-lg" Outlined="true" Elevation="1">
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>@tx.Date</MudText>
<MudText Color="@(tx.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(tx.Amount)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tx.Date</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه: @tx.Channel</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tx.Channel</MudText>
<MudDivider />
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween">
<!-- کیف پول اصلی -->
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Color="Color.Primary">اصلی</MudText>
<MudText Color="@(tx.CreditChange > 0 ? Color.Success : tx.CreditChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
@(tx.CreditChange != 0 ? (tx.CreditChange > 0 ? "+" : "") + FormatPrice(tx.CreditChange) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem;">@FormatPrice(tx.CreditBalance)</MudText>
</MudStack>
</MudPaper>
<!-- پاداش های دریافتی -->
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Color="Color.Success">پاداش های دریافتی</MudText>
<MudText Color="@(tx.NetworkChange > 0 ? Color.Success : tx.NetworkChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
@(tx.NetworkChange != 0 ? (tx.NetworkChange > 0 ? "+" : "") + FormatPrice(tx.NetworkChange) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem;">@FormatPrice(tx.NetworkBalance)</MudText>
</MudStack>
</MudPaper>
<!-- کیف پول اعتباری -->
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Color="Color.Warning">اعتباری</MudText>
<MudText Color="@(tx.DiscountChange > 0 ? Color.Success : tx.DiscountChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
@(tx.DiscountChange != 0 ? (tx.DiscountChange > 0 ? "+" : "") + FormatPrice(tx.DiscountChange) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem;">@FormatPrice(tx.DiscountBalance)</MudText>
</MudStack>
</MudPaper>
</MudStack>
<MudText Typo="Typo.body2">@tx.Description</MudText>
</MudStack>
</MudPaper>
@@ -126,49 +264,5 @@
</MudStack>
</MudHidden>
</MudPaper>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">درخواست‌های برداشت ثبت‌شده</MudText>
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_withdrawals" Dense="true">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>روش</MudTh>
<MudTh>تاریخ</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.WeekNumber</MudTd>
<MudTd>@FormatPrice(context.Amount)</MudTd>
<MudTd>
<MudChip T="string" Color="@(ResolveStatusColor(context.Status))" Variant="Variant.Outlined" Size="Size.Small">
@ResolveStatusText(context.Status)
</MudChip>
</MudTd>
<MudTd>@ResolveMethodText(context.Method)</MudTd>
<MudTd>@context.Created</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var wd in _withdrawals)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>@wd.WeekNumber</MudText>
<MudText Color="Color.Primary">@FormatPrice(wd.Amount)</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">وضعیت: @ResolveStatusText(wd.Status) | روش: @ResolveMethodText(wd.Method)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@wd.Created</MudText>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
</MudPaper>
</MudStack>
</MudContainer>
@@ -6,71 +6,50 @@ namespace FrontOffice.Main.Pages.Profile;
public partial class Wallet : ComponentBase
{
private long _minWithdrawalAmount = 1_000_000; // مقدار پیش‌فرض، از CMS خوانده می‌شود
[Inject] private AuthService AuthService { get; set; } = default!;
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
private List<WalletTransaction> _txs = new();
private List<WalletWithdrawal> _withdrawals = new();
private string? _filterReferenceId;
private string _filterType = "all";
private string _withdrawStatusFilter = "all";
private bool _isSubmittingWithdrawal;
private long _withdrawPayoutId;
private WithdrawalMethodClient _withdrawMethod = WithdrawalMethodClient.Cash;
private string? _withdrawIban;
private bool _isClubMemberActive;
private bool _hasPurchasedPackage;
private bool _isMagicWallet;
private bool _magicCeilingFull;
protected override async Task OnInitializedAsync()
{
var b = await WalletService.GetBalancesAsync();
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
_txs = await WalletService.GetTransactionsAsync();
_withdrawals = await WalletService.GetWithdrawalsAsync();
var settings = await WalletService.GetWithdrawalSettingsAsync();
if (settings.MinWithdrawalAmount > 0)
_minWithdrawalAmount = settings.MinWithdrawalAmount;
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task SubmitWithdrawal()
{
var target = _withdrawals.FirstOrDefault(x => x.Id == _withdrawPayoutId);
if (target is null)
try
{
_withdrawals = await WalletService.GetWithdrawalsAsync();
target = _withdrawals.FirstOrDefault(x => x.Id == _withdrawPayoutId);
var userInfo = await AuthService.GetUserAuthInfo();
_isClubMemberActive = userInfo.IsClubMemberActive;
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
}
if (target != null && target.Amount < _minWithdrawalAmount)
catch
{
Snackbar.Add($"مبلغ این واریز کمتر از حداقل برداشت ({_minWithdrawalAmount:N0} تومان) است.", Severity.Warning);
return;
}
if (_withdrawPayoutId <= 0)
{
Snackbar.Add("شناسه واریز (PayoutId) الزامی است.", Severity.Warning);
return;
}
if (_withdrawMethod == WithdrawalMethodClient.Cash && string.IsNullOrWhiteSpace(_withdrawIban))
{
Snackbar.Add("برای برداشت نقدی، شماره شبا لازم است.", Severity.Warning);
return;
_isClubMemberActive = false;
_hasPurchasedPackage = false;
}
try
{
_isSubmittingWithdrawal = true;
await WalletService.RequestWithdrawalAsync(_withdrawPayoutId, _withdrawMethod, _withdrawIban);
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
_isMagicWallet = magicStatus.WalletMode == 1;
_magicCeilingFull = _isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
}
catch (Exception ex)
catch
{
Snackbar.Add($"خطا در ثبت برداشت: {ex.Message}", Severity.Error);
}
finally
{
_isSubmittingWithdrawal = false;
_isMagicWallet = false;
_magicCeilingFull = false;
}
var b = await WalletService.GetBalancesAsync();
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
_txs = await WalletService.GetTransactionsAsync();
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task ApplyFilters()
{
long? refId = null;
@@ -85,39 +64,5 @@ public partial class Wallet : ComponentBase
_ => null
};
_txs = await WalletService.GetTransactionsAsync(refId, isIncrease);
int? status = _withdrawStatusFilter switch
{
"pending" => 1,
"requested" => 2,
"withdrawn" => 3,
"cancelled" => 4,
_ => null
};
_withdrawals = await WalletService.GetWithdrawalsAsync(status);
}
private static string ResolveStatusText(int status) => status switch
{
0 => "ایجاد شده",
1 => "پرداخت شده",
2 => "درخواست برداشت",
3 => "برداشت شده",
4 => "لغو شده",
_ => status.ToString()
};
private static Color ResolveStatusColor(int status) => status switch
{
2 => Color.Warning,
3 => Color.Success,
4 => Color.Error,
_ => Color.Info
};
private static string ResolveMethodText(int? method) => method switch
{
0 => "نقدی",
1 => "الماس",
_ => "-"
};
}
@@ -0,0 +1,160 @@
@attribute [Route(RouteConstants.Profile.WithdrawalRequests)]
<PageTitle>درخواست‌های برداشت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="درخواست‌های برداشت" BackHref="@RouteConstants.Profile.Wallet" />
<!-- فرم درخواست برداشت جدید -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">ثبت درخواست برداشت جدید</MudText>
@if (!_availablePayouts.Any())
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
<MudText>در حال حاضر واریزی برای برداشت وجود ندارد.</MudText>
</MudAlert>
}
else
{
<MudStack Spacing="2">
<MudSelect T="CommissionPayoutDto" @bind-Value="_selectedPayout"
Label="انتخاب واریز"
Variant="Variant.Outlined"
AnchorOrigin="Origin.BottomLeft"
Required="true"
ToStringFunc="@(p => p != null ? $"{p.WeekDisplayName} - {p.AmountFormatted}" : "")">
@foreach (var payout in _availablePayouts)
{
<MudSelectItem T="CommissionPayoutDto" Value="@payout">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="w-100">
<MudText>@payout.WeekDisplayName</MudText>
<MudChip T="string" Color="Color.Success" Size="Size.Small" Variant="Variant.Filled">
@payout.AmountFormatted
</MudChip>
</MudStack>
</MudSelectItem>
}
</MudSelect>
@if (_selectedPayout != null)
{
<MudAlert Severity="Severity.Success" Variant="Variant.Text" Dense="true">
مبلغ قابل برداشت: <strong>@_selectedPayout.AmountFormatted</strong>
</MudAlert>
}
<MudRadioGroup T="WithdrawalMethodClient" @bind-Value="_withdrawMethod" Row="true">
<MudRadio T="WithdrawalMethodClient" Value="@WithdrawalMethodClient.Cash" Color="Color.Primary">
برداشت نقدی (نیاز به شبا)
</MudRadio>
<MudRadio T="WithdrawalMethodClient" Disabled="true" Value="@WithdrawalMethodClient.Diamond" Color="Color.Secondary">
الماس/غیرنقدی
</MudRadio>
</MudRadioGroup>
<MudTextField @bind-Value="_withdrawIban"
Label="شماره شبا"
Variant="Variant.Outlined"
Disabled="_withdrawMethod == WithdrawalMethodClient.Diamond"
Placeholder="IRxxxxxxxxxxxx"
Adornment="Adornment.End"
AdornmentText="IR" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
حداقل مبلغ برداشت: @FormatPrice(_minWithdrawalAmount)
</MudText>
<MudButton Disabled="@(_isSubmittingWithdrawal || _selectedPayout == null)"
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Outbound"
OnClick="SubmitWithdrawal">
@(_isSubmittingWithdrawal ? "در حال ثبت..." : "ثبت درخواست برداشت")
</MudButton>
</MudStack>
}
</MudPaper>
<!-- فیلتر وضعیت -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.End">
<MudSelect T="string" @bind-Value="_statusFilter" Label="فیلتر وضعیت" Variant="Variant.Outlined" Style="min-width: 180px;">
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("pending")">در انتظار</MudSelectItem>
<MudSelectItem T="string" Value="@("requested")">درخواست شده</MudSelectItem>
<MudSelectItem T="string" Value="@("withdrawn")">برداشت شده</MudSelectItem>
<MudSelectItem T="string" Value="@("cancelled")">لغو شده</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Outlined" OnClick="ApplyFilter" StartIcon="@Icons.Material.Filled.FilterList">اعمال فیلتر</MudButton>
</MudStack>
</MudPaper>
<!-- لیست درخواست‌های برداشت -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">لیست درخواست‌ها</MudText>
@if (_isLoading)
{
<LoadingState />
}
else if (!_withdrawals.Any())
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
هنوز درخواست برداشتی ثبت نشده است.
</MudAlert>
}
else
{
<!-- نمایش دسکتاپ -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_withdrawals" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>روش برداشت</MudTh>
<MudTh>تاریخ ثبت</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="هفته">@context.WeekDisplayName</MudTd>
<MudTd DataLabel="مبلغ">@FormatPrice(context.Amount)</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string" Color="@(ResolveStatusColor(context.Status))" Variant="Variant.Filled" Size="Size.Small">
@ResolveStatusText(context.Status)
</MudChip>
</MudTd>
<MudTd DataLabel="روش">@ResolveMethodText(context.Method)</MudTd>
<MudTd DataLabel="تاریخ">@context.Created</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
<!-- نمایش موبایل -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var wd in _withdrawals)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2">@wd.WeekDisplayName</MudText>
<MudChip T="string" Color="@(ResolveStatusColor(wd.Status))" Variant="Variant.Filled" Size="Size.Small">
@ResolveStatusText(wd.Status)
</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(wd.Amount)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@ResolveMethodText(wd.Method)</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@wd.Created</MudText>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
}
</MudPaper>
</MudStack>
</MudContainer>
@@ -0,0 +1,161 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class WithdrawalRequests : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
private long _minWithdrawalAmount = 1_000_000;
private List<WalletWithdrawal> _withdrawals = new();
private List<CommissionPayoutDto> _availablePayouts = new();
private string _statusFilter = "all";
private bool _isLoading = true;
private bool _isSubmittingWithdrawal;
private CommissionPayoutDto? _selectedPayout;
private WithdrawalMethodClient _withdrawMethod = WithdrawalMethodClient.Cash;
private string? _withdrawIban;
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
_isLoading = true;
try
{
_withdrawals = await WalletService.GetWithdrawalsAsync();
_availablePayouts = await CommissionService.GetWithdrawablePayoutsAsync();
var settings = await WalletService.GetWithdrawalSettingsAsync();
if (settings.MinWithdrawalAmount > 0)
_minWithdrawalAmount = settings.MinWithdrawalAmount;
}
finally
{
_isLoading = false;
}
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task SubmitWithdrawal()
{
if (_selectedPayout == null)
{
Snackbar.Add("لطفاً یک واریز را انتخاب کنید.", Severity.Warning);
return;
}
if (_withdrawMethod == WithdrawalMethodClient.Cash && string.IsNullOrWhiteSpace(_withdrawIban))
{
Snackbar.Add("برای برداشت نقدی، شماره شبا لازم است.", Severity.Warning);
return;
}
try
{
_isSubmittingWithdrawal = true;
string? normalizedIban = null;
if (_withdrawMethod == WithdrawalMethodClient.Cash)
{
normalizedIban = NormalizeIranianIban(_withdrawIban);
if (normalizedIban is null)
{
Snackbar.Add("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد (خط تیره اختیاری است).", Severity.Warning);
return;
}
}
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, normalizedIban);
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
// بروزرسانی لیست
await LoadData();
// ریست فرم
_selectedPayout = null;
_withdrawIban = null;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ثبت برداشت: {ex.Message}", Severity.Error);
}
finally
{
_isSubmittingWithdrawal = false;
}
}
private async Task ApplyFilter()
{
_isLoading = true;
try
{
int? status = _statusFilter switch
{
"pending" => 1,
"requested" => 2,
"withdrawn" => 3,
"cancelled" => 4,
_ => null
};
_withdrawals = await WalletService.GetWithdrawalsAsync(status);
}
finally
{
_isLoading = false;
}
}
private static string ResolveStatusText(int status) => status switch
{
0 => "ایجاد شده",
1 => "در انتظار پرداخت",
2 => "درخواست برداشت",
3 => "برداشت شده",
4 => "لغو شده",
_ => status.ToString()
};
private static Color ResolveStatusColor(int status) => status switch
{
1 => Color.Info,
2 => Color.Warning,
3 => Color.Success,
4 => Color.Error,
_ => Color.Default
};
private static string ResolveMethodText(int? method) => method switch
{
0 => "نقدی (شبا)",
1 => "الماس",
_ => "-"
};
/// <summary>
/// فاصله و خط تیره را حذف می‌کند و شبا را به شکل IR + ۲۴ رقم برمی‌گرداند.
/// </summary>
private static string? NormalizeIranianIban(string? iban)
{
if (string.IsNullOrWhiteSpace(iban))
return null;
var normalized = iban.Trim().ToUpperInvariant()
.Replace(" ", "", StringComparison.Ordinal)
.Replace("-", "", StringComparison.Ordinal);
if (normalized.StartsWith("IR", StringComparison.Ordinal))
normalized = normalized[2..];
normalized = "IR" + normalized;
return System.Text.RegularExpressions.Regex.IsMatch(normalized, @"^IR\d{24}$")
? normalized
: null;
}
}
+68 -55
View File
@@ -6,10 +6,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="px-1">
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="5" Class="d-none d-sm-block">
<MudStack Spacing="2" Class="mb-6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ثبت‌نام سه
مرحله‌ای</MudChip>
<MudText Typo="Typo.h3" Class="mb-2">
<MudStack Spacing="3" Class="mb-6">
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" Size="Size.Small" Class="rounded-pill">
ثبت‌نام سه مرحله‌ای
</MudChip>
<MudText Typo="Typo.h3" Class="mb-1">
فقط در چند دقیقه حساب خود را فعال کنید
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@@ -18,34 +19,47 @@
</MudText>
</MudStack>
<MudPaper Elevation="1" Class="pa-4 rounded-xl gradient-border">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Primary" />
<MudPaper Elevation="0" Class="pa-5 rounded-xl" Style="border:1px solid var(--mud-palette-divider);">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudAvatar Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Outlined.Verified" Size="Size.Small" />
</MudAvatar>
<MudText Typo="Typo.subtitle1">مزایای ثبت‌نام آنلاین</MudText>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
• تایید سریع و آنلاین شماره موبایل<br />
• تکمیل اطلاعات هویتی بدون مراجعه حضوری<br />
• دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق
</MudText>
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.PhoneAndroid" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">تایید سریع و آنلاین شماره موبایل</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.Badge" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">تکمیل اطلاعات هویتی بدون مراجعه حضوری</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.Description" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="7">
<MudPaper Elevation="3" Class="pa-6 wizard-card">
<MudPaper Elevation="1" Class="pa-5 pa-md-6 wizard-card">
@if (_completed)
{
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
<MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center py-6">
<MudAvatar Size="Size.Large" Color="Color.Success" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Outlined.CheckCircle" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width:400px;">
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام
را دنبال کنید.
</MudText>
<MudStack Row="true" Spacing="2" Justify="Justify.Center">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه
اصلی</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
@@ -56,43 +70,45 @@
}
else
{
<MudStack Spacing="3" >
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h4">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله
<MudText Typo="Typo.h5">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small" Class="rounded-pill">
مرحله @(_activeStep + 1) از ۳
</MudChip>
</MudStack>
@if (_isSubmitting)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="rounded" />
}
<MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4 " id="registration-stepper">
<MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4" id="registration-stepper">
<ChildContent>
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone">
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Outlined.PhoneAndroid">
@* Inline AuthDialog with captcha enabled *@
<AuthDialog @ref="_authDialog" InlineMode="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" />
</MudStep>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge">
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Outlined.Badge">
<MudForm @ref="_stepTwoForm">
<MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
<MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.LastName" For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
MaxLength="10" @bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)" InputType="InputType.Number" />
<MudStack Spacing="3">
<MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
<MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.LastName" For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
MaxLength="10" @bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)" InputType="InputType.Number" />
</MudStack>
</MudForm>
</MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule">
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Outlined.Rule">
<MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3 rounded-lg">
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک
تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره
کنید.
تایید را فعال نمایید.
</MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
@@ -106,47 +122,44 @@
</MudList>
</MudPaper>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Download" Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود/پرینت قرارداد نمونه
</MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: PDF</MudText>
</MudStack>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-lg mb-3"
StartIcon="@Icons.Material.Outlined.Download" Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود قرارداد نمونه (PDF)
</MudButton>
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Primary"
For="@(() => _model.AcceptTerms)"
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm>
</MudStep>
</ChildContent>
<ActionContent Context="stepper">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mt-2">
@if (_isAuthenticated && _activeStep == 1)
{
<MudButton Variant="Variant.Text" Color="Color.Secondary"
<MudButton Variant="Variant.Text" Color="Color.Error" Size="Size.Small"
OnClick="@AuthService.LogoutAsync">خروج از حساب</MudButton>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Secondary"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
}
<MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
Disabled="_isSubmitting"
OnClick="@(async () => { await GoNextAsync(); })">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
@_nextButtonText
</MudButton>
</MudStack>
</ActionContent>
</MudStepper>
</MudStack>
}
</MudPaper>
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using DateTimeConverterCL;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
@@ -23,13 +23,14 @@ public partial class RegisterWizard
private bool _completed;
private AuthDialog? _authDialog;
private MudStepper? _mudStepper;
private UpdateUserRequest _updateUserRequest = new();
private UpdateCustomerProfileRequest _updateUserRequest = new();
private bool _isAuthenticated;
private CancellationTokenSource? _operationCts;
private UserAuthInfo _userAuthInfo;
private Guid signGuid = Guid.NewGuid();
private const string TokenStorageKey = "auth:token";
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
private bool _initialDataLoaded;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private IDeviceDetector _deviceDetector { get; set; } = default!;
@@ -55,6 +56,7 @@ public partial class RegisterWizard
if (_userAuthInfo.IsSignMainContract)
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
return;
}
if (_activeStep == 0)
@@ -63,11 +65,12 @@ public partial class RegisterWizard
_activeStep = 1;
await InvokeAsync(StateHasChanged);
}
else if (_activeStep == 1)
else if (_activeStep == 1 && !_initialDataLoaded)
{
_initialDataLoaded = true;
try
{
var existUser = await UserContract.GetUserAsync(new Empty());
var existUser = await UserContract.GetUserForCustomerAsync(new GetUserForCustomerRequest());
if (existUser != null && !string.IsNullOrEmpty(existUser.FirstName) &&
string.IsNullOrWhiteSpace(_model.FirstName))
{
@@ -95,7 +98,6 @@ public partial class RegisterWizard
}
}
await base.OnAfterRenderAsync(firstRender);
}
@@ -355,7 +357,7 @@ public partial class RegisterWizard
_updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish();
await UserContract.UpdateUserAsync(request: _updateUserRequest);
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
return true;
}

Some files were not shown because too many files have changed in this diff Show More