Feature/nice to have polish#24
Conversation
…nternationalization support
…ent URL pagination
…pages with backend integration
… with i18n support
📝 WalkthroughWalkthroughAdds admin/system features (activity logs, notifications, settings), bulk product operations, inventory+order/payment integrations, SignalR real-time notifications, EF Core entities/migrations, many backend repositories/services/controllers, and a large i18n/localization and UI overhaul across the frontend. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin/Manager
participant AdminClient as Admin Client (Browser)
participant AdminHub as SignalR Hub
participant Dispatcher as AdminNotificationDispatcher
participant NotificationService as SystemNotificationService
participant NotificationRepo as SystemNotificationRepository
participant Database as Database
AdminClient->>AdminHub: Connect to /hub/admin (SignalR)
Admin->>NotificationService: SendNotificationAsync(type,title,message,...)
NotificationService->>NotificationRepo: AddAsync(notification)
NotificationRepo->>Database: INSERT system_notifications
Database-->>NotificationRepo: new Id
NotificationRepo-->>NotificationService: return Id
NotificationService->>Dispatcher: BroadcastNotificationAsync(notification)
Dispatcher->>AdminHub: SendAsync("ReceiveNotification", notification)
AdminHub->>AdminClient: ReceiveNotification event
AdminClient->>AdminClient: Invalidate cache / refresh UI
sequenceDiagram
participant Manager as Manager
participant Controller as ManagerOrdersController
participant OrderSvc as OrderService
participant ActivitySvc as ActivityLogService
participant PaymentSvc as PaymentService
participant NotificationSvc as SystemNotificationService
Manager->>Controller: PUT mark-ready/{orderId}
Controller->>OrderSvc: MarkReadyAsync(orderId, performedByUserId)
OrderSvc->>OrderSvc: Update order status in DB
OrderSvc->>ActivitySvc: LogActionAsync("ORDER_STATUS_CHANGED", ...)
ActivitySvc->>Database: INSERT system_activity_logs
Manager->>Controller: POST {orderId}/cash-collected
Controller->>PaymentSvc: MarkCashPaidAsync(orderId)
PaymentSvc->>PaymentSvc: Update payment status = PAID
PaymentSvc->>NotificationSvc: SendNotificationAsync("PAYMENT_RECEIVED", ...)
NotificationSvc->>Dispatcher: BroadcastNotificationAsync(...)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~150 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 4
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
backend/OrderSystemApi/OrderSystemApi/Controllers/Manager/ManagerProductImagesController.cs (1)
36-54:⚠️ Potential issue | 🟠 MajorEnforce the product scope on image mutations.
productIdis bound from the nested route but ignored for update/delete, so/products/{productId}/images/{imageId}can mutate an image that belongs to a different product if theimageIdis known. PassproductIdinto the service or verify ownership before mutating.Suggested direction
- var updated = await _productImageService.UpdateImageAsync(imageId, request, ct); + var updated = await _productImageService.UpdateImageAsync(productId, imageId, request, ct); @@ - var deleted = await _productImageService.DeleteImageAsync(imageId, ct); + var deleted = await _productImageService.DeleteImageAsync(productId, imageId, ct);The service should return not found/false when the image does not belong to the route product.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystemApi/Controllers/Manager/ManagerProductImagesController.cs` around lines 36 - 54, The UpdateImage and DeleteImage actions ignore the route productId and thus can mutate images across products; modify UpdateImage and DeleteImage to enforce product-scoped mutations by including the route productId in the validation/service call (e.g. call _productImageService.UpdateImageAsync(imageId, productId, request, ct) and _productImageService.DeleteImageAsync(imageId, productId, ct) or first verify ownership via a method like _productImageService.IsImageForProductAsync(imageId, productId) and return NotFound() when it does not belong to the specified product), ensuring the service returns false/NotFound when imageId is not owned by productId.backend/OrderSystemApi/OrderSystemApi/Program.cs (1)
152-173:⚠️ Potential issue | 🟡 MinorUploads folder is served publicly via
UseStaticFiles().
wwwroot/uploadsis exposed anonymously to the internet onceUseStaticFiles()is enabled. This is fine for product images, but if the same folder ever stores admin/manager-uploaded content that isn't intended to be public (receipts, invoices, user-uploaded documents), URL guessing/enumeration becomes a risk. Consider (a) serving uploads from a dedicated request path with content-type and size restrictions, and/or (b) storing sensitive artifacts outsidewwwrootbehind an authorized controller action.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystemApi/Program.cs` around lines 152 - 173, The uploads folder is currently created under wwwroot and becomes publicly accessible via app.UseStaticFiles(), so change the design: stop serving sensitive uploads from wwwroot and either (A) move the uploadsPath out of the public webroot (e.g., set uploadsPath = Path.Combine(app.Environment.ContentRootPath, "uploads") instead of "wwwroot/uploads") and remove any StaticFile exposure for that folder, then implement an authorized endpoint (e.g., an UploadsController with a GetFile action) that locates files via the same uploadsPath/FileProvider, validates permissions and content-type/size, and returns a FileStreamResult; or (B) if you must serve statically, configure app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(uploadsPath), RequestPath = "/uploads", OnPrepareResponse = ctx => { /* reject anonymous or enforce headers */ } }) but ensure OnPrepareResponse enforces authentication/authorization and strict content-type checks — update references to uploadsPath and the call to app.UseStaticFiles() in Program.cs accordingly.backend/OrderSystemApi/OrderSystem.Application/Inventorys/Services/InventoryService.cs (1)
42-66:⚠️ Potential issue | 🔴 CriticalFix atomicity violation: wrap product update and movement insert in a single transaction.
Lines 66 and 77 execute two separate
SaveChangesAsync()calls. If_productRepository.Update()succeeds but_movementRepository.AddAsync()fails (or vice versa), the product quantity/status update persists without a matchingInventoryMovementrecord, corrupting audit history and leaving the inventory in an inconsistent state.Refactor to use explicit transaction control (e.g.,
_context.Database.BeginTransactionAsync()) or implement a Unit of Work pattern to ensure both operations commit atomically within a single database transaction.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Application/Inventorys/Services/InventoryService.cs` around lines 42 - 66, Currently InventoryService performs _productRepository.Update(product) and _movementRepository.AddAsync(movement) in separate save operations causing an atomicity violation; modify the method to wrap both the product update and the movement insert in a single database transaction (e.g., use _context.Database.BeginTransactionAsync() or the project’s UnitOfWork) so that both _productRepository.Update(product) and _movementRepository.AddAsync(...) are committed together and rolled back together on failure, ensuring SaveChangesAsync is called only within the enclosing transaction scope.frontend/src/modules/manager/hooks/useManagerOrders.ts (1)
26-28:⚠️ Potential issue | 🟠 MajorInvalidate all orders list queries using a root prefix key.
On line 27,
invalidateOrders()usesmanagerOrderKeys.orders()which resolves to['manager', 'orders', undefined]. This does not prefix-match parameterized list queries like['manager', 'orders', { status: 'pending' }], leaving filtered orders lists stale after mutations succeed.Create a root key for prefix-based invalidation:
Proposed fix
export const managerOrderKeys = { + ordersRoot: () => ['manager', 'orders'] as const, orders: (params?: OrderQuery) => ['manager', 'orders', params] as const, order: (id: number) => ['manager', 'orders', id] as const, }; function invalidateOrders(orderId: number) { - queryClient.invalidateQueries({ queryKey: managerOrderKeys.orders() }); + queryClient.invalidateQueries({ queryKey: managerOrderKeys.ordersRoot() }); queryClient.invalidateQueries({ queryKey: managerOrderKeys.order(orderId) }); }Affects all mutations:
useMarkOrderReady,useMarkOrderOutForDelivery,useMarkOrderDelivered, anduseMarkCashCollected(line 55).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/manager/hooks/useManagerOrders.ts` around lines 26 - 28, invalidateOrders currently calls queryClient.invalidateQueries with managerOrderKeys.orders() which resolves to a fully parameterized key and won’t prefix-match filtered lists; update invalidateOrders (the function in useManagerOrders.ts) to call queryClient.invalidateQueries with prefix matching by passing exact: false (e.g., queryClient.invalidateQueries({ queryKey: managerOrderKeys.orders(), exact: false })) so all parameterized list queries (like ['manager','orders',{...}]) get invalidated, leaving the per-order invalidation (managerOrderKeys.order(orderId)) intact for the single-order cache.backend/OrderSystemApi/OrderSystem.Application/Orders/Services/OrderService.cs (1)
270-329:⚠️ Potential issue | 🟠 MajorKeep activity logging from turning successful status changes into failed API calls.
Each status method updates the order before awaiting
LogActionAsync. If logging fails, the API reports failure even though the status changed. Persist the status and log atomically, or make the log write best-effort/outbox-backed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Application/Orders/Services/OrderService.cs` around lines 270 - 329, The activity logging currently runs after updating the order and, if it throws, will surface as an API failure even though the order status was persisted; in the status-change methods (e.g., MarkOutForDeliveryAsync, MarkDeliveredAsync, and the Ready method where _orderRepository.Update(order) is followed by _activityLogService.LogActionAsync) make logging best-effort by ensuring the order update is persisted first and wrapping the LogActionAsync call in a non-failing path (e.g., try/catch that captures/logs the logging error without rethrowing) or replace the direct LogActionAsync call with an outbox enqueue that is transactional with the order update so failures to record activity do not cause the API to fail.
🧹 Nitpick comments (25)
frontend/src/shared/components/SectionHeading.tsx (1)
17-17: Prefershrink-0overflex-shrink-0for Tailwind v4.In Tailwind CSS v4,
flex-shrink-*utilities were renamed toshrink-*. The legacy name may still resolve, but the idiomatic v4 class isshrink-0.♻️ Proposed change
- {action && <div className="flex-shrink-0">{action}</div>} + {action && <div className="shrink-0">{action}</div>}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/shared/components/SectionHeading.tsx` at line 17, The JSX in the SectionHeading component uses the legacy Tailwind utility "flex-shrink-0" on the action wrapper ({action && <div className="flex-shrink-0">{action}</div>}); update that class to the Tailwind v4 idiomatic "shrink-0" so the action container uses the new utility name (change the className on the action div in SectionHeading.tsx).backend/OrderSystemApi/OrderSystemApi/OrderSystem.Api.csproj (1)
10-10: Verify the intended use case for Bogus in the API project.The
Boguslibrary is typically used for generating fake/test data and is commonly added to test projects rather than production API projects. Adding it here raises questions:
- If used for development/testing only: Consider moving this dependency to a separate test project or seeder tool to keep the production API lean.
- If used for production data seeding: Ensure this is intentional and that Bogus data generation is properly isolated to startup/migration scenarios, not active request handling.
Adding development/test dependencies to the main API increases bundle size and production surface area unnecessarily.
📦 Alternative approaches for test data generation
Option 1: Move to a separate test or seeder project:
- Create
OrderSystem.DataSeederproject- Reference Bogus only there
- Run as a separate tool or startup configuration
Option 2: Use conditional compilation if seeding is needed:
- <PackageReference Include="Bogus" Version="35.6.5" /> + <PackageReference Include="Bogus" Version="35.6.5" Condition="'$(Configuration)' == 'Debug'" />This ensures Bogus is only included in development builds.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystemApi/OrderSystem.Api.csproj` at line 10, The csproj currently references the Bogus package via PackageReference Include="Bogus", which is intended for test/data seeding rather than runtime API usage; remove this PackageReference from the API project and either (A) move Bogus into a dedicated seeder/test project (e.g., create OrderSystem.DataSeeder and add Bogus there) and perform seeding from that tool, or (B) if you must keep it in the repo, limit inclusion to non-production builds by adding Bogus only to a test/seeder project or use conditional inclusion so Bogus is only referenced/used during startup seeding paths and never in active request handling (also audit any code in Startup/Program that references Bogus to ensure it’s isolated to development/seeding flows).frontend/src/shared/components/PageContainer.tsx (1)
1-1: Consider using a type-only import forReactNode.
ReactNodeis used only as a type in the interface. While your project hasverbatimModuleSyntaxdisabled intsconfig.app.json, usingimport typeis still a best practice for clarity and future-proofs the code should configs change.Suggested improvement
-import { ReactNode } from 'react'; +import type { ReactNode } from 'react';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/shared/components/PageContainer.tsx` at line 1, The import for ReactNode in PageContainer.tsx is only used as a type; change it to a type-only import to make the intent explicit and future-proof the file: replace the current import of ReactNode with a type import (import type { ReactNode } from 'react') so the symbol is only retained for type-checking and not as a runtime import; ensure any other uses in the file still reference the ReactNode type in the component props/interface.backend/OrderSystemApi/OrderSystem.Application/Products/Interfaces/IProductRepository.cs (1)
22-22: Thread cancellation through the bulk update contract.
UpdateBulkcan persist many products, but it cannot observe request cancellation while the read methods above can. Consider adding aCancellationTokenwhile this API is new.♻️ Proposed contract adjustment
- Task<bool> UpdateBulk(IEnumerable<Product> products); + Task<bool> UpdateBulk(IEnumerable<Product> products, CancellationToken ct);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Application/Products/Interfaces/IProductRepository.cs` at line 22, The UpdateBulk contract lacks cancellation support—change the IProductRepository.UpdateBulk signature to accept a CancellationToken (e.g., Task<bool> UpdateBulk(IEnumerable<Product> products, CancellationToken cancellationToken)) and update all implementations and call sites to pass the token through (propagate token from controller/service entry points and honor it in any loops/async operations inside implementations such as repository classes that perform DB or batch work), and ensure cancellations are observed (throw OperationCanceledException or return early) where appropriate.backend/OrderSystemApi/OrderSystemApi/Program.cs (1)
166-170: CORS: consider reading allowed origins from configuration.Origins are hardcoded to localhost dev ports, which will need a code change for staging/prod. Moving them to
appsettings(or env vars) avoids a code edit per environment and reduces the chance of deploying a dev-only CORS policy. Non-blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystemApi/Program.cs` around lines 166 - 170, The app.UseCors call currently hardcodes origins; change it to read allowed origins from configuration instead: add a configuration key (e.g. "Cors:AllowedOrigins") in appsettings or an env var, load it via the IConfiguration instance in Program.cs, parse into a string[] (or split a comma-separated env var) and pass that array to builder.WithOrigins(...) in the existing app.UseCors block (referencing the same app.UseCors builder lambda); keep AllowAnyHeader/AllowAnyMethod/AllowCredentials as-is and validate the config value for null/empty before applying.frontend/src/modules/manager/api/products.api.ts (1)
51-62: Optional: type the POST response.
http.postis invoked without a generic, sodatais inferred asanyandaddProductImagereturnsPromise<any>. Consider specifying a response type (e.g.{ id: number }or aProductImageDTO) for safer consumers.Proposed tweak
- const { data } = await http.post( + const { data } = await http.post<{ id: number }>( `/api/v1/manager/products/${payload.productId}/images`, { imageUrl, altText, sortOrder, isPrimary } );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/manager/api/products.api.ts` around lines 51 - 62, The addProductImage function currently returns Promise<any> because http.post is called without a generic; update addProductImage to declare and return a concrete response type (for example a small DTO like ProductImage or { id: number }) and pass that type as the generic to http.post so TypeScript infers data correctly; update the function signature (addProductImage(payload: CreateProductImageRequest): Promise<YourResponseType>) and adjust any callers if needed. Ensure the change is applied in the addProductImage implementation that constructs the POST to `/api/v1/manager/products/${payload.productId}/images` (no change needed for deleteProductImage).backend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418180805_AddSystemNotification.cs (1)
14-31: Consider indexing unread/recent notification access.If the new notification endpoints query unread items and recent activity, this table will scan as it grows. Add an index that matches the read-status/recent-sort access pattern.
Possible migration addition
constraints: table => { table.PrimaryKey("PK_system_notifications", x => x.Id); }); + + migrationBuilder.CreateIndex( + name: "IX_system_notifications_IsRead_CreatedAt", + table: "system_notifications", + columns: new[] { "IsRead", "CreatedAt" });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418180805_AddSystemNotification.cs` around lines 14 - 31, Add a composite index to speed unread/recent queries by calling migrationBuilder.CreateIndex after the CreateTable for "system_notifications"; create an index named like "IX_system_notifications_IsRead_CreatedAt" on the columns IsRead and CreatedAt (e.g., columns: new[] { "IsRead", "CreatedAt" }) so queries filtering IsRead and ordering by CreatedAt will use the index.backend/OrderSystemApi/OrderSystem.Infrastructure/Data/Configurations/SystemNotificationConfiguration.cs (1)
14-37: Consider adding indexes for common notification query patterns.Admin notification listings typically filter/sort by fields like
IsRead,NotificationType, orCreatedAt. If the repository queries on these, adding indexes here (e.g.,builder.HasIndex(x => x.CreatedAt),builder.HasIndex(x => new { x.IsRead, x.CreatedAt })) will prevent full table scans as the table grows.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Data/Configurations/SystemNotificationConfiguration.cs` around lines 14 - 37, Add indexes on the SystemNotification entity to support common query patterns: update the Configure method in SystemNotificationConfiguration to add builder.HasIndex(x => x.CreatedAt), builder.HasIndex(x => x.NotificationType), and a composite index builder.HasIndex(x => new { x.IsRead, x.CreatedAt }) so queries that filter/sort by CreatedAt, NotificationType, or IsRead+CreatedAt use indexes instead of full table scans; ensure these index calls are placed after the Property(...) declarations in the Configure(EntityTypeBuilder<SystemNotification> builder) method.frontend/src/modules/customer/pages/CustomerOrdersListPage.tsx (1)
54-77: DeademptyMessageprop now that empty state is handled via early return.Since the component early-returns the
EmptyStatewhenever!isLoading && data.items.length === 0, theAppTable'semptyMessagebranch is unreachable. Either drop the prop for clarity, or (preferred) consolidate and letAppTablerender the sharedEmptyStateso loading→empty transitions stay inside one render path without an extra unmount/remount ofPageHeader.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/customer/pages/CustomerOrdersListPage.tsx` around lines 54 - 77, The early-return in CustomerOrdersListPage that renders EmptyState makes the AppTable.emptyMessage branch dead; remove the early return and instead let AppTable handle empty rendering so PageHeader stays mounted across loading→empty transitions, or if you prefer to keep the early-return, remove the unused emptyMessage prop from the AppTable call; to implement the preferred consolidation, keep PageHeader rendered, pass either the EmptyState component or the t.orders.noOrders/noOrdersDesc strings into AppTable (via emptyMessage or an emptyComponent prop), and rely on AppTable to check isLoading and data?.items.length to render the same EmptyState UI.backend/OrderSystemApi/OrderSystem.Application/Products/Interfaces/IProductService.cs (1)
18-19: Document thepercentageChangeunit and consider bounds.
percentageChangeasdecimalis ambiguous — callers can't tell whether10means +10% or +1000% (i.e.,0.10). Please clarify via XML docs on the interface (and enforce the contract in the implementation, e.g., reject values that would make prices non-positive, and cap the allowed range to avoid catastrophic bulk mutations from an admin typo).Suggested doc
+ /// <summary> + /// Applies a percentage change to the price of the specified products. + /// </summary> + /// <param name="percentageChange">Percentage expressed as a whole number, e.g. 10 = +10%, -5 = -5%.</param> Task<int> BulkUpdatePriceAsync(List<long> productIds, decimal percentageChange, CancellationToken ct);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Application/Products/Interfaces/IProductService.cs` around lines 18 - 19, Add XML documentation to IProductService.BulkUpdatePriceAsync clarifying that percentageChange is a fractional multiplier (e.g., 0.10 = +10%, -0.10 = -10%) and whether the method applies price = price * (1 + percentageChange). In the implementing class for BulkUpdatePriceAsync validate percentageChange against safe bounds (e.g., disallow values that would make any resulting price non-positive and cap extreme inputs to a configured range such as [-0.9, 1.0] or similar), and throw ArgumentOutOfRangeException (or return a clear error) when the argument is outside policy; also explicitly handle/validate each product’s computed new price before persisting to avoid catastrophic bulk mutations.frontend/src/app/i18n/i18n-context.tsx (1)
35-38: WraplocalStorage.setItemin try/catch for consistency.
getSavedLocalealready guards against storage errors (Safari private mode, quota, disabled cookies), butsetLocaledoesn't. IfsetItemthrows, the user's locale flip will fail loudly even though the in-memory state updated successfully. Match the defensive pattern used above.const setLocale = useCallback((newLocale: Locale) => { setLocaleState(newLocale); - localStorage.setItem(LOCALE_STORAGE_KEY, newLocale); + try { + localStorage.setItem(LOCALE_STORAGE_KEY, newLocale); + } catch { + // Ignored — in-memory state still reflects the change + } }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/i18n/i18n-context.tsx` around lines 35 - 38, The setLocale callback currently calls localStorage.setItem without guarding against storage errors; update the setLocale function (which calls setLocaleState and uses LOCALE_STORAGE_KEY) to wrap the localStorage.setItem(LOCALE_STORAGE_KEY, newLocale) call in a try/catch so failures (e.g., Safari private mode) don't throw; ensure you still call setLocaleState(newLocale) before/around the try and handle the catch by logging or silently ignoring to match the defensive pattern used by getSavedLocale.frontend/src/layouts/ManagerLayout.tsx (1)
29-79: Avoid redeclaringSidebarContenton every render.Defining
SidebarContentinsideManagerLayoutmeans React sees a brand-new component type on every parent render. Every state update (e.g., togglingsidebarOpen, auth/i18n context changes) forces the sidebar subtree to unmount and remount, discarding any internal state insideLanguageSwitcheror future children (focus, uncontrolled input values, etc.). Extract it as a sibling component, or inline the JSX into aconst sidebarContent = (...)expression so it isn't a new component identity on each render.♻️ Suggested minimal refactor
- const SidebarContent = () => ( - <> + const sidebarContent = ( + <> ... - </> - ); + </> + ); @@ - <SidebarContent /> + {sidebarContent} @@ - <SidebarContent /> + {sidebarContent}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/layouts/ManagerLayout.tsx` around lines 29 - 79, SidebarContent is declared as a component inside ManagerLayout which creates a new component type on each render causing unmount/remount of its subtree (affecting LanguageSwitcher and other internal state); move SidebarContent out of the ManagerLayout function (make it a sibling/top-level component) or replace it with a const sidebarContent = ( ... ) JSX value so its identity is stable, and pass in any needed props/state (user, navLinks, location, setSidebarOpen, handleLogout, t) to the extracted component instead of capturing them from closure.frontend/src/modules/catalog/pages/ProductsListPage.tsx (1)
31-39: Search is debounced-free; consider debouncing before writing to the URL.
setSearchruns on every keystroke and writes to the URL + triggers a new query. Even withreplace: true, this spams react-router updates and upstream API calls. A small debounce (e.g., 300 ms) on the URL write — while keeping the input value in local state — would reduce churn without changing behavior on submit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/catalog/pages/ProductsListPage.tsx` around lines 31 - 39, setSearch currently writes to URL on every keystroke causing excessive router updates and queries; debounce the URL write while keeping the immediate input in local state. Change the component (ProductsListPage) to store the raw input in a local state (e.g., searchInput) updated on every keystroke, and debounce the call that calls setSearchParams (the inner updater that sets/deletes 'search' and deletes 'page') by ~300ms using a stable debounced function or a timeout stored in a ref; ensure you still call setSearchParams with { replace: true } and clear the timeout on unmount to avoid leaks. Use the existing setSearchParams/setSearch logic as the target for the debounced invocation so behavior on form submit can still immediately sync the URL.backend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418174733_AddSystemActivityLog.cs (1)
38-41: Consider additional indexes for common activity-log queries.Activity logs are typically queried by
timestamp(descending for recency) and byentity_type+entity_id(to show history for a given entity). As the table grows, the singleuser_idindex may not be sufficient. A follow-up migration adding e.g.IX_system_activity_logs_timestampand a compositeIX_system_activity_logs_entity_type_entity_idwould help once usage patterns are clear.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418174733_AddSystemActivityLog.cs` around lines 38 - 41, Add additional indexes to the migration to support common queries: create an index on the timestamp column (e.g., index name "IX_system_activity_logs_timestamp" and consider a descending order for recency if your EF/DB supports it) and create a composite index on entity_type + entity_id (e.g., "IX_system_activity_logs_entity_type_entity_id"); update the AddSystemActivityLog migration by adding two migrationBuilder.CreateIndex calls (matching the existing migrationBuilder.CreateIndex usage for "IX_system_activity_logs_user_id") to create these indexes so queries filtering by timestamp or by entity_type/entity_id are optimized.frontend/src/modules/auth/pages/LoginPage.tsx (1)
2-2: Consider using type-only import forPathfrom react-hook-form.
Pathis only used as a type (line 39:key as Path<LoginForm>). While not strictly required with the current configuration (verbatimModuleSyntax: false), using a type-only import improves clarity:Suggested change
-import { useForm, Path } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import type { Path } from 'react-hook-form';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/auth/pages/LoginPage.tsx` at line 2, The import currently brings in Path from 'react-hook-form' as a value import; change it to a type-only import so Path is imported only for types (leave useForm as a normal import). Update the top-level import that references Path to a type import and keep the runtime import for useForm unchanged; this affects the statement that casts keys with "key as Path<LoginForm>" and ensures Path is erased at runtime.backend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs (1)
502-510: Add an index for recent activity-log reads.The activity API returns recent logs and supports filters, but only
PerformedByUserIdis indexed. Add an index such asTimestampdescending, or composite indexes aligned withentityType/userId + Timestamp, in the source EF configuration and regenerate the migration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs` around lines 502 - 510, The snapshot shows only an index on PerformedByUserId; update the EF Core entity configuration for the SystemActivityLog entity (e.g., in the EntityTypeBuilder for SystemActivityLog inside OnModelCreating or the SystemActivityLogConfiguration class) to add an index on the Timestamp property (preferably as descending) and/or a composite index such as (EntityType, PerformedByUserId, Timestamp) to support recent reads and common filters, using HasIndex(...).Then regenerate the migration so the ApplicationDbContextModelSnapshot and migration files include the new index definitions and apply the migration.backend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/ActivityLogRepository.cs (1)
21-54: Repository eagerly commits, andcountis unbounded.
AddAsynccallsSaveChangesAsyncinternally. If the caller is part of a larger unit of work (e.g., status change + activity log fromOrderService.MarkReadyAsync), the log is persisted even if subsequent operations fail — or, conversely, the caller's prior un-flushed changes get flushed alongside the log. Consider letting the caller ownSaveChangesAsyncso the log row participates in the same transaction as the action it describes.GetRecentLogsAsync(int count, …)does not validate/capcount. A large or negative value will either thrash the DB or (for negatives) produce an EF runtime error. Clamp, e.g.,count = Math.Clamp(count, 1, 500);.♻️ Suggested fix for the count bound
public async Task<IEnumerable<SystemActivityLog>> GetRecentLogsAsync(int count, string? entityType = null, long? userId = null, CancellationToken cancellationToken = default) { + if (count <= 0) count = 50; + count = System.Math.Min(count, 500); var query = _context.SystemActivityLogs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/ActivityLogRepository.cs` around lines 21 - 54, The AddAsync method currently calls _context.SaveChangesAsync causing eager commits; change AddAsync(SystemActivityLog log, ...) to only call _context.SystemActivityLogs.AddAsync(log, cancellationToken) and remove the SaveChangesAsync call so callers control transaction/commit (e.g., OrderService.MarkReadyAsync can flush once). In GetRecentLogsAsync(int count, ...) validate and clamp count (e.g., count = Math.Clamp(count, 1, 500)) before applying Take(count) to prevent negative or excessive values; keep existing filtering and ordering logic intact.backend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/ISystemNotificationRepository.cs (1)
14-14:UpdateAsyncmissingCancellationToken— inconsistent with the rest of the interface.Every other method on
ISystemNotificationRepositoryaccepts aCancellationToken ct. Add one here too for API consistency and to allow cancellation to flow through the service/controller layers.- Task<bool> UpdateAsync(SystemNotification notification); + Task<bool> UpdateAsync(SystemNotification notification, CancellationToken ct);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/ISystemNotificationRepository.cs` at line 14, The UpdateAsync method on ISystemNotificationRepository is missing a CancellationToken parameter; update its signature to Task<bool> UpdateAsync(SystemNotification notification, CancellationToken ct) so it matches the other methods and allows cancellation to flow; ensure you update any implementations of ISystemNotificationRepository (classes implementing UpdateAsync) and any call sites (service/controller layers) to pass the CancellationToken through.frontend/src/modules/admin/pages/RevenueReportPage.tsx (1)
60-82: Currency formatting & CSV button/headers bypass i18n and hardcode USD.
`$${row.amount.toFixed(2)}`,`$${d.totalRevenue.toFixed(2)}`, etc., hardcode the$symbol. The codebase already usesPriceText(used inCartPage) for consistent currency rendering — prefer that so locale/currency changes stay centralized.Export CSVbutton label and the CSV headers['Date', 'Orders', 'Revenue']are not translated. At minimum translate the button label; the CSV headers can stay English if that's a deliberate "machine-readable export" choice, but document it.- Minor: exporting raw
row.dateinto CSV (line 73) while displaying it viaformatDate(..., dateLocale)means CSV and UI diverge — consider exporting the formatted string if the CSV is intended for end-users.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/admin/pages/RevenueReportPage.tsx` around lines 60 - 82, Replace hardcoded USD symbols and raw date export: use the existing PriceText component to render amounts (replace references to row.amount and d.totalRevenue in the table and header export) so currency formatting follows app locale/currency; for CSV export, pass the same formatted date string (use formatDate(date, dateLocale) or the same formatting helper used in the UI) instead of raw r.date. Also make the export button label translatable (use t.admin.exportCsv or appropriate i18n key) and document that CSV headers ['Date','Orders','Revenue'] are intentionally kept machine-readable if that is desired; update downloadCSV usage in the Export button click handler accordingly.frontend/src/modules/cart/pages/CartPage.tsx (1)
19-19: AvoidanyforCartItemRowprops.The
updateItem,deleteItem, andtprops are typed asany, which defeats TS checking in an otherwise typed component. Prefer the actual mutate signatures returned by the hooks and thettype exported from the i18n context.♻️ Suggested typing
-function CartItemRow({ item, updateItem, deleteItem, t }: { item: CartItem; updateItem: any; deleteItem: any; t: any }) { +type UpdateItemFn = (vars: { itemId: number; quantity: number }) => void; +type DeleteItemFn = (itemId: number) => void; + +function CartItemRow({ item, updateItem, deleteItem, t }: { + item: CartItem; + updateItem: UpdateItemFn; + deleteItem: DeleteItemFn; + t: ReturnType<typeof useI18n>['t']; +}) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/cart/pages/CartPage.tsx` at line 19, CartItemRow currently uses `any` for updateItem, deleteItem, and t which loses type safety; replace those `any` types with the actual mutate function signatures from your cart hooks and the i18n T-function type. Concretely, change the prop types on function CartItemRow to use the mutate types returned by your hooks (e.g. the `mutate`/`mutateAsync` type from your useUpdateCartItem / useDeleteCartItem hooks or React Query/SWR MutateFunction return types) and import the i18n TFunction (or the `T`/`TFunction` type exported by your i18n setup) for `t`; keep CartItem as the item type. This ensures updateItem/deleteItem match the real hook signatures (mutate/mutateAsync) and `t` uses the i18n TFunction.backend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/SystemSettingsRepository.cs (1)
31-52: Remove redundant.Update()call and documentnulldescription semantics.
_context.SystemSettings.Update(existing)is unnecessary —existingis already tracked afterFirstOrDefaultAsync, so EF change tracking will pick up property mutations. CallingUpdatemarks all properties as modified, which can bloat the generated UPDATE statement.if (description != null) existing.Description = description;silently preserves the existing description when callers passnull. If this partial-update behavior is intentional, document it; otherwise, callers have no way to clear a description via this upsert.♻️ Proposed tweak
if (existing != null) { existing.Value = value; if (description != null) existing.Description = description; existing.UpdatedAt = DateTime.UtcNow; - _context.SystemSettings.Update(existing); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/SystemSettingsRepository.cs` around lines 31 - 52, Remove the unnecessary _context.SystemSettings.Update(existing) call in SystemSettingsRepository.UpsertAsync (existing is already tracked after FirstOrDefaultAsync) and add documentation on the UpsertAsync method explaining that passing description == null preserves the current Description (i.e., null means "do not modify"); if clearing the description should be supported instead, adjust the method contract (e.g., use an explicit flag or sentinel) before changing behavior.frontend/src/modules/customer/pages/CheckoutPage.tsx (1)
26-34: Migrate from deprecatedmessagetoerrorparameter in Zod 4.In Zod 4, the
messageoption onz.nativeEnum()(and other schema methods) is still honored for backward compatibility, but has been deprecated in favor of the unifiederrorparameter. While the current code works, migrating toerroris recommended for future compatibility. The same pattern should be updated inRegisterPage.tsxandInventoryManagePage.tsx.Proposed change
- paymentMethod: z.preprocess( - (val) => Number(val), - z.nativeEnum(PaymentMethod, { - message: t.validation?.required || 'Payment method is required' - }) - ), + paymentMethod: z.preprocess( + (val) => Number(val), + z.nativeEnum(PaymentMethod, { + error: t.validation?.required || 'Payment method is required', + }), + ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/customer/pages/CheckoutPage.tsx` around lines 26 - 34, Update the Zod nativeEnum option to use the new unified error parameter: in the checkoutSchema where z.nativeEnum(PaymentMethod, { message: ... }) is used, replace the deprecated message option with error: { message: t.validation?.required || 'Payment method is required' }; apply the same change for the other occurrences referenced (e.g., RegisterPage.tsx and InventoryManagePage.tsx) so all z.nativeEnum calls use error: { message: ... } instead of message.frontend/src/modules/customer/pages/RegisterPage.tsx (1)
18-42: All validation keys are defined in both locales; consider consistent safe fallbacks.The referenced validation messages (
minLength,required,invalidEmail,passwordStrength,passwordNoMatch) all exist in bothen.tsandar.ts. However, theas stringcasts suppress type awareness at compile time. For consistency across the codebase, adopt the safer pattern already used inCheckoutPage.tsx:
- Use
t.validation?.<key> ?? 'fallback'instead ofas string- This makes the fallback behavior explicit and maintains type safety
A small helper within the
useMemocan reduce repetition:const msg = (k: keyof typeof t.validation, fb: string) => t.validation?.[k] ?? fb;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/customer/pages/RegisterPage.tsx` around lines 18 - 42, The registerSchema useMemo currently uses unsafe "as string" casts for translation keys; replace those casts with explicit safe fallbacks using the nullish coalescing pattern (t.validation?.<key> ?? '<fallback>') and reduce repetition by adding a small helper inside the same useMemo (e.g., const msg = (k, fb) => t.validation?.[k] ?? fb) and then call msg('minLength','Minimum length is 2'), msg('required','Required'), msg('invalidEmail','Invalid email'), msg('passwordStrength','Password must meet strength requirements') and msg('passwordNoMatch','Passwords do not match') when constructing registerSchema (including inside superRefine) so type safety and clear defaults mirror the CheckoutPage.tsx approach.backend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/SystemNotificationRepository.cs (1)
56-68: PreferExecuteUpdateAsyncfor bulk updates inMarkAllAsReadAsync.The current implementation loads all unread notifications into memory, mutates them, and saves—inefficient for large backlogs. EF Core 8.0.24 (confirmed in the project file) supports
ExecuteUpdateAsyncfor server-sideUPDATEstatements, which is faster and avoids memory overhead.Suggested refactor
- public async Task<int> MarkAllAsReadAsync(CancellationToken ct) - { - var unread = await _context.SystemNotifications.Where(n => !n.IsRead).ToListAsync(ct); - if (!unread.Any()) return 0; - - foreach (var item in unread) - { - item.IsRead = true; - } - - _context.SystemNotifications.UpdateRange(unread); - return await _context.SaveChangesAsync(ct); - } + public Task<int> MarkAllAsReadAsync(CancellationToken ct) + { + return _context.SystemNotifications + .Where(n => !n.IsRead) + .ExecuteUpdateAsync(s => s.SetProperty(n => n.IsRead, true), ct); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/SystemNotificationRepository.cs` around lines 56 - 68, Replace the in-memory load-and-update in MarkAllAsReadAsync with a server-side update: call _context.SystemNotifications.Where(n => !n.IsRead).ExecuteUpdateAsync(...) to set IsRead = true (use SetProperty to assign true) and pass the CancellationToken; return the integer result from ExecuteUpdateAsync instead of calling ToListAsync/UpdateRange and SaveChangesAsync. Ensure you remove the unread variable/loop and keep using the existing CancellationToken (ct) when invoking ExecuteUpdateAsync.frontend/src/modules/manager/pages/OrderDetailsPage.tsx (1)
120-145: Derive timeline progress fromstatusStepsindexes instead of enum values.The current implementation using
order.status / 3andorder.status >= step.keydepends onOrderStatushaving exactly the values0..3in timeline order, which is fragile. While the enum currently matches this (PROCESSING=0, READY=1, OUT_FOR_DELIVERY=2, DELIVERED=3), deriving progress fromstatusStepsarray indexes would be more maintainable and prevent breakage if enum values change.♻️ Proposed refactoring
const statusSteps = [ { key: OrderStatus.PROCESSING, label: t.orders.processing, icon: Clock }, { key: OrderStatus.READY, label: t.orders.ready, icon: Package }, { key: OrderStatus.OUT_FOR_DELIVERY, label: t.orders.outForDelivery, icon: Truck }, { key: OrderStatus.DELIVERED, label: t.orders.delivered, icon: CheckCircle2 }, ]; + const currentStepIndex = statusSteps.findIndex((step) => step.key === order.status); + const progress = + currentStepIndex <= 0 ? 0 : (currentStepIndex / (statusSteps.length - 1)) * 100; @@ - <div className="absolute top-4 left-0 h-0.5 bg-primary-500 z-0 transition-all" style={{ width: `${(order.status / 3) * 100}%` }} /> - {statusSteps.map((step) => { + <div className="absolute top-4 left-0 h-0.5 bg-primary-500 z-0 transition-all" style={{ width: `${progress}%` }} /> + {statusSteps.map((step, stepIndex) => { const Icon = step.icon; - const isCompleted = order.status >= step.key; + const isCompleted = currentStepIndex >= stepIndex; const isCurrent = order.status === step.key;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/manager/pages/OrderDetailsPage.tsx` around lines 120 - 145, The timeline progress should be computed from the statusSteps array indexes instead of assuming numeric enum values; find the currentStepIndex by locating the index in statusSteps where step.key === order.status, compute progressPercent = (currentStepIndex / (statusSteps.length - 1)) * 100 and use that for the width style instead of (order.status / 3) * 100, and replace the isCompleted check to compare indices (currentStepIndex >= stepIndex) when mapping statusSteps (refer to statusSteps, step.key, order.status and the inline style width and isCompleted logic).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c3022f52-c713-4897-ae86-63dd60387f23
⛔ Files ignored due to path filters (9)
backend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/2f882db4-81a8-4d6f-9908-111b10653e1c.jpegis excluded by!**/*.jpegbackend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/4ae09ddc-a55b-4898-a259-486efab33f1b.jpegis excluded by!**/*.jpegbackend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/639ee342-8d80-4e55-9a76-b6e6ddbeb8e4.jpgis excluded by!**/*.jpgbackend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/a7a1b5dd-9d1b-4d20-9abd-44159d484670.jpegis excluded by!**/*.jpegbackend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/c0e3001d-baa5-4f45-ba42-e68b4296146e.pngis excluded by!**/*.pngbackend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/ce9909aa-3b84-44ce-8e92-8a870edffd39.jpegis excluded by!**/*.jpegbackend/OrderSystemApi/OrderSystemApi/wwwroot/uploads/d3a0b2aa-0ded-4465-83bf-7957a17a7ca7.jpegis excluded by!**/*.jpegfrontend/package-lock.jsonis excluded by!**/package-lock.jsonfrontend/public/default-product.pngis excluded by!**/*.png
📒 Files selected for processing (134)
ProjectInstructions.txtbackend/OrderSystemApi/OrderSystem.Application/Admin/DTOs/Requests/AdminRequests.csbackend/OrderSystemApi/OrderSystem.Application/Admin/DTOs/Responses/AdminOrderDto.csbackend/OrderSystemApi/OrderSystem.Application/Admin/DTOs/Responses/AdminUserDto.csbackend/OrderSystemApi/OrderSystem.Application/Admin/DTOs/Responses/DashboardResponse.csbackend/OrderSystemApi/OrderSystem.Application/Admin/DTOs/Responses/ManagerPerformanceDto.csbackend/OrderSystemApi/OrderSystem.Application/Admin/DTOs/Responses/RevenueReportResponse.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/IActivityLogRepository.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/IActivityLogService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/IAdminNotificationDispatcher.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/IAdminRepository.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/IAdminService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/ISystemNotificationRepository.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/ISystemNotificationService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/ISystemSettingsRepository.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Interfaces/ISystemSettingsService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Services/ActivityLogService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Services/AdminService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Services/SystemNotificationService.csbackend/OrderSystemApi/OrderSystem.Application/Admin/Services/SystemSettingsService.csbackend/OrderSystemApi/OrderSystem.Application/Inventorys/Services/InventoryService.csbackend/OrderSystemApi/OrderSystem.Application/Orders/DTOs/Responses/OrderDetailsResponse.csbackend/OrderSystemApi/OrderSystem.Application/Orders/Interfaces/IOrderService.csbackend/OrderSystemApi/OrderSystem.Application/Orders/Services/OrderService.csbackend/OrderSystemApi/OrderSystem.Application/Payments/Interfaces/IPaymentService.csbackend/OrderSystemApi/OrderSystem.Application/Payments/Services/PaymentService.csbackend/OrderSystemApi/OrderSystem.Application/Products/Interfaces/IProductRepository.csbackend/OrderSystemApi/OrderSystem.Application/Products/Interfaces/IProductService.csbackend/OrderSystemApi/OrderSystem.Application/Products/Services/ProductService.csbackend/OrderSystemApi/OrderSystem.Domain/Entities/OrderItem.csbackend/OrderSystemApi/OrderSystem.Domain/Entities/Product.csbackend/OrderSystemApi/OrderSystem.Domain/Entities/SystemActivityLog.csbackend/OrderSystemApi/OrderSystem.Domain/Entities/SystemNotification.csbackend/OrderSystemApi/OrderSystem.Domain/Entities/SystemSetting.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Data/ApplicationDbContext.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Data/Configurations/OrderItemConfiguration.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Data/Configurations/SystemActivityLogConfiguration.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Data/Configurations/SystemNotificationConfiguration.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Data/Configurations/SystemSettingConfiguration.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418174733_AddSystemActivityLog.Designer.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418174733_AddSystemActivityLog.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418180805_AddSystemNotification.Designer.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418180805_AddSystemNotification.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418192929_AddSystemSettings.Designer.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/20260418192929_AddSystemSettings.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/ActivityLogRepository.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/AdminRepository.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/ProductRepository.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/SystemNotificationRepository.csbackend/OrderSystemApi/OrderSystem.Infrastructure/Repositories/SystemSettingsRepository.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Admin/AdminActivityController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Admin/AdminCatalogController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Admin/AdminController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Admin/AdminNotificationsController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Admin/AdminSettingsController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Manager/ManagerCategoriesController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Manager/ManagerOrdersController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Manager/ManagerProductImagesController.csbackend/OrderSystemApi/OrderSystemApi/Controllers/Manager/UploadController.csbackend/OrderSystemApi/OrderSystemApi/Hubs/AdminHub.csbackend/OrderSystemApi/OrderSystemApi/OrderSystem.Api.csprojbackend/OrderSystemApi/OrderSystemApi/Program.csbackend/OrderSystemApi/OrderSystemApi/Services/AdminNotificationDispatcher.csbackend/OrderSystemApi/OrderSystemApi/crash_log.txtfrontend/index.htmlfrontend/package.jsonfrontend/src/App.cssfrontend/src/app/api/http.tsfrontend/src/app/i18n/direction.tsfrontend/src/app/i18n/i18n-context.tsxfrontend/src/app/i18n/messages/ar.tsfrontend/src/app/i18n/messages/en.tsfrontend/src/app/providers/AppProviders.tsxfrontend/src/app/router/index.tsxfrontend/src/app/store/auth-context.tsxfrontend/src/index.cssfrontend/src/layouts/AdminLayout.tsxfrontend/src/layouts/CustomerLayout.tsxfrontend/src/layouts/ManagerLayout.tsxfrontend/src/layouts/PublicLayout.tsxfrontend/src/modules/admin/api/admin.api.tsfrontend/src/modules/admin/components/NotificationBell.tsxfrontend/src/modules/admin/hooks/useAdmin.tsfrontend/src/modules/admin/pages/ActivityLogPage.tsxfrontend/src/modules/admin/pages/AdminCatalogPage.tsxfrontend/src/modules/admin/pages/AdminSystemSettingsPage.tsxfrontend/src/modules/admin/pages/DashboardPage.tsxfrontend/src/modules/admin/pages/InventoryStatusPage.tsxfrontend/src/modules/admin/pages/LowStockPage.tsxfrontend/src/modules/admin/pages/ManagerPerformancePage.tsxfrontend/src/modules/admin/pages/OrderOverviewPage.tsxfrontend/src/modules/admin/pages/RevenueReportPage.tsxfrontend/src/modules/admin/pages/UsersPage.tsxfrontend/src/modules/auth/pages/LoginPage.tsxfrontend/src/modules/auth/pages/SettingsPage.tsxfrontend/src/modules/cart/pages/CartPage.tsxfrontend/src/modules/catalog/pages/ProductDetailsPage.tsxfrontend/src/modules/catalog/pages/ProductsListPage.tsxfrontend/src/modules/customer/pages/CheckoutPage.tsxfrontend/src/modules/customer/pages/CustomerOrderDetailsPage.tsxfrontend/src/modules/customer/pages/CustomerOrdersListPage.tsxfrontend/src/modules/customer/pages/PaymentPage.tsxfrontend/src/modules/customer/pages/RegisterPage.tsxfrontend/src/modules/error/pages/NotFoundPage.tsxfrontend/src/modules/manager/api/orders.api.tsfrontend/src/modules/manager/api/products.api.tsfrontend/src/modules/manager/api/upload.api.tsfrontend/src/modules/manager/hooks/useManagerOrders.tsfrontend/src/modules/manager/hooks/useManagerProducts.tsfrontend/src/modules/manager/pages/CategoriesPage.tsxfrontend/src/modules/manager/pages/InventoryListPage.tsxfrontend/src/modules/manager/pages/InventoryManagePage.tsxfrontend/src/modules/manager/pages/OrderDetailsPage.tsxfrontend/src/modules/manager/pages/OrdersPage.tsxfrontend/src/modules/manager/pages/ProductEditPage.tsxfrontend/src/modules/manager/pages/ProductsPage.tsxfrontend/src/shared/components/AppTable.tsxfrontend/src/shared/components/DetailList.tsxfrontend/src/shared/components/EmptyState.tsxfrontend/src/shared/components/Footer.tsxfrontend/src/shared/components/FormSection.tsxfrontend/src/shared/components/ImageFallback.tsxfrontend/src/shared/components/LanguageSwitcher.tsxfrontend/src/shared/components/PageContainer.tsxfrontend/src/shared/components/ProductCard.tsxfrontend/src/shared/components/QuantityStepper.tsxfrontend/src/shared/components/SectionHeading.tsxfrontend/src/shared/components/StatCard.tsxfrontend/src/shared/components/TableToolbar.tsxfrontend/src/shared/types/orders.tsfrontend/src/shared/utils/date.tsfrontend/src/shared/utils/error.tsfrontend/src/shared/utils/exportUtils.ts
| public class BulkPriceRequest | ||
| { | ||
| public List<long> ProductIds { get; set; } = new(); | ||
| public decimal PercentageChange { get; set; } | ||
| } | ||
|
|
||
| [HttpPut("bulk-price")] | ||
| public async Task<IActionResult> BulkPrice([FromBody] BulkPriceRequest request, CancellationToken ct) | ||
| { | ||
| var count = await _productService.BulkUpdatePriceAsync(request.ProductIds, request.PercentageChange, ct); | ||
| return Ok(new { message = $"Successfully updated {count} products." }); |
There was a problem hiding this comment.
Make bulk price adjustments safe against retries and invalid ranges.
PUT bulk-price applies a relative change, so a retry can apply the percentage again and corrupt prices. It also accepts unbounded percentages. Use a non-idempotent action endpoint plus validation, or require an idempotency key.
🐛 Proposed guardrails
- [HttpPut("bulk-price")]
+ [HttpPost("bulk-price-adjustments")]
public async Task<IActionResult> BulkPrice([FromBody] BulkPriceRequest request, CancellationToken ct)
{
+ if (request.ProductIds.Count == 0)
+ return BadRequest(new { message = "At least one product must be selected." });
+
+ if (request.PercentageChange <= -100m || request.PercentageChange > 100m)
+ return BadRequest(new { message = "Percentage change must be greater than -100 and at most 100." });
+
var count = await _productService.BulkUpdatePriceAsync(request.ProductIds, request.PercentageChange, ct);
return Ok(new { message = $"Successfully updated {count} products." });
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/OrderSystemApi/OrderSystemApi/Controllers/Admin/AdminCatalogController.cs`
around lines 35 - 45, The BulkPrice endpoint is unsafe for retries and accepts
unbounded percentages; update the BulkPrice action and BulkPriceRequest handling
to require and validate an idempotency key and sensible percentage ranges: add a
mandatory string idempotencyKey (from header or body) to the BulkPrice request
handling and call an idempotency guard (e.g.,
IIdempotencyService.EnsureNotProcessedAsync(idempotencyKey)) before performing
the update using _productService.BulkUpdatePriceAsync, validate
request.ProductIds is not empty and validate PercentageChange is within a safe
range (e.g., -100 to 100) and reject invalid requests with BadRequest; ensure
the idempotency key is passed through or recorded so repeated requests with the
same key are no-ops and include reference to BulkPriceRequest, the BulkPrice
controller action, and _productService.BulkUpdatePriceAsync.
| [ApiController] | ||
| [Route("api/v1/manager/[controller]")] | ||
| [Authorize] | ||
| public class UploadController : ControllerBase |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm other manager controllers use role-based auth so we align with existing pattern.
rg -nP -C2 '\[Authorize' -g 'backend/**/Controllers/Manager/*.cs'Repository: NowwarAbuRashed/OrderSystem
Length of output: 3725
Missing role-based authorization on a manager-only endpoint.
[Authorize] only requires any authenticated principal, so a regular customer token can POST to /api/v1/manager/upload and drop files into wwwroot/uploads. All other manager controllers enforce role-based authorization — this endpoint should match that pattern.
- [Authorize]
+ [Authorize(Roles = "MANAGER,ADMIN")]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/OrderSystemApi/OrderSystemApi/Controllers/Manager/UploadController.cs`
around lines 6 - 9, The UploadController is protected only by [Authorize],
allowing any authenticated user to access manager-only endpoints; update the
class-level authorization to require the manager role (match other manager
controllers) by replacing or augmenting [Authorize] on the UploadController with
the role-based attribute used in the project (e.g., [Authorize(Roles =
"Manager")] or the equivalent policy name) so POST /api/v1/manager/upload is
restricted to managers only; ensure the attribute is applied to the
UploadController class declaration and matches the project's existing
role/policy convention.
| const { data } = await http.post<{ url: string }>('/api/v1/manager/upload', formData, { | ||
| headers: { 'Content-Type': 'multipart/form-data' } | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "upload.api.ts" -type fRepository: NowwarAbuRashed/OrderSystem
Length of output: 119
🏁 Script executed:
cat -n frontend/src/modules/manager/api/upload.api.ts | head -30Repository: NowwarAbuRashed/OrderSystem
Length of output: 479
🌐 Web query:
axios FormData multipart/form-data boundary issue Content-Type header
💡 Result:
The common issue with axios, FormData, and multipart/form-data boundary occurs when developers manually set the Content-Type header to 'multipart/form-data' without including the required boundary parameter. This leads to servers rejecting requests because the boundary (which delimits parts in the multipart body) is missing from the header, causing errors like "Multipart: Boundary not found". Key points from official docs and issues: - Do not manually set Content-Type: 'multipart/form-data' when using FormData. Axios (in browser/XHR adapter) automatically detects FormData, sets the correct Content-Type with boundary generated by the browser, and sends the request properly. Manual override bypasses this, omitting the boundary. Example (correct): const formData = new FormData; formData.append('file', file); axios.post('/upload', formData); // Auto-handles Content-Type: multipart/form-data; boundary=----WebKitFormBoundary... - In Node.js, axios uses the form-data package, which also auto-generates boundary. Same rule applies: omit manual Content-Type. - Historical bugs: In v0.27.0, removing auto-deletion of manual Content-Type broke browser FormData (fixed in v0.27.2 by partial revert). Recent fetch adapter issue (#7054, closed Apr 2026) had similar problems when manually setting header. - Workarounds if needed (e.g., non-standard env): Use formData.getBoundary to append manually, but avoid if possible: headers: { 'Content-Type': multipart/form-data; boundary=${formData.getBoundary} } But official recommendation: let axios/browser handle it. - Shortcut methods like axios.postForm preset the header correctly for multipart. This behavior aligns with browser Fetch/XMLHttpRequest spec: manual Content-Type without boundary prevents auto-addition. Use latest axios (v1.7+ as of 2026) for best FormData support, including fetch adapter improvements.
Citations:
- 1: FormData request header doesn't include boundary axios/axios#4631
- 2: Axios does not append form boundary for multipart requests in node environment axios/axios#5379
- 3: Axios fetch adapter: Missing boundary when Content-Type is set manually with FormData axios/axios#7054
- 4: Form data - start boundary issue axios/axios#381
- 5: https://stackoverflow.com/questions/75742226/cant-set-content-type-header-for-axios-with-formdata
- 6: Fixing remove Content-Type when data is instance of FormData axios/axios#3785
- 7: Don't delete Content-Type when formData axios/axios#767
- 8: https://stackoverflow.com/questions/49579640/how-to-send-data-correct-axios-error-multipart-boundary-not-found
- 9: https://axios-http.com/docs/multipart
- 10: https://github.com/axios/axios/releases/tag/v0.27.0
- 11: Fixed FormData posting in browser environment by reverting #3785; axios/axios#4640
- 12: https://github.com/axios/axios/tree/main
Don't hard-code Content-Type: multipart/form-data — the boundary is lost.
When you pass FormData to axios, it automatically sets Content-Type with the required ; boundary=... parameter. Manually overriding it with the bare string 'multipart/form-data' bypasses this auto-detection and omits the boundary. Most servers (including Kestrel/ASP.NET Core multipart parsers) will reject the request with a parsing error like "Multipart: Boundary not found". Let axios infer the header:
🛠 Proposed fix
- const { data } = await http.post<{ url: string }>('/api/v1/manager/upload', formData, {
- headers: { 'Content-Type': 'multipart/form-data' }
- });
+ const { data } = await http.post<{ url: string }>('/api/v1/manager/upload', formData);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/manager/api/upload.api.ts` around lines 7 - 9, The
request is manually setting the 'Content-Type' header which strips the multipart
boundary axios would normally add; update the upload call (the http.post usage
that posts formData in upload.api.ts) to remove the explicit 'Content-Type':
'multipart/form-data' header so axios can auto-set the header including the
boundary (if you must keep a headers object for other headers, ensure it does
not include Content-Type). Locate the http.post call that returns { data } and
delete or omit the Content-Type entry only.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/modules/manager/api/categories.api.ts (1)
4-12:⚠️ Potential issue | 🔴 CriticalgetManagerCategoryById() calls a non-existent backend endpoint.
The review's premise about moving reads to versioned manager endpoints is incorrect—
ManagerCategoriesControllerhas no GET methods and the architectural split is intentional. However, the frontend functiongetManagerCategoryById(id)callsGET /api/Categories/{id}, which doesn't exist:CategoriesControlleronly implementsGetCategories()with no by-ID variant. This function will fail at runtime.Either add a
GetCategoryById(id)endpoint toCategoriesController, or use a different approach (e.g., fetch all categories and filter client-side, or add the endpoint toManagerCategoriesControllerif this is manager-specific data).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/manager/api/categories.api.ts` around lines 4 - 12, getManagerCategoryById calls a non-existent backend endpoint; either implement a GET /api/Categories/{id} (add GetCategoryById(id) to CategoriesController or add equivalent to ManagerCategoriesController) or change the frontend to not call that endpoint—update getManagerCategoryById to call getManagerCategories() and filter the returned array for the matching id (or remove the function and use getManagerCategories where needed). Ensure the chosen fix updates references to getManagerCategoryById and documents the new backend method name GetCategoryById if added.
🧹 Nitpick comments (6)
frontend/src/modules/catalog/pages/ProductDetailsPage.tsx (1)
109-109: Avoidanyfor product images.
product.imagesalready has a type fromuseProductQuery; typingimgasanyhere discards it and risks silent drift if the backend shape changes (e.g.,imageUrl,altText,id). Prefer the inferred element type (or an explicitProductImagetype) so the compiler catches accidental misuse in this JSX block, inproduct.images?.[selectedImage]?.imageUrl, and in the arrow-nav handlers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx` at line 109, The map callback currently types the image parameter as any which discards the real type from useProductQuery; change the map to use the inferred element type (remove : any) or explicitly use the product image type (e.g., ProductImage) so TypeScript can validate properties like imageUrl/altText/id; update occurrences that reference img (inside product.images.map(...), product.images?.[selectedImage]?.imageUrl, and the arrow-nav handlers) to rely on the correct type instead of any so the compiler will catch mismatches.frontend/src/modules/catalog/pages/ProductsListPage.tsx (2)
17-19: Harden URL param parsing forcategoryId.If the URL carries a non-numeric
categoryId(manual edit, stale link, bookmark),Number('abc')yieldsNaN, which is then passed intouseProductsQuery({ ..., categoryId: NaN })and ultimately into the backend request. Minor nit:searchParams.get('categoryId')is also called twice.♻️ Proposed fix
- const page = Number(searchParams.get('page')) || 1; - const search = searchParams.get('search') || ''; - const categoryId = searchParams.get('categoryId') ? Number(searchParams.get('categoryId')) : undefined; + const page = Number(searchParams.get('page')) || 1; + const search = searchParams.get('search') || ''; + const rawCategoryId = searchParams.get('categoryId'); + const parsedCategoryId = rawCategoryId ? Number(rawCategoryId) : undefined; + const categoryId = Number.isFinite(parsedCategoryId) ? parsedCategoryId : undefined;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/catalog/pages/ProductsListPage.tsx` around lines 17 - 19, The categoryId parsing is fragile: calling searchParams.get('categoryId') twice and passing Number(...) can produce NaN which then flows into useProductsQuery; change parsing to call searchParams.get('categoryId') once, store it in a local variable (e.g., rawCategoryId), validate it with Number.isInteger or parseInt and isFinite, and only set categoryId when it is a valid number (otherwise leave undefined); update the variable used in useProductsQuery({..., categoryId}) so the backend never receives NaN.
147-156: All "Add to cart" buttons disable together during any in-flight add.
useAddCartItemis called once at the page level, soisAddingToCartis a single boolean shared across every renderedProductCard. The moment a user clicks Add on one card, every other card's button also becomes disabled until the mutation resolves. Works, but may feel laggy on slower networks; if per-card disable is desired, track the pendingproductIdlocally (e.g.,const [pendingId, setPendingId] = useState<number | null>(null)) and passisAddingToCart={pendingId === prod.id}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/catalog/pages/ProductsListPage.tsx` around lines 147 - 156, The page-level boolean from useAddCartItem causes every ProductCard's isAddingToCart prop to disable all buttons simultaneously; change to track the pending product id locally (e.g., add state pendingId with useState<number | null>(null)) and update handleAddToCart to setPendingId(prod.id) before calling the mutation and clear it (setPendingId(null)) in the mutation's finally/onSettled handler so each ProductCard receives isAddingToCart={pendingId === prod.id}; keep using useAddCartItem for the mutation itself but only derive per-card disable state from pendingId.frontend/src/layouts/CustomerLayout.tsx (1)
55-66: Header search replaces, rather than merges, existing filter params.When a user is already on
/products?categoryId=3&page=2and submits the header search,navigate('/products?search=...')dropscategoryId(andpage, which is probably desired). Consider preservingcategoryIdso the header search behaves like an additional filter rather than a full reset.♻️ Possible approach
- onSubmit={(e) => { - e.preventDefault(); - const search = new FormData(e.currentTarget).get('search') as string; - if (search) { - navigate(`/products?search=${encodeURIComponent(search)}`); - } else { - navigate('/products'); - } - }} + onSubmit={(e) => { + e.preventDefault(); + const search = (new FormData(e.currentTarget).get('search') as string) ?? ''; + const params = new URLSearchParams(window.location.pathname === '/products' ? window.location.search : ''); + if (search) params.set('search', search); + else params.delete('search'); + params.delete('page'); + const qs = params.toString(); + navigate(qs ? `/products?${qs}` : '/products'); + }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/layouts/CustomerLayout.tsx` around lines 55 - 66, The header search form's onSubmit handler currently calls navigate('/products?search=...') which replaces existing query params (dropping categoryId); update the handler in CustomerLayout's form onSubmit so it reads the current location.search (via useLocation or the existing router location), parses query params, sets/updates the "search" param while preserving existing params like "categoryId", then builds the new query string and calls navigate(`/products?${newQuery}`); ensure that when the search is empty the "search" param is removed (not set to empty) so other filters remain intact.frontend/src/modules/manager/pages/InventoryManagePage.tsx (2)
127-152: Card title doesn't match its contents.The overview card's
CardTitleist.manager.productQuantity(rendered as "Current Stock"), but the card body renders both the current stock and the min-quantity tile. Consider a more general title key (e.g. a newt.manager.stockOverview) so the header accurately represents the section.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx` around lines 127 - 152, The Card header currently uses t.manager.productQuantity (CardTitle) which mislabels the section because the body displays both product.quantity and product.minQuantity; change the title to a more general key (e.g., add and use t.manager.stockOverview) and update the CardTitle to use that new key (replace t.manager.productQuantity with t.manager.stockOverview) so the header accurately reflects the CardContent that includes the current stock, minQuantity tiles and the isLow warning.
68-81: Defensive mapping of unknown server error keys.The fallback branch casts any server field name to
keyof InventoryFormType:setError(key as keyof InventoryFormType, { type: 'server', message: msg });The only form fields are
qtyandaction, so keys likeminQuantity(present onAdjustInventoryRequest) or any other DTO field will register an error against a non-existent input — the message is effectively swallowed and the user sees no feedback. Safer to surface unknown keys onroot.serverErrorinstead.♻️ Proposed fix
onError: (err: any) => { const map = getApiErrorMap(err); if (Object.keys(map).length > 0) { + const knownFields: Array<keyof InventoryFormType> = ['qty', 'action']; for (const [key, msg] of Object.entries(map)) { if (key === 'quantityDelta' || key === 'quantity') { setError('qty', { type: 'server', message: msg }); - } else { - setError(key as keyof InventoryFormType, { type: 'server', message: msg }); + } else if ((knownFields as string[]).includes(key)) { + setError(key as keyof InventoryFormType, { type: 'server', message: msg }); + } else { + setError('root.serverError', { type: 'server', message: msg }); } } } else { setError('root.serverError', { type: 'server', message: getApiErrorMessage(err) }); } }Also consider typing
errasunknownrather thanany.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx` around lines 68 - 81, The handler maps server error keys directly to form fields causing messages for unknown DTO fields to be lost; update the onError logic in InventoryManagePage to only call setError for actual form fields (e.g., map 'quantity' and 'quantityDelta' to the form field 'qty', and allow 'action' if present) and for any other unknown keys call setError('root.serverError', { type: 'server', message: msg }) so the user sees the message; use getApiErrorMap and setError as shown, and consider typing the err parameter as unknown instead of any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/layouts/CustomerLayout.tsx`:
- Around line 97-101: The mobile drawer badge in CustomerLayout.tsx is rendering
cartItemCount uncapped; mirror the desktop behavior by capping the displayed
value to '9+' when cartItemCount > 9. Locate the mobile badge rendering where
cartItemCount is directly output (the badge near the drawer markup) and replace
the direct value with a conditional expression similar to the desktop badge (use
cartItemCount > 9 ? '9+' : cartItemCount) so both desktop and mobile show the
same capped label.
- Around line 130-135: The mobile menu toggle button in CustomerLayout.tsx is
unlabeled and lacks aria-expanded/aria-controls; update the button that uses
setMobileMenuOpen and mobileMenuOpen to include an accessible label (use i18n
via t, e.g., t('nav.openMenu') / t('nav.closeMenu')), add
aria-expanded={mobileMenuOpen} and aria-controls pointing to the drawer
container id, and give the drawer container a matching id; add the two new
strings to the i18n nav namespace so the button label reflects the Menu/X state
for screen readers.
In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx`:
- Around line 86-99: Replace the hardcoded English aria-labels on the image
navigation buttons with the i18n translation keys used elsewhere (e.g.
t('products.previousImage') and t('products.nextImage')); locate the two buttons
that call setSelectedImage (the previous/next handlers referencing
product.images!.length) and change their aria-label props to use the translation
function (ensuring the component has access to t from useTranslation or the
existing i18n hook) so the labels become localized keys consistent with the rest
of the app.
- Around line 49-50: The hardcoded error strings in ProductDetailsPage (the
isNaN(id) check and the error/product null branch that returns <ErrorState ...
/> using getApiErrorMessage) must be localized: import/use the useI18n hook and
replace the literal messages with translation keys (e.g.
t.products.invalidProductId and t.products.notFound or similar), and add those
keys to both en.ts and ar.ts; keep getApiErrorMessage for API errors but wrap or
fallback to t.products.notFound when product is missing.
- Around line 197-214: The trust strip labels/descriptions in ProductDetailsPage
are hardcoded English; move each string into the translations under t.products
in both en.ts and ar.ts (e.g. keys like products.fastDelivery.label,
products.fastDelivery.desc, products.qualityGuarantee.label,
products.qualityGuarantee.desc, products.easyReturns.label,
products.easyReturns.desc), update the map in the JSX (the array mapped over
that uses Truck/Shield/RefreshCw and Icon) to reference t.products.<key>.label
and t.products.<key>.desc via the i18n `t` function, and ensure the new keys are
added to both locale files so Arabic renders correctly.
- Around line 37-46: The thumbnail auto-scroll logic in the useEffect uses
offsetLeft and scrollTo which break in RTL; update the effect that references
thumbnailsRef and selectedImage so that instead of computing scrollLeft you call
scrollIntoView on the active thumbnail element (i.e., replace the activeThumb
offsetLeft/scrollTo block inside the useEffect with
activeThumb?.scrollIntoView({ inline: 'center', behavior: 'smooth', block:
'nearest' })) to ensure consistent centering in both LTR and RTL.
In `@frontend/src/modules/catalog/pages/ProductsListPage.tsx`:
- Around line 29-36: The debounce effect in ProductsListPage reads `search` and
calls `setSearch` without listing them in deps; replace the dual-effect pattern
with a single debounce effect that uses a `useRef` (add to the React import) to
track the last-applied search value so you can safely include all real
dependencies. Specifically: add a ref like `lastAppliedSearchRef`, initialize it
to `search`, update it when you call `setSearch`, and change the existing
`useEffect` (the one that debounces `localSearch`) to depend on `localSearch`,
`search`, and `setSearch` (or simply `localSearch` if the ref eliminates the
need to read `search`), using the ref to avoid stale writes; remove the separate
sync effect that previously kept `localSearch` in step with `search`. Ensure
`lastAppliedSearchRef` is updated whenever `setSearch` runs.
In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx`:
- Around line 56-57: Several user-facing strings in InventoryManagePage are
still hardcoded; replace them with i18n keys using the t function (e.g.,
t('products.loadError'), t('products.invalid'), t('products.stockBelowMin'),
t('products.addStock'), t('products.remove'), t('products.noMovementHistory'))
wherever loadError, product/id validation, the low-stock label, Add Stock
button, Remove button, and the empty history message are used; add the
corresponding keys to the locale files (en.ts/ar.ts) and ensure the
InventoryManagePage imports/uses the t hook/prop already present in the file.
- Around line 62-67: The onSuccess handler in the opts object uses setTimeout to
clear setSuccessMessage after 3s but never clears the timer on unmount or before
scheduling a new one; create a ref (e.g., successTimeoutRef) to store the timer
id, clear any existing timeout before setting a new one, and add a useEffect
cleanup that clears successTimeoutRef.current on component unmount so
setSuccessMessage is not called on an unmounted component; update references in
the onSuccess block that call setSuccessMessage/reset to use this timeout
management.
- Around line 168-182: The stale validation error persists because
setValue('action', ...) is called without triggering validation; update the two
button handlers that call setValue('action', 'add') and setValue('action',
'remove') to pass shouldValidate: true so the qty field's superRefine check is
re-run when switching actions (i.e., call setValue('action', 'add', {
shouldValidate: true }) and setValue('action', 'remove', { shouldValidate: true
})); also add aria-pressed={actionValue === 'add'} / aria-pressed={actionValue
=== 'remove'} to the respective buttons for improved screen-reader
accessibility.
---
Outside diff comments:
In `@frontend/src/modules/manager/api/categories.api.ts`:
- Around line 4-12: getManagerCategoryById calls a non-existent backend
endpoint; either implement a GET /api/Categories/{id} (add GetCategoryById(id)
to CategoriesController or add equivalent to ManagerCategoriesController) or
change the frontend to not call that endpoint—update getManagerCategoryById to
call getManagerCategories() and filter the returned array for the matching id
(or remove the function and use getManagerCategories where needed). Ensure the
chosen fix updates references to getManagerCategoryById and documents the new
backend method name GetCategoryById if added.
---
Nitpick comments:
In `@frontend/src/layouts/CustomerLayout.tsx`:
- Around line 55-66: The header search form's onSubmit handler currently calls
navigate('/products?search=...') which replaces existing query params (dropping
categoryId); update the handler in CustomerLayout's form onSubmit so it reads
the current location.search (via useLocation or the existing router location),
parses query params, sets/updates the "search" param while preserving existing
params like "categoryId", then builds the new query string and calls
navigate(`/products?${newQuery}`); ensure that when the search is empty the
"search" param is removed (not set to empty) so other filters remain intact.
In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx`:
- Line 109: The map callback currently types the image parameter as any which
discards the real type from useProductQuery; change the map to use the inferred
element type (remove : any) or explicitly use the product image type (e.g.,
ProductImage) so TypeScript can validate properties like imageUrl/altText/id;
update occurrences that reference img (inside product.images.map(...),
product.images?.[selectedImage]?.imageUrl, and the arrow-nav handlers) to rely
on the correct type instead of any so the compiler will catch mismatches.
In `@frontend/src/modules/catalog/pages/ProductsListPage.tsx`:
- Around line 17-19: The categoryId parsing is fragile: calling
searchParams.get('categoryId') twice and passing Number(...) can produce NaN
which then flows into useProductsQuery; change parsing to call
searchParams.get('categoryId') once, store it in a local variable (e.g.,
rawCategoryId), validate it with Number.isInteger or parseInt and isFinite, and
only set categoryId when it is a valid number (otherwise leave undefined);
update the variable used in useProductsQuery({..., categoryId}) so the backend
never receives NaN.
- Around line 147-156: The page-level boolean from useAddCartItem causes every
ProductCard's isAddingToCart prop to disable all buttons simultaneously; change
to track the pending product id locally (e.g., add state pendingId with
useState<number | null>(null)) and update handleAddToCart to
setPendingId(prod.id) before calling the mutation and clear it
(setPendingId(null)) in the mutation's finally/onSettled handler so each
ProductCard receives isAddingToCart={pendingId === prod.id}; keep using
useAddCartItem for the mutation itself but only derive per-card disable state
from pendingId.
In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx`:
- Around line 127-152: The Card header currently uses t.manager.productQuantity
(CardTitle) which mislabels the section because the body displays both
product.quantity and product.minQuantity; change the title to a more general key
(e.g., add and use t.manager.stockOverview) and update the CardTitle to use that
new key (replace t.manager.productQuantity with t.manager.stockOverview) so the
header accurately reflects the CardContent that includes the current stock,
minQuantity tiles and the isLow warning.
- Around line 68-81: The handler maps server error keys directly to form fields
causing messages for unknown DTO fields to be lost; update the onError logic in
InventoryManagePage to only call setError for actual form fields (e.g., map
'quantity' and 'quantityDelta' to the form field 'qty', and allow 'action' if
present) and for any other unknown keys call setError('root.serverError', {
type: 'server', message: msg }) so the user sees the message; use getApiErrorMap
and setError as shown, and consider typing the err parameter as unknown instead
of any.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e389ea30-c42a-4a01-a04a-e2901c36ef85
📒 Files selected for processing (7)
frontend/src/layouts/CustomerLayout.tsxfrontend/src/modules/cart/pages/CartPage.tsxfrontend/src/modules/catalog/hooks/useCatalog.tsfrontend/src/modules/catalog/pages/ProductDetailsPage.tsxfrontend/src/modules/catalog/pages/ProductsListPage.tsxfrontend/src/modules/manager/api/categories.api.tsfrontend/src/modules/manager/pages/InventoryManagePage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/modules/cart/pages/CartPage.tsx
| {cartItemCount > 0 && ( | ||
| <span className="absolute -top-0.5 -right-0.5 w-5 h-5 bg-accent-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center ring-2 ring-white"> | ||
| {cartItemCount > 9 ? '9+' : cartItemCount} | ||
| </span> | ||
| )} |
There was a problem hiding this comment.
Cart-badge cap is inconsistent between desktop and mobile.
Desktop (line 99) shows 9+ once the count exceeds 9, but the mobile drawer badge (line 168) renders cartItemCount uncapped — so a count of 27 would display tightly inside a small pill. Apply the same cap for visual consistency.
🛠 Proposed fix
- {cartItemCount > 0 && (
- <span className="ml-auto bg-accent-500 text-white text-xs font-bold px-2 py-0.5 rounded-full">
- {cartItemCount}
- </span>
- )}
+ {cartItemCount > 0 && (
+ <span className="ml-auto bg-accent-500 text-white text-xs font-bold px-2 py-0.5 rounded-full">
+ {cartItemCount > 9 ? '9+' : cartItemCount}
+ </span>
+ )}Also applies to: 166-170
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/layouts/CustomerLayout.tsx` around lines 97 - 101, The mobile
drawer badge in CustomerLayout.tsx is rendering cartItemCount uncapped; mirror
the desktop behavior by capping the displayed value to '9+' when cartItemCount >
9. Locate the mobile badge rendering where cartItemCount is directly output (the
badge near the drawer markup) and replace the direct value with a conditional
expression similar to the desktop badge (use cartItemCount > 9 ? '9+' :
cartItemCount) so both desktop and mobile show the same capped label.
| <button | ||
| onClick={() => setMobileMenuOpen(!mobileMenuOpen)} | ||
| className="p-2 text-slate-500 hover:text-primary-600 hover:bg-primary-50 rounded-xl transition-all md:hidden" | ||
| > | ||
| {mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />} | ||
| </button> |
There was a problem hiding this comment.
A11y: mobile menu toggle is unlabeled and missing aria-expanded/aria-controls.
Screen readers will announce it only as "button"; there's no way for AT users to know it controls a menu or whether it's open. Since the icon swaps between Menu and X, the state must be conveyed via ARIA.
🛠 Proposed fix
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 text-slate-500 hover:text-primary-600 hover:bg-primary-50 rounded-xl transition-all md:hidden"
+ aria-label={mobileMenuOpen ? t.nav.closeMenu : t.nav.openMenu}
+ aria-expanded={mobileMenuOpen}
+ aria-controls="customer-mobile-drawer"
>
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>And on the drawer container (line 141):
- <div className="md:hidden border-t border-slate-100 bg-white shadow-lg">
+ <div id="customer-mobile-drawer" className="md:hidden border-t border-slate-100 bg-white shadow-lg">Add the two strings to your i18n nav namespace.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/layouts/CustomerLayout.tsx` around lines 130 - 135, The mobile
menu toggle button in CustomerLayout.tsx is unlabeled and lacks
aria-expanded/aria-controls; update the button that uses setMobileMenuOpen and
mobileMenuOpen to include an accessible label (use i18n via t, e.g.,
t('nav.openMenu') / t('nav.closeMenu')), add aria-expanded={mobileMenuOpen} and
aria-controls pointing to the drawer container id, and give the drawer container
a matching id; add the two new strings to the i18n nav namespace so the button
label reflects the Menu/X state for screen readers.
| useEffect(() => { | ||
| if (thumbnailsRef.current) { | ||
| const container = thumbnailsRef.current; | ||
| const activeThumb = container.children[selectedImage] as HTMLElement; | ||
| if (activeThumb) { | ||
| const scrollLeft = activeThumb.offsetLeft - container.offsetWidth / 2 + activeThumb.offsetWidth / 2; | ||
| container.scrollTo({ left: scrollLeft, behavior: 'smooth' }); | ||
| } | ||
| } | ||
| }, [selectedImage]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for Arabic/i18n/RTL configuration
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.json" \) | head -20Repository: NowwarAbuRashed/OrderSystem
Length of output: 860
🏁 Script executed:
# Look for i18n or locale configuration files
fd -e json -e ts -e tsx | grep -E "(i18n|locale|translation|arabic|rtl)" | head -20Repository: NowwarAbuRashed/OrderSystem
Length of output: 219
🏁 Script executed:
# Search for RTL-related code in the codebase
rg -i "rtl|arabic|dir=" --type ts --type tsx | head -30Repository: NowwarAbuRashed/OrderSystem
Length of output: 99
🏁 Script executed:
# Check the ProductDetailsPage component for context
cat -n frontend/src/modules/catalog/pages/ProductDetailsPage.tsx | head -60Repository: NowwarAbuRashed/OrderSystem
Length of output: 3158
🏁 Script executed:
cat -n frontend/src/app/i18n/direction.tsRepository: NowwarAbuRashed/OrderSystem
Length of output: 414
🏁 Script executed:
cat -n frontend/src/app/i18n/i18n-context.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 2173
🏁 Script executed:
# Look for where dir attribute is set in the app
rg "dir\s*=" frontend/src --type tsx --type ts -A 2 -B 2Repository: NowwarAbuRashed/OrderSystem
Length of output: 99
🏁 Script executed:
# Check the App.tsx root component
cat -n frontend/src/App.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 303
🏁 Script executed:
# Search for the thumbnail container rendering
rg "thumbnailsRef" frontend/src --type tsx -B 5 -A 5Repository: NowwarAbuRashed/OrderSystem
Length of output: 99
🏁 Script executed:
# Search for thumbnailsRef usage in the entire ProductDetailsPage
rg "thumbnailsRef" frontend/src --type ts -B 3 -A 8Repository: NowwarAbuRashed/OrderSystem
Length of output: 3805
🏁 Script executed:
# Get more of the ProductDetailsPage to see thumbnail rendering
cat -n frontend/src/modules/catalog/pages/ProductDetailsPage.tsx | tail -150Repository: NowwarAbuRashed/OrderSystem
Length of output: 9057
Thumbnail auto-scroll will malfunction in RTL mode.
The app applies dir="rtl" to the document element when Arabic is selected, and the thumbnail container inherits this context. The current code uses offsetLeft and container.scrollTo({ left }), which have inconsistent browser behavior in RTL—scrollLeft calculations differ between Chromium, Firefox, and Safari when the container has dir="rtl". This will cause the "center the active thumbnail" effect to scroll incorrectly or fail to center properly in Arabic.
Replace with activeThumb?.scrollIntoView({ inline: 'center', behavior: 'smooth', block: 'nearest' }), which handles RTL consistently:
Diff
useEffect(() => {
- if (thumbnailsRef.current) {
- const container = thumbnailsRef.current;
- const activeThumb = container.children[selectedImage] as HTMLElement;
- if (activeThumb) {
- const scrollLeft = activeThumb.offsetLeft - container.offsetWidth / 2 + activeThumb.offsetWidth / 2;
- container.scrollTo({ left: scrollLeft, behavior: 'smooth' });
- }
- }
+ const container = thumbnailsRef.current;
+ const activeThumb = container?.children[selectedImage] as HTMLElement | undefined;
+ activeThumb?.scrollIntoView({ inline: 'center', behavior: 'smooth', block: 'nearest' });
}, [selectedImage]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx` around lines 37 -
46, The thumbnail auto-scroll logic in the useEffect uses offsetLeft and
scrollTo which break in RTL; update the effect that references thumbnailsRef and
selectedImage so that instead of computing scrollLeft you call scrollIntoView on
the active thumbnail element (i.e., replace the activeThumb offsetLeft/scrollTo
block inside the useEffect with activeThumb?.scrollIntoView({ inline: 'center',
behavior: 'smooth', block: 'nearest' })) to ensure consistent centering in both
LTR and RTL.
| if (isNaN(id)) return <ErrorState message="Invalid product reference" />; | ||
| if (error || !product) return <ErrorState message={error ? getApiErrorMessage(error) : 'Product not found'} />; |
There was a problem hiding this comment.
Localize error messages via useI18n.
These user-facing error strings are hardcoded English while the rest of the page was just converted to t.*. They will not translate when the locale is Arabic. Consider adding keys (e.g. t.products.invalidProductId, t.products.notFound) to both en.ts and ar.ts and consuming them here.
- if (isNaN(id)) return <ErrorState message="Invalid product reference" />;
- if (error || !product) return <ErrorState message={error ? getApiErrorMessage(error) : 'Product not found'} />;
+ if (isNaN(id)) return <ErrorState message={t.products.invalidProductId} />;
+ if (error || !product) return <ErrorState message={error ? getApiErrorMessage(error) : t.products.notFound} />;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx` around lines 49 -
50, The hardcoded error strings in ProductDetailsPage (the isNaN(id) check and
the error/product null branch that returns <ErrorState ... /> using
getApiErrorMessage) must be localized: import/use the useI18n hook and replace
the literal messages with translation keys (e.g. t.products.invalidProductId and
t.products.notFound or similar), and add those keys to both en.ts and ar.ts;
keep getApiErrorMessage for API errors but wrap or fallback to
t.products.notFound when product is missing.
| <button | ||
| onClick={() => setSelectedImage((prev) => prev === 0 ? product.images!.length - 1 : prev - 1)} | ||
| className="absolute start-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 border border-slate-200 shadow-lg flex items-center justify-center text-slate-600 hover:bg-primary-50 hover:text-primary-600 hover:border-primary-200 transition-all backdrop-blur-sm" | ||
| aria-label="Previous image" | ||
| > | ||
| <ChevronLeft className="w-5 h-5" /> | ||
| </button> | ||
| <button | ||
| onClick={() => setSelectedImage((prev) => prev === product.images!.length - 1 ? 0 : prev + 1)} | ||
| className="absolute end-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 border border-slate-200 shadow-lg flex items-center justify-center text-slate-600 hover:bg-primary-50 hover:text-primary-600 hover:border-primary-200 transition-all backdrop-blur-sm" | ||
| aria-label="Next image" | ||
| > | ||
| <ChevronRight className="w-5 h-5" /> | ||
| </button> |
There was a problem hiding this comment.
Localize the image navigation aria-labels.
"Previous image" / "Next image" are hardcoded and will be read in English by Arabic screen readers. Add dedicated keys (e.g. t.products.previousImage, t.products.nextImage) and use them here for consistency with the rest of the i18n overhaul.
- aria-label="Previous image"
+ aria-label={t.products.previousImage}
...
- aria-label="Next image"
+ aria-label={t.products.nextImage}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| onClick={() => setSelectedImage((prev) => prev === 0 ? product.images!.length - 1 : prev - 1)} | |
| className="absolute start-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 border border-slate-200 shadow-lg flex items-center justify-center text-slate-600 hover:bg-primary-50 hover:text-primary-600 hover:border-primary-200 transition-all backdrop-blur-sm" | |
| aria-label="Previous image" | |
| > | |
| <ChevronLeft className="w-5 h-5" /> | |
| </button> | |
| <button | |
| onClick={() => setSelectedImage((prev) => prev === product.images!.length - 1 ? 0 : prev + 1)} | |
| className="absolute end-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 border border-slate-200 shadow-lg flex items-center justify-center text-slate-600 hover:bg-primary-50 hover:text-primary-600 hover:border-primary-200 transition-all backdrop-blur-sm" | |
| aria-label="Next image" | |
| > | |
| <ChevronRight className="w-5 h-5" /> | |
| </button> | |
| <button | |
| onClick={() => setSelectedImage((prev) => prev === 0 ? product.images!.length - 1 : prev - 1)} | |
| className="absolute start-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 border border-slate-200 shadow-lg flex items-center justify-center text-slate-600 hover:bg-primary-50 hover:text-primary-600 hover:border-primary-200 transition-all backdrop-blur-sm" | |
| aria-label={t.products.previousImage} | |
| > | |
| <ChevronLeft className="w-5 h-5" /> | |
| </button> | |
| <button | |
| onClick={() => setSelectedImage((prev) => prev === product.images!.length - 1 ? 0 : prev + 1)} | |
| className="absolute end-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/90 border border-slate-200 shadow-lg flex items-center justify-center text-slate-600 hover:bg-primary-50 hover:text-primary-600 hover:border-primary-200 transition-all backdrop-blur-sm" | |
| aria-label={t.products.nextImage} | |
| > | |
| <ChevronRight className="w-5 h-5" /> | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx` around lines 86 -
99, Replace the hardcoded English aria-labels on the image navigation buttons
with the i18n translation keys used elsewhere (e.g. t('products.previousImage')
and t('products.nextImage')); locate the two buttons that call setSelectedImage
(the previous/next handlers referencing product.images!.length) and change their
aria-label props to use the translation function (ensuring the component has
access to t from useTranslation or the existing i18n hook) so the labels become
localized keys consistent with the rest of the app.
| {/* Trust Strip */} | ||
| <div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> | ||
| {[ | ||
| { icon: Truck, label: 'Fast Delivery', desc: 'Same-day delivery available' }, | ||
| { icon: Shield, label: 'Quality Guarantee', desc: '100% fresh products' }, | ||
| { icon: RefreshCw, label: 'Easy Returns', desc: 'Hassle-free return policy' }, | ||
| ].map(({ icon: Icon, label, desc }) => ( | ||
| <div key={label} className="flex items-center gap-3 bg-white p-4 rounded-xl border border-slate-200/60 shadow-sm"> | ||
| <div className="p-2 bg-primary-50 rounded-lg text-primary-600"> | ||
| <Icon className="w-5 h-5" /> | ||
| </div> | ||
| <div> | ||
| <p className="text-sm font-semibold text-slate-900">{label}</p> | ||
| <p className="text-xs text-slate-500">{desc}</p> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
Trust Strip labels/descriptions are not localized.
The six strings 'Fast Delivery', 'Same-day delivery available', 'Quality Guarantee', '100% fresh products', 'Easy Returns', 'Hassle-free return policy' are hardcoded English. Given this PR's explicit i18n overhaul, Arabic users will see untranslated English here — this is the single largest gap in the page's localization coverage. Move these into t.products (both en.ts and ar.ts) and reference them from the map.
🌐 Proposed fix
{/* Trust Strip */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
- { icon: Truck, label: 'Fast Delivery', desc: 'Same-day delivery available' },
- { icon: Shield, label: 'Quality Guarantee', desc: '100% fresh products' },
- { icon: RefreshCw, label: 'Easy Returns', desc: 'Hassle-free return policy' },
+ { icon: Truck, label: t.products.trust.fastDelivery.label, desc: t.products.trust.fastDelivery.desc },
+ { icon: Shield, label: t.products.trust.qualityGuarantee.label, desc: t.products.trust.qualityGuarantee.desc },
+ { icon: RefreshCw, label: t.products.trust.easyReturns.label, desc: t.products.trust.easyReturns.desc },
].map(({ icon: Icon, label, desc }) => (
<div key={label} className="flex items-center gap-3 bg-white p-4 rounded-xl border border-slate-200/60 shadow-sm">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/catalog/pages/ProductDetailsPage.tsx` around lines 197 -
214, The trust strip labels/descriptions in ProductDetailsPage are hardcoded
English; move each string into the translations under t.products in both en.ts
and ar.ts (e.g. keys like products.fastDelivery.label,
products.fastDelivery.desc, products.qualityGuarantee.label,
products.qualityGuarantee.desc, products.easyReturns.label,
products.easyReturns.desc), update the map in the JSX (the array mapped over
that uses Truck/Shield/RefreshCw and Icon) to reference t.products.<key>.label
and t.products.<key>.desc via the i18n `t` function, and ensure the new keys are
added to both locale files so Arabic renders correctly.
| useEffect(() => { | ||
| const timer = setTimeout(() => { | ||
| if (localSearch !== search) { | ||
| setSearch(localSearch); | ||
| } | ||
| }, 400); | ||
| return () => clearTimeout(timer); | ||
| }, [localSearch]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check eslint-plugin-react-hooks version and any exhaustive-deps config
fd -e json -H package.json -x cat {} | jq -r '. | (.dependencies["eslint-plugin-react-hooks"] // .devDependencies["eslint-plugin-react-hooks"] // empty) | select(.!=null)'
fd -H -t f 'eslint*' -x cat {}Repository: NowwarAbuRashed/OrderSystem
Length of output: 694
🏁 Script executed:
cd frontend/src/modules/catalog/pages && head -160 ProductsListPage.tsx | cat -nRepository: NowwarAbuRashed/OrderSystem
Length of output: 7614
Debounce effect violates exhaustive-deps lint rule and will fail with eslint-plugin-react-hooks v7.0.1.
The effect at lines 29-36 reads search (line 31) and calls setSearch (line 32) without listing them as dependencies. Your project has eslint-plugin-react-hooks v7.0.1 installed with the recommended config (which includes exhaustive-deps), so this will trigger a lint error.
While the code works behaviorally because the first sync effect (lines 25-27) keeps localSearch in step with search, the closure is fragile. The linter is correct—if the two effects ever decouple, stale values would silently reappear.
A useRef to track the last written value makes the dependency set explicit and eliminates the dual-effect pattern:
♻️ Refactor — single debounce effect with a ref
- const [localSearch, setLocalSearch] = useState(search);
-
- useEffect(() => {
- setLocalSearch(search);
- }, [search]);
-
- useEffect(() => {
- const timer = setTimeout(() => {
- if (localSearch !== search) {
- setSearch(localSearch);
- }
- }, 400);
- return () => clearTimeout(timer);
- }, [localSearch]);
+ const [localSearch, setLocalSearch] = useState(search);
+ const lastWrittenRef = useRef(search);
+
+ // Sync from URL → input when URL changes externally.
+ useEffect(() => {
+ if (search !== lastWrittenRef.current) {
+ setLocalSearch(search);
+ lastWrittenRef.current = search;
+ }
+ }, [search]);
+
+ // Debounced write from input → URL.
+ useEffect(() => {
+ if (localSearch === search) return;
+ const timer = setTimeout(() => {
+ lastWrittenRef.current = localSearch;
+ setSearch(localSearch);
+ }, 400);
+ return () => clearTimeout(timer);
+ }, [localSearch, search]);(Add useRef to the React import on line 1.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/catalog/pages/ProductsListPage.tsx` around lines 29 -
36, The debounce effect in ProductsListPage reads `search` and calls `setSearch`
without listing them in deps; replace the dual-effect pattern with a single
debounce effect that uses a `useRef` (add to the React import) to track the
last-applied search value so you can safely include all real dependencies.
Specifically: add a ref like `lastAppliedSearchRef`, initialize it to `search`,
update it when you call `setSearch`, and change the existing `useEffect` (the
one that debounces `localSearch`) to depend on `localSearch`, `search`, and
`setSearch` (or simply `localSearch` if the ref eliminates the need to read
`search`), using the ref to avoid stale writes; remove the separate sync effect
that previously kept `localSearch` in step with `search`. Ensure
`lastAppliedSearchRef` is updated whenever `setSearch` runs.
| if (loadError) return <ErrorState message="Could not load product." />; | ||
| if (!product) return null; | ||
|
|
||
| const handleAdjust = (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| const amount = parseInt(qty, 10); | ||
| if (!amount || amount <= 0 || !reason.trim()) return; | ||
| if (!product || isNaN(id)) return <ErrorState message="Invalid product." />; |
There was a problem hiding this comment.
Hardcoded English strings left behind in the i18n overhaul.
Several user-facing strings remain non-translated even though the stated goal of this change is to move copy onto t:
- Line 56:
"Could not load product." - Line 57:
"Invalid product." - Line 148:
"Stock is below minimum threshold"(next to the translatedt.products.lowStock) - Line 173:
"Add Stock" - Line 180:
"Remove" - Line 214:
"No movement history for this product."
In RTL/Arabic these will render untranslated, which breaks the localization flow introduced elsewhere in this file.
🌐 Suggested direction (keys can be added to en.ts/ar.ts)
- if (loadError) return <ErrorState message="Could not load product." />;
- if (!product || isNaN(id)) return <ErrorState message="Invalid product." />;
+ if (loadError) return <ErrorState message={t.manager.loadProductError} />;
+ if (!product || isNaN(id)) return <ErrorState message={t.manager.invalidProduct} />;
...
- ⚠️ {t.products.lowStock} — Stock is below minimum threshold
+ ⚠️ {t.products.lowStock} — {t.products.lowStockDesc}
...
- <Plus className="w-4 h-4" /> Add Stock
+ <Plus className="w-4 h-4" /> {t.manager.addStock}
...
- <Minus className="w-4 h-4" /> Remove
+ <Minus className="w-4 h-4" /> {t.manager.removeStock}
...
- emptyMessage="No movement history for this product."
+ emptyMessage={t.manager.noMovementHistory}Also applies to: 148-148, 173-173, 180-180, 214-214
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx` around lines 56 -
57, Several user-facing strings in InventoryManagePage are still hardcoded;
replace them with i18n keys using the t function (e.g., t('products.loadError'),
t('products.invalid'), t('products.stockBelowMin'), t('products.addStock'),
t('products.remove'), t('products.noMovementHistory')) wherever loadError,
product/id validation, the low-stock label, Add Stock button, Remove button, and
the empty history message are used; add the corresponding keys to the locale
files (en.ts/ar.ts) and ensure the InventoryManagePage imports/uses the t
hook/prop already present in the file.
| const opts = { | ||
| onSuccess: () => { | ||
| setQty(''); | ||
| setReason(''); | ||
| setSuccessMessage(t.common.success); | ||
| reset(); | ||
| setTimeout(() => setSuccessMessage(null), 3000); | ||
| }, |
There was a problem hiding this comment.
setTimeout not cleared on unmount.
If the user navigates away within the 3-second window after a successful adjust, React will warn about a state update on an unmounted component (and the timer keeps a reference alive). Track the handle and clear it on unmount / before scheduling a new one.
🧹 Proposed fix
+import { useEffect, useMemo, useRef, useState } from 'react';
...
const [successMessage, setSuccessMessage] = useState<string | null>(null);
+ const successTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+ useEffect(() => () => {
+ if (successTimerRef.current) clearTimeout(successTimerRef.current);
+ }, []);
...
onSuccess: () => {
setSuccessMessage(t.common.success);
reset();
- setTimeout(() => setSuccessMessage(null), 3000);
+ if (successTimerRef.current) clearTimeout(successTimerRef.current);
+ successTimerRef.current = setTimeout(() => setSuccessMessage(null), 3000);
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const opts = { | |
| onSuccess: () => { | |
| setQty(''); | |
| setReason(''); | |
| setSuccessMessage(t.common.success); | |
| reset(); | |
| setTimeout(() => setSuccessMessage(null), 3000); | |
| }, | |
| const opts = { | |
| onSuccess: () => { | |
| setSuccessMessage(t.common.success); | |
| reset(); | |
| if (successTimerRef.current) clearTimeout(successTimerRef.current); | |
| successTimerRef.current = setTimeout(() => setSuccessMessage(null), 3000); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx` around lines 62 -
67, The onSuccess handler in the opts object uses setTimeout to clear
setSuccessMessage after 3s but never clears the timer on unmount or before
scheduling a new one; create a ref (e.g., successTimeoutRef) to store the timer
id, clear any existing timeout before setting a new one, and add a useEffect
cleanup that clears successTimeoutRef.current on component unmount so
setSuccessMessage is not called on an unmounted component; update references in
the onSuccess block that call setSuccessMessage/reset to use this timeout
management.
| <button | ||
| type="button" | ||
| onClick={() => setValue('action', 'add')} | ||
| className={`flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold border-2 transition-all ${actionValue === 'add' ? 'border-success-500 bg-success-50 text-success-700' : 'border-slate-200 text-slate-500 hover:border-slate-300'}`} | ||
| > | ||
| <Plus className="w-4 h-4" /> Add Stock | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => setValue('action', 'remove')} | ||
| className={`flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold border-2 transition-all ${actionValue === 'remove' ? 'border-danger-500 bg-danger-50 text-danger-700' : 'border-slate-200 text-slate-500 hover:border-slate-300'}`} | ||
| > | ||
| <Minus className="w-4 h-4" /> Remove | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "InventoryManagePage.tsx" -type fRepository: NowwarAbuRashed/OrderSystem
Length of output: 132
🏁 Script executed:
fd -i "inventorymanagepage" --type fRepository: NowwarAbuRashed/OrderSystem
Length of output: 130
🏁 Script executed:
git ls-files "frontend/src/modules/manager/pages/" | head -20Repository: NowwarAbuRashed/OrderSystem
Length of output: 454
🏁 Script executed:
git ls-files | head -30 | catRepository: NowwarAbuRashed/OrderSystem
Length of output: 2502
🏁 Script executed:
rg "superRefine" --type ts --type tsx -i | head -30Repository: NowwarAbuRashed/OrderSystem
Length of output: 99
🏁 Script executed:
fd "inventory" -i --type f | head -20Repository: NowwarAbuRashed/OrderSystem
Length of output: 1736
🏁 Script executed:
rg "setValue\(" --type ts --type tsx -B 2 -A 2 | head -50Repository: NowwarAbuRashed/OrderSystem
Length of output: 99
🏁 Script executed:
ls -la | head -20Repository: NowwarAbuRashed/OrderSystem
Length of output: 545
🌐 Web query:
react-hook-form setValue shouldValidate default behavior
💡 Result:
The default behavior of setValue in react-hook-form for the shouldValidate option is false. When calling setValue without specifying options, or with options where shouldValidate is not explicitly set to true, validation is not triggered for the field. To trigger validation, you must explicitly pass { shouldValidate: true } in the options object. This is confirmed by the official documentation, which shows examples like setValue("firstName", "Bill") without validation, and setValue("firstName", "Luo", { shouldValidate: true, shouldDirty: true }) to trigger it. GitHub issues also discuss needing to set shouldValidate: true explicitly to run validation, as setValue does not validate by default unlike user input.
Citations:
- 1: https://react-hook-form.com/docs/useform/setvalue
- 2: https://www.react-hook-form.com/api/useform/setvalue/
🌐 Web query:
react-hook-form clearing validation errors when switching form state
💡 Result:
To clear validation errors in React Hook Form when switching form state (such as tabs, modes, or conditional rendering), use the clearErrors function from useForm. This manually removes errors without affecting validation rules or formState.isValid. Key approaches: 1. Call clearErrors on state change: Invoke clearErrors (clears all errors) or clearErrors('fieldName') when your form state changes, like in a useEffect watching the state or on tab switch handlers. Example: const { clearErrors } = useForm; const [activeTab, setActiveTab] = useState('tab1'); useEffect( => { clearErrors; // Clear all errors when tab changes }, [activeTab, clearErrors]); 2. For tabbed/conditional forms: Use shouldUnregister: false in useForm to retain field state across unmounts/remounts (common in tabs). Errors may persist if fields had validation failures; pair with clearErrors to reset them. Example: const { control, clearErrors } = useForm({ shouldUnregister: false }); 3. Alternative: reset with options: Use reset(undefined, { keepErrors: false }) to reset form while clearing errors, or resetField('fieldName', { keepError: false }) for specific fields. From official docs: clearErrors is designed exactly for manually clearing errors in scenarios like this. Older versions auto-cleared on unmount, but current behavior requires explicit calls for control. This prevents stale errors from previous states blocking submission or UX.
Citations:
- 1: https://react-hook-form.com/docs/useform/clearerrors
- 2: https://react-hook-form.com/docs/useform/reset
- 3: https://react-hook-form.com/docs/useform/resetfield
- 4: Form wrapped around multiple Material UI tabs does not retain state react-hook-form/react-hook-form#3834
- 5: a field is cleared from dirtyFields in spite of shouldUnregister=false react-hook-form/react-hook-form#3437
- 6: https://react-hook-form.com/docs/useform/unregister
- 7: Cleanup errors when component is being destroyed react-hook-form/react-hook-form#199
- 8: https://react-hook-form.com/docs/useform/
🏁 Script executed:
wc -l frontend/src/modules/manager/pages/InventoryManagePage.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 134
🏁 Script executed:
sed -n '160,190p' frontend/src/modules/manager/pages/InventoryManagePage.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 1809
🏁 Script executed:
cat -n frontend/src/modules/manager/pages/InventoryManagePage.tsx | sed -n '160,190p'Repository: NowwarAbuRashed/OrderSystem
Length of output: 2026
🏁 Script executed:
head -200 frontend/src/modules/manager/pages/InventoryManagePage.tsx | tail -50Repository: NowwarAbuRashed/OrderSystem
Length of output: 2455
🏁 Script executed:
cat frontend/src/modules/manager/pages/InventoryManagePage.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 10261
🏁 Script executed:
rg "superRefine|adjustInventorySchema" -A 10 -B 2 frontend/src/modules/manager/Repository: NowwarAbuRashed/OrderSystem
Length of output: 1397
🏁 Script executed:
find frontend/src/modules/manager -type f \( -name "*.ts" -o -name "*.tsx" \) -exec rg -l "adjustInventory|superRefine" {} \;Repository: NowwarAbuRashed/OrderSystem
Length of output: 238
🏁 Script executed:
rg "action.*qty|qty.*action|max.*check" frontend/src/modules/manager -A 5 -B 5Repository: NowwarAbuRashed/OrderSystem
Length of output: 2365
🏁 Script executed:
rg "z\\.object|z\\.string|z\\.number" frontend/src/modules/manager -l | head -10Repository: NowwarAbuRashed/OrderSystem
Length of output: 239
🏁 Script executed:
find frontend/src/modules/manager -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs rg -l "z\."Repository: NowwarAbuRashed/OrderSystem
Length of output: 239
🏁 Script executed:
grep -r "superRefine" frontend/src/modules/manager/Repository: NowwarAbuRashed/OrderSystem
Length of output: 164
🏁 Script executed:
grep -r "const.*= z\." frontend/src/modules/manager/ | head -10Repository: NowwarAbuRashed/OrderSystem
Length of output: 53
🏁 Script executed:
ls -la frontend/src/modules/manager/Repository: NowwarAbuRashed/OrderSystem
Length of output: 340
🏁 Script executed:
find frontend/src/modules/manager -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "z\."Repository: NowwarAbuRashed/OrderSystem
Length of output: 239
🏁 Script executed:
grep -n "action" frontend/src/modules/manager/pages/InventoryManagePage.tsx | head -20Repository: NowwarAbuRashed/OrderSystem
Length of output: 1100
🏁 Script executed:
python3 << 'EOF'
with open('frontend/src/modules/manager/pages/InventoryManagePage.tsx', 'r') as f:
lines = f.readlines()
# Print lines 160-190
for i in range(159, min(190, len(lines))):
print(f"{i+1:4d}: {lines[i]}", end='')
EOFRepository: NowwarAbuRashed/OrderSystem
Length of output: 1995
🏁 Script executed:
python3 << 'EOF'
with open('frontend/src/modules/manager/pages/InventoryManagePage.tsx', 'r') as f:
content = f.read()
# Find the validation schema/form setup
if 'superRefine' in content:
print("=== Found superRefine ===")
idx = content.find('superRefine')
print(content[max(0, idx-500):idx+1000])
else:
print("No superRefine found")
# Find the buttons
if 'Add Stock' in content:
print("\n=== Found Add Stock button ===")
idx = content.find('Add Stock')
print(content[max(0, idx-1000):idx+500])
EOFRepository: NowwarAbuRashed/OrderSystem
Length of output: 3131
🏁 Script executed:
find frontend/src -type f \( -name "*.ts" -o -name "*.tsx" \) -exec grep -l "superRefine" {} \;Repository: NowwarAbuRashed/OrderSystem
Length of output: 284
🏁 Script executed:
python3 << 'EOF'
import os
# Search for any validation or schema files
for root, dirs, files in os.walk('frontend/src'):
for file in files:
if file.endswith(('.ts', '.tsx')):
filepath = os.path.join(root, file)
with open(filepath, 'r') as f:
try:
content = f.read()
if 'superRefine' in content:
print(f"Found in: {filepath}")
except:
pass
EOFRepository: NowwarAbuRashed/OrderSystem
Length of output: 324
🏁 Script executed:
cat frontend/src/modules/manager/pages/InventoryManagePage.tsx > /tmp/inventory.txt && head -n 200 /tmp/inventory.txt | tail -n 50Repository: NowwarAbuRashed/OrderSystem
Length of output: 2455
🏁 Script executed:
python3 << 'EOF'
with open('frontend/src/modules/manager/pages/InventoryManagePage.tsx') as f:
lines = f.readlines()
total = len(lines)
print(f"Total lines: {total}\n")
# Show around line 168-182
start = max(0, 167)
end = min(len(lines), 185)
for i in range(start, end):
print(f"{i+1:4d}: {lines[i]}", end='')
# Also search for Add Stock and Remove buttons
print("\n\n=== Searching for buttons ===\n")
for i, line in enumerate(lines):
if 'Add Stock' in line or 'Remove' in line and 'button' in lines[max(0, i-2):i+1]:
start_ctx = max(0, i-5)
end_ctx = min(len(lines), i+3)
for j in range(start_ctx, end_ctx):
print(f"{j+1:4d}: {lines[j]}", end='')
print()
EOFRepository: NowwarAbuRashed/OrderSystem
Length of output: 1853
🏁 Script executed:
grep -n "setValue.*action" frontend/src/modules/manager/pages/InventoryManagePage.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 202
🏁 Script executed:
rg "setValue" frontend/src/modules/manager/pages/InventoryManagePage.tsx -nRepository: NowwarAbuRashed/OrderSystem
Length of output: 328
🏁 Script executed:
rg "Add Stock" frontend/src/modules/manager/pages/InventoryManagePage.tsx -B 10 -A 2 -nRepository: NowwarAbuRashed/OrderSystem
Length of output: 859
🏁 Script executed:
awk '/Add Stock/{print NR": "$0}' frontend/src/modules/manager/pages/InventoryManagePage.tsxRepository: NowwarAbuRashed/OrderSystem
Length of output: 133
🏁 Script executed:
python3 << 'EOF'
with open('frontend/src/modules/manager/pages/InventoryManagePage.tsx') as f:
lines = f.readlines()
for i, line in enumerate(lines):
if 'Add Stock' in line:
print(f"Line {i+1}: Found 'Add Stock'")
# Print context
for j in range(max(0, i-15), min(len(lines), i+5)):
print(f"{j+1:4d}: {lines[j]}", end='')
break
EOFRepository: NowwarAbuRashed/OrderSystem
Length of output: 1389
Call setValue with shouldValidate: true to clear stale validation errors when switching actions.
The superRefine check attached to the qty field only applies when action='remove'. When a user exceeds the product quantity with action='remove', an error persists on the input. Clicking Add Stock switches action to 'add', but because setValue defaults to shouldValidate: false, the stale error remains visible on the quantity input even though the max-stock check no longer applies (the opposite occurs when switching from 'add' to 'remove').
♻️ Proposed fix
- onClick={() => setValue('action', 'add')}
+ onClick={() => setValue('action', 'add', { shouldValidate: true, shouldDirty: true })}
...
- onClick={() => setValue('action', 'remove')}
+ onClick={() => setValue('action', 'remove', { shouldValidate: true, shouldDirty: true })}Also consider adding aria-pressed={actionValue === 'add' ? true : false} to both buttons for screen-reader accessibility.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| type="button" | |
| onClick={() => setValue('action', 'add')} | |
| className={`flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold border-2 transition-all ${actionValue === 'add' ? 'border-success-500 bg-success-50 text-success-700' : 'border-slate-200 text-slate-500 hover:border-slate-300'}`} | |
| > | |
| <Plus className="w-4 h-4" /> Add Stock | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setValue('action', 'remove')} | |
| className={`flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold border-2 transition-all ${actionValue === 'remove' ? 'border-danger-500 bg-danger-50 text-danger-700' : 'border-slate-200 text-slate-500 hover:border-slate-300'}`} | |
| > | |
| <Minus className="w-4 h-4" /> Remove | |
| </button> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => setValue('action', 'add', { shouldValidate: true, shouldDirty: true })} | |
| className={`flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold border-2 transition-all ${actionValue === 'add' ? 'border-success-500 bg-success-50 text-success-700' : 'border-slate-200 text-slate-500 hover:border-slate-300'}`} | |
| > | |
| <Plus className="w-4 h-4" /> Add Stock | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setValue('action', 'remove', { shouldValidate: true, shouldDirty: true })} | |
| className={`flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-semibold border-2 transition-all ${actionValue === 'remove' ? 'border-danger-500 bg-danger-50 text-danger-700' : 'border-slate-200 text-slate-500 hover:border-slate-300'}`} | |
| > | |
| <Minus className="w-4 h-4" /> Remove | |
| </button> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/modules/manager/pages/InventoryManagePage.tsx` around lines 168
- 182, The stale validation error persists because setValue('action', ...) is
called without triggering validation; update the two button handlers that call
setValue('action', 'add') and setValue('action', 'remove') to pass
shouldValidate: true so the qty field's superRefine check is re-run when
switching actions (i.e., call setValue('action', 'add', { shouldValidate: true
}) and setValue('action', 'remove', { shouldValidate: true })); also add
aria-pressed={actionValue === 'add'} / aria-pressed={actionValue === 'remove'}
to the respective buttons for improved screen-reader accessibility.
it is sum 3 branchesFrontEnd_Module_2, feature/admin-operational, feature/admin-medium-impact
Summary by CodeRabbit
New Features
Enhancements
UI/UX Improvements