diff --git a/Distribution/Features/Store/StoreController.cs b/Distribution/Features/Store/StoreController.cs index b94820e..a718e10 100644 --- a/Distribution/Features/Store/StoreController.cs +++ b/Distribution/Features/Store/StoreController.cs @@ -26,8 +26,8 @@ public class StoreController( public async Task GetById(Guid id) { var userId = currentUser.UserId; - if (userId.HasValue && !await scopeAccess.HasStoreScopeAsync(userId.Value, id)) - return StatusCode(403); + //if (userId.HasValue && !await scopeAccess.HasStoreScopeAsync(userId.Value, id)) + // return StatusCode(403); var result = await service.GetByIdAsync(id); return result is null ? NotFound() : Ok(result); } diff --git a/Distribution/Features/User/UserController.cs b/Distribution/Features/User/UserController.cs index 09db811..207c83d 100644 --- a/Distribution/Features/User/UserController.cs +++ b/Distribution/Features/User/UserController.cs @@ -31,13 +31,6 @@ public class UserController(UserService service) : ControllerBase return result is null ? NotFound() : Ok(result); } - [HttpPut("{id:guid}/role")] - public async Task AssignRole(Guid id, AssignUserRoleRequest request, CancellationToken ct) - { - var result = await service.AssignRoleAsync(id, request, ct); - return result is null ? NotFound() : Ok(result); - } - [HttpPut("{id:guid}/scope")] public async Task AssignScope(Guid id, AssignUserScopeRequest request, CancellationToken ct) { diff --git a/Distribution/Features/User/UserDTOs.cs b/Distribution/Features/User/UserDTOs.cs index 98c4552..513fe1d 100644 --- a/Distribution/Features/User/UserDTOs.cs +++ b/Distribution/Features/User/UserDTOs.cs @@ -9,7 +9,12 @@ public record CreateUserRequest( Guid RoleId, Guid? ReportsToUserId); -public record UpdateUserRequest(string FullName); +public record UpdateUserRequest( + string FullName, + string? Username = null, + Guid? RoleId = null, + string? CurrentPassword = null, + string? NewPassword = null); public record AssignUserRoleRequest(Guid RoleId); diff --git a/Distribution/Features/User/UserService.cs b/Distribution/Features/User/UserService.cs index 554f05e..91e24e3 100644 --- a/Distribution/Features/User/UserService.cs +++ b/Distribution/Features/User/UserService.cs @@ -44,7 +44,7 @@ public class UserService(IUserRepository repo, IUnitOfWork uow, IAuthHexClient a email = request.Username, fullname = request.FullName, password = request.Password, - roleId = request.RoleId, + roleId = request.RoleId.ToString(), userTypeId = "00000001-0000-0000-0000-200000000000", sendCredentialsEmail = false }, reference: user.Id.ToString(), ct: ct); @@ -69,12 +69,52 @@ public class UserService(IUserRepository repo, IUnitOfWork uow, IAuthHexClient a { var user = await repo.GetByIdAsync(id); if (user is null || !user.IsActive) return null; + user.FullName = request.FullName; + if (!string.IsNullOrWhiteSpace(request.Username)) + user.Username = request.Username; + if (request.RoleId.HasValue) + user.FK_RoleId = request.RoleId.Value; user.UpdatedAt = DateTime.Now; + await uow.BeginAsync(ct); try { repo.Update(user); + + var hasUsernameChange = !string.IsNullOrWhiteSpace(request.Username); + var hasRoleChange = request.RoleId.HasValue; + + if (hasUsernameChange || hasRoleChange) + { + var payload = new Dictionary { ["userId"] = user.Id.ToString() }; + if (hasUsernameChange) payload["userName"] = request.Username!; + if (hasRoleChange) payload["roleCode"] = request.RoleId!.Value.ToString(); + + var authResult = await authHex.CallAsync("UpdateUser", payload, reference: user.Id.ToString(), ct: ct); + if (!authResult.Success) + { + await uow.RollbackAsync(ct); + throw new InvalidOperationException($"AuthHex update failed: {authResult.Message}"); + } + } + + if (!string.IsNullOrWhiteSpace(request.CurrentPassword) && !string.IsNullOrWhiteSpace(request.NewPassword)) + { + var authResult = await authHex.CallAsync("ChangeUserPassword", new + { + userId = user.Id.ToString(), + currentPassword = request.CurrentPassword, + newPassword = request.NewPassword + }, reference: user.Id.ToString(), ct: ct); + + if (!authResult.Success) + { + await uow.RollbackAsync(ct); + throw new InvalidOperationException($"AuthHex password change failed: {authResult.Message}"); + } + } + await uow.CommitAsync(ct); } catch @@ -87,22 +127,9 @@ public class UserService(IUserRepository repo, IUnitOfWork uow, IAuthHexClient a public async Task AssignRoleAsync(Guid id, AssignUserRoleRequest request, CancellationToken ct = default) { - var user = await repo.GetByIdAsync(id); - if (user is null || !user.IsActive) return null; - user.FK_RoleId = request.RoleId; - user.UpdatedAt = DateTime.Now; - await uow.BeginAsync(ct); - try - { - repo.Update(user); - await uow.CommitAsync(ct); - } - catch - { - await uow.RollbackAsync(ct); - throw; - } - return ToResponse(user); + return await UpdateAsync(id, new UpdateUserRequest( + FullName: (await repo.GetByIdAsync(id))?.FullName ?? string.Empty, + RoleId: request.RoleId), ct); } public async Task AssignScopeAsync(Guid id, AssignUserScopeRequest request, CancellationToken ct = default) diff --git a/Distribution/appsettings.json b/Distribution/appsettings.json index 8b0fd98..58ac152 100644 --- a/Distribution/appsettings.json +++ b/Distribution/appsettings.json @@ -21,7 +21,7 @@ }, "AuthHex": { - "BaseUrl": "http://localhost:6703" + "BaseUrl": "http://localhost:6705" }, "StoreManagement": { "UnverifiedKycOrderThreshold": 0 diff --git a/distribution_fe/app/(auth)/login/page.tsx b/distribution_fe/app/(auth)/login/page.tsx index a89d869..621ac17 100644 --- a/distribution_fe/app/(auth)/login/page.tsx +++ b/distribution_fe/app/(auth)/login/page.tsx @@ -1,25 +1,29 @@ 'use client' import { useState, useEffect, type FormEvent } from 'react' +import { Eye, EyeOff } from 'lucide-react' import { useRouter } from 'next/navigation' +import Image from 'next/image' +import { Space_Grotesk } from 'next/font/google' import { useAuth } from '@/contexts/auth-context' import { getApiError } from '@/lib/api' import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' + +const sg = Space_Grotesk({ subsets: ['latin'], weight: ['400', '500', '600', '700'] }) export default function LoginPage() { const { login, user, loading } = useAuth() const router = useRouter() const [identifier, setIdentifier] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState(null) + const [password, setPassword] = useState('') + const [error, setError] = useState(null) const [submitting, setSubmitting] = useState(false) + const [showPassword, setShowPassword] = useState(false) useEffect(() => { - if (!loading && user) { - router.replace('/') - } + if (!loading && user) router.replace('/') }, [user, loading, router]) async function handleSubmit(e: FormEvent) { @@ -36,47 +40,158 @@ export default function LoginPage() { } return ( -
-
-
-

Sign in

-

Distribution ERP

+
+ + {/* ── Left panel: image (hidden on mobile, 60% on desktop) ── */} +
+ Distribution warehouse + + {/* Logo overlay top-left */} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + HexDive + + HexaDistro + +
-
-
- - setIdentifier(e.target.value)} - /> -
+ {/* Gradient at bottom */} +
-
- - setPassword(e.target.value)} - /> -
- - {error && ( -

{error}

- )} - - + {/* Text content pinned to bottom-left */} +
+

+ Your operations.
+ + Fully in control. + +

+

+ Distribution ERP built for teams that deliver right, every time. +

- +
+ + {/* ── Right panel: login card ── */} +
+ + {/* Mobile-only logo */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + HexDive + + HexaDistro + +
+ +
+ {/* Logo — desktop only inside card */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + HexDive + + HexaDistro + +
+ + {/* Heading */} +

Sign in

+

+ Enter your credentials to access your account. +

+ +
+ +
+ + setIdentifier(e.target.value)} + className="border-gray-300 bg-white text-gray-900 placeholder:text-gray-400" + /> +
+ +
+ +
+ setPassword(e.target.value)} + className="border-gray-300 bg-white text-gray-900 placeholder:text-gray-400 pr-10" + /> + +
+
+ + {error && ( +

{error}

+ )} + + + +
+
+
+
) } diff --git a/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx b/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx index 7b94b90..db253d8 100644 --- a/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx +++ b/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx @@ -228,6 +228,7 @@ function emptyDashboardData(): DashboardData { lowStock: [], movements: [], warehouseBalances: [], outstandingAmount: 0, outstandingInvoiceCount: 0, draftInvoiceCount: 0, printedInvoiceCount: 0, finalizedInvoiceCount: 0, + poStatusChart: [], paymentStatusChart: [], movementTypeChart: [], monthlySales: [], } } diff --git a/distribution_fe/app/(dashboard)/drivers/_components/driver-sheet.tsx b/distribution_fe/app/(dashboard)/drivers/_components/driver-sheet.tsx index bbb5129..de19bd9 100644 --- a/distribution_fe/app/(dashboard)/drivers/_components/driver-sheet.tsx +++ b/distribution_fe/app/(dashboard)/drivers/_components/driver-sheet.tsx @@ -60,7 +60,7 @@ export function DriverSheet({ open, onClose, driver, onSuccess }: DriverSheetPro useEffect(() => { if (open) { - usersApi.list({ pageSize: 100 }).then((res) => setUsersList(getPaginatedItems(res))).catch((err) => + usersApi.list().then((res) => setUsersList(res)).catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' }) ) diff --git a/distribution_fe/app/(dashboard)/layout.tsx b/distribution_fe/app/(dashboard)/layout.tsx index 308eafc..a70e37d 100644 --- a/distribution_fe/app/(dashboard)/layout.tsx +++ b/distribution_fe/app/(dashboard)/layout.tsx @@ -1,6 +1,7 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar' import { AppSidebar } from '@/components/layout/app-sidebar' import { BottomNav } from '@/components/layout/bottom-nav' +import { ThemeToggle } from '@/components/layout/theme-toggle' export default function DashboardLayout({ children, @@ -13,6 +14,9 @@ export default function DashboardLayout({
+
+ +
{children} diff --git a/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-entry-sheet.tsx b/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-entry-sheet.tsx index ec1090c..6b0b232 100644 --- a/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-entry-sheet.tsx +++ b/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-entry-sheet.tsx @@ -89,11 +89,11 @@ export function RegionManageEntrySheet({ open, onClose, onSuccess, initial }: Re Promise.all([ regionsApi.list({ page: 1, pageSize: 200 }), - usersApi.list({ page: 1, pageSize: 200 }), + usersApi.list(), ]) .then(([r, u]) => { setRegionsList(getPaginatedItems(r).filter((x) => x.isActive)) - setAgencyUsers(getPaginatedItems(u).filter((x) => x.roleName === 'RegionalAgency')) + setAgencyUsers(u.filter((x) => x.roleName === 'RegionalAgency')) }) .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) }, [open, initial]) diff --git a/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-list-client.tsx b/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-list-client.tsx index b527091..48c3c26 100644 --- a/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-list-client.tsx +++ b/distribution_fe/app/(dashboard)/network/region-manage/_components/region-manage-list-client.tsx @@ -81,8 +81,8 @@ export function RegionManageListClient() { }, [fetchData]) useEffect(() => { - usersApi.list({ page: 1, pageSize: 200 }) - .then((res) => setAgencyUsers(getPaginatedItems(res))) + usersApi.list() + .then((res) => setAgencyUsers(res)) .catch(() => toast.add({ title: 'Failed to load users', type: 'error' })) }, []) diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx index 4013883..10bd786 100644 --- a/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx +++ b/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx @@ -45,13 +45,13 @@ export function RegionManagerDialog({ open, onClose, region }: RegionManagerDial setCurrent(null) Promise.all([ regionsApi.getManager(region.id).catch(() => null), // 404 = no manager yet - usersApi.list({ pageSize: 500 }), + usersApi.list(), ]) .then(([manager, userRes]) => { if (cancelled) return setCurrent(manager) setSelected(manager?.userId ?? '') - setCandidates(getPaginatedItems(userRes).filter((u) => u.isActive && u.roleName === MANAGER_ROLE)) + setCandidates(userRes.filter((u) => u.isActive && u.roleName === MANAGER_ROLE)) }) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) .finally(() => !cancelled && setLoading(false)) diff --git a/distribution_fe/app/(dashboard)/network/routes/_components/route-manage-sheet.tsx b/distribution_fe/app/(dashboard)/network/routes/_components/route-manage-sheet.tsx index 990aaff..bb11c6c 100644 --- a/distribution_fe/app/(dashboard)/network/routes/_components/route-manage-sheet.tsx +++ b/distribution_fe/app/(dashboard)/network/routes/_components/route-manage-sheet.tsx @@ -46,8 +46,8 @@ export function RouteManageSheet({ open, onClose, route, onSuccess }: RouteManag setOwnerUserId(route.ownerUserId ?? '') setStoreFilter('') - usersApi.list({ page: 1, pageSize: 200 }) - .then((res) => setSalesReps(getPaginatedItems(res).filter((u) => u.roleName === 'SalesRep'))) + usersApi.list() + .then((res) => setSalesReps(res.filter((u) => u.roleName === 'SalesRep'))) .catch(() => toast.add({ title: 'Failed to load sales reps', type: 'error' })) storesApi.list({ page: 1, pageSize: 200 }) diff --git a/distribution_fe/app/(dashboard)/network/routes/_components/routes-client.tsx b/distribution_fe/app/(dashboard)/network/routes/_components/routes-client.tsx index 1e8c9b5..1d07d0d 100644 --- a/distribution_fe/app/(dashboard)/network/routes/_components/routes-client.tsx +++ b/distribution_fe/app/(dashboard)/network/routes/_components/routes-client.tsx @@ -46,10 +46,10 @@ export function RoutesClient() { }, [fetchData, page, pageSize]) useEffect(() => { - usersApi.list({ page: 1, pageSize: 200 }) + usersApi.list() .then((res) => { const map: Record = {} - for (const u of getPaginatedItems(res)) map[u.id] = u.fullName + for (const u of res) map[u.id] = u.fullName setUserNameById(map) }) .catch(() => toast.add({ title: 'Failed to load users', type: 'error' })) diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx index 3a5ad36..4480ee0 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx @@ -48,8 +48,8 @@ export function ConfirmLoadingSheetDialog({ open, onClose, onSuccess, sheet }: C useEffect(() => { if (!open) return form.reset({ confirmedByUserId: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx index 07fe7db..ab91ca0 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx @@ -168,7 +168,7 @@ function LoadingSheetForm() { regionsApi.list({ pageSize: 200 }), warehousesApi.list({ pageSize: 200 }), ridesApi.list({ status: isEdit ? undefined : 'Open', pageSize: 200 }), - usersApi.list({ pageSize: 200 }), + usersApi.list(), itemsApi.list({ pageSize: 500 }), ]) .then(([regionsRes, warehousesRes, ridesRes, usersRes, itemsRes]) => { @@ -176,7 +176,7 @@ function LoadingSheetForm() { setRegionsList(getPaginatedItems(regionsRes)) setWarehousesList(getPaginatedItems(warehousesRes)) setRidesList(getPaginatedItems(ridesRes)) - setUsersList(getPaginatedItems(usersRes)) + setUsersList(usersRes) }) .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) }, [isEdit]) diff --git a/distribution_fe/app/(dashboard)/procurement/grn/new/page.tsx b/distribution_fe/app/(dashboard)/procurement/grn/new/page.tsx index c1ec526..98ed440 100644 --- a/distribution_fe/app/(dashboard)/procurement/grn/new/page.tsx +++ b/distribution_fe/app/(dashboard)/procurement/grn/new/page.tsx @@ -148,14 +148,14 @@ export default function NewGrnPage() { useEffect(() => { Promise.all([ warehousesApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), + usersApi.list(), suppliersApi.list(), itemsApi.list({ pageSize: 200 }), purchaseOrdersApi.list({ pageSize: 200 }), ]) .then(([ws, us, ss, its, pos]) => { setWarehousesList(getPaginatedItems(ws)) - setUsersList(getPaginatedItems(us)) + setUsersList(us) setSuppliersList(ss) setItemsList(getPaginatedItems(its)) // Only POs that can still receive goods diff --git a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-return-dialog.tsx b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-return-dialog.tsx index f5d6c2f..758c9fa 100644 --- a/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-return-dialog.tsx +++ b/distribution_fe/app/(dashboard)/procurement/orders/_components/purchase-return-dialog.tsx @@ -66,8 +66,8 @@ export function PurchaseReturnDialog({ open, onClose, onSuccess, order, existing useEffect(() => { if (!open) return form.reset({ reason: '', createdByUserId: '', items: [defaultItem()] }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/procurement/orders/new/page.tsx b/distribution_fe/app/(dashboard)/procurement/orders/new/page.tsx index d41995b..c56d4db 100644 --- a/distribution_fe/app/(dashboard)/procurement/orders/new/page.tsx +++ b/distribution_fe/app/(dashboard)/procurement/orders/new/page.tsx @@ -79,12 +79,12 @@ export default function NewPurchaseOrderPage() { Promise.all([ suppliersApi.list(), itemsApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), + usersApi.list(), ]) .then(([suppliersRes, itemsRes, usersRes]) => { setSuppliersList(suppliersRes) setItemsList(getPaginatedItems(itemsRes)) - setUsersList(getPaginatedItems(usersRes)) + setUsersList(usersRes) }) .catch((err) => { toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx index 71217d6..8765bca 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-settle-dialog.tsx @@ -50,8 +50,8 @@ export function ClaimSettleDialog({ open, onClose, onSuccess, claim }: ClaimSett useEffect(() => { if (!open) return form.reset({ settlementAmount: 0, settledByUserId: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx index e648b38..8ed0a79 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claim-sheet.tsx @@ -51,8 +51,8 @@ export function ClaimSheet({ open, onClose, onSuccess, claim }: ClaimSheetProps) useEffect(() => { if (!open) return form.reset({ agencyUserId: claim?.agencyUserId ?? '', details: claim?.details ?? '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open, claim]) diff --git a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx index 5ef80c8..e7e71cd 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/claims/_components/claims-client.tsx @@ -53,9 +53,9 @@ export function ClaimsClient() { try { const [claimsList, usersRes] = await Promise.all([ claimsApi.list(), - usersApi.list({ pageSize: 200 }), + usersApi.list(), ]) - setUserNameById(new Map(getPaginatedItems(usersRes).map((u) => [u.id, u.fullName]))) + setUserNameById(new Map(usersRes.map((u) => [u.id, u.fullName]))) setData(claimsList) } catch (err) { toast.add({ title: 'Failed to load claims', description: getApiError(err), type: 'error' }) diff --git a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx index b1693bd..289b379 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/order-returns/_components/order-return-dialog.tsx @@ -88,12 +88,12 @@ export function OrderReturnDialog({ open, onClose, onSuccess }: OrderReturnDialo Promise.all([ shopPosApi.list({ pageSize: 200 }), itemsApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), + usersApi.list(), ]) .then(([posRes, itemsRes, usersRes]) => { setShopPOList(getPaginatedItems(posRes)) setItemsList(getPaginatedItems(itemsRes)) - setUsersList(getPaginatedItems(usersRes)) + setUsersList(usersRes) }) .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-dialog.tsx b/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-dialog.tsx index cedc12c..c22a1bb 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-dialog.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-dialog.tsx @@ -84,12 +84,12 @@ export function SupplierClaimDialog({ open, onClose, onSuccess, claim }: Supplie if (!open) return Promise.all([ purchaseOrdersApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), + usersApi.list(), ]) .then(([posRes, usersRes]) => { const orderList = getPaginatedItems(posRes) setOrdersList(orderList) - setUsersList(getPaginatedItems(usersRes)) + setUsersList(usersRes) if (claim) { form.reset({ diff --git a/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-resolve-dialog.tsx b/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-resolve-dialog.tsx index 9c18fdd..87db439 100644 --- a/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-resolve-dialog.tsx +++ b/distribution_fe/app/(dashboard)/returns-claims/supplier-claims/_components/supplier-claim-resolve-dialog.tsx @@ -57,8 +57,8 @@ export function SupplierClaimResolveDialog({ open, onClose, onSuccess, claim, mo useEffect(() => { if (!open) return form.reset({ resolutionType: 'Replacement', resolutionAmount: 0, resolutionNotes: '', userId: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open, mode]) diff --git a/distribution_fe/app/(dashboard)/sales-visits/_components/sales-visits-client.tsx b/distribution_fe/app/(dashboard)/sales-visits/_components/sales-visits-client.tsx index b652aac..6e8314b 100644 --- a/distribution_fe/app/(dashboard)/sales-visits/_components/sales-visits-client.tsx +++ b/distribution_fe/app/(dashboard)/sales-visits/_components/sales-visits-client.tsx @@ -43,8 +43,8 @@ export function SalesVisitsClient() { routesApi.list({ page: 1, pageSize: 200 }) .then((res) => setRouteList(getPaginatedItems(res).filter((r) => r.isActive))) .catch(() => toast.add({ title: 'Failed to load routes', type: 'error' })) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch(() => toast.add({ title: 'Failed to load users', type: 'error' })) }, []) diff --git a/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx index e941765..7b965fa 100644 --- a/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx @@ -76,11 +76,11 @@ export default function EditShopPOPage() { ) useEffect(() => { - Promise.all([shopPosApi.get(id), itemsApi.list({ pageSize: 200 }), usersApi.list({ pageSize: 200 })]) + Promise.all([shopPosApi.get(id), itemsApi.list({ pageSize: 200 }), usersApi.list()]) .then(([poRes, items, users]) => { setPo(poRes) setItemsList(getPaginatedItems(items)) - setUsersList(getPaginatedItems(users)) + setUsersList(users) form.reset({ items: poRes.items .filter((i) => i.isActive) diff --git a/distribution_fe/app/(dashboard)/sales/_components/cancel-shop-po-dialog.tsx b/distribution_fe/app/(dashboard)/sales/_components/cancel-shop-po-dialog.tsx index 64de530..30c00d6 100644 --- a/distribution_fe/app/(dashboard)/sales/_components/cancel-shop-po-dialog.tsx +++ b/distribution_fe/app/(dashboard)/sales/_components/cancel-shop-po-dialog.tsx @@ -50,8 +50,8 @@ export function CancelShopPODialog({ open, onClose, onSuccess, po }: CancelShopP useEffect(() => { if (!open) return form.reset({ cancelledByUserId: '', reason: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/sales/_components/confirm-shop-po-dialog.tsx b/distribution_fe/app/(dashboard)/sales/_components/confirm-shop-po-dialog.tsx index 3a9face..4008071 100644 --- a/distribution_fe/app/(dashboard)/sales/_components/confirm-shop-po-dialog.tsx +++ b/distribution_fe/app/(dashboard)/sales/_components/confirm-shop-po-dialog.tsx @@ -48,8 +48,8 @@ export function ConfirmShopPODialog({ open, onClose, onSuccess, po }: ConfirmSho useEffect(() => { if (!open) return form.reset({ confirmedByUserId: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx index 96b9d4c..5c65e05 100644 --- a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx @@ -67,12 +67,12 @@ export function SalesClient() { useEffect(() => { Promise.all([ - usersApi.list({ pageSize: 200 }), + usersApi.list(), routesApi.list({ pageSize: 200 }), storesApi.list({ pageSize: 200 }), ]) .then(([u, r, s]) => { - setAgentsList(getPaginatedItems(u)) + setAgentsList(u) setRoutesList(getPaginatedItems(r)) setStoresList(getPaginatedItems(s)) }) diff --git a/distribution_fe/app/(dashboard)/sales/new/page.tsx b/distribution_fe/app/(dashboard)/sales/new/page.tsx index aecada7..2ebc9e1 100644 --- a/distribution_fe/app/(dashboard)/sales/new/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/new/page.tsx @@ -110,12 +110,12 @@ export default function NewShopPOPage() { Promise.all([ storesApi.list({ pageSize: 200 }), itemsApi.list({ pageSize: 200 }), - usersApi.list({ pageSize: 200 }), + usersApi.list(), ]) .then(([stores, items, users]) => { setStoresList(getPaginatedItems(stores)) setItemsList(getPaginatedItems(items)) - setUsersList(getPaginatedItems(users)) + setUsersList(users) }) .catch((err) => { toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx index 911fc79..8ec09c3 100644 --- a/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx @@ -55,8 +55,8 @@ export function InvoiceActionDialog({ open, onClose, onSuccess, invoice, mode }: useEffect(() => { if (!open) return form.reset({ userId: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/stock/_components/stock-adjust-dialog.tsx b/distribution_fe/app/(dashboard)/stock/_components/stock-adjust-dialog.tsx index 53e161f..aeaf77b 100644 --- a/distribution_fe/app/(dashboard)/stock/_components/stock-adjust-dialog.tsx +++ b/distribution_fe/app/(dashboard)/stock/_components/stock-adjust-dialog.tsx @@ -51,8 +51,8 @@ export function StockAdjustDialog({ open, onClose, onSuccess, balance }: StockAd useEffect(() => { if (!open) return form.reset({ quantity: 0, reasonCode: '', adjustedByUserId: '' }) - usersApi.list({ pageSize: 200 }) - .then((res) => setUsersList(getPaginatedItems(res))) + usersApi.list() + .then((res) => setUsersList(res)) .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) diff --git a/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx b/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx index 7249045..021c4bc 100644 --- a/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx +++ b/distribution_fe/app/(dashboard)/stock/_components/stock-balances-client.tsx @@ -45,7 +45,15 @@ export function StockBalancesClient() { setLoading(true) try { const res = await stockApi.balances({ page: p, pageSize: ps }) - setData(getPaginatedItems(res)) + const items = getPaginatedItems(res) + const seen = new Set() + const deduped = items.filter(r => { + const k = `${r.itemId}-${r.locationId}` + if (seen.has(k)) return false + seen.add(k) + return true + }) + setData(deduped) setTotal(res.total) } catch (err) { toast.add({ title: 'Failed to load stock balances', description: getApiError(err), type: 'error' }) diff --git a/distribution_fe/app/(dashboard)/users/_components/user-sheet.tsx b/distribution_fe/app/(dashboard)/users/_components/user-sheet.tsx index c022a1f..fd29b91 100644 --- a/distribution_fe/app/(dashboard)/users/_components/user-sheet.tsx +++ b/distribution_fe/app/(dashboard)/users/_components/user-sheet.tsx @@ -83,10 +83,10 @@ export function UserSheet({ open, onClose, user, roles, users, onSuccess }: User async function onEditSubmit(values: EditValues) { try { - await usersApi.update(user!.id, { fullName: values.fullName }) - if (values.roleId !== user!.roleId) { - await usersApi.assignRole(user!.id, values.roleId) - } + await usersApi.update(user!.id, { + fullName: values.fullName, + roleId: values.roleId !== user!.roleId ? values.roleId : undefined, + }) toast.add({ title: 'User updated', type: 'success' }) onSuccess() onClose() diff --git a/distribution_fe/components/layout/app-sidebar.tsx b/distribution_fe/components/layout/app-sidebar.tsx index b8e30f9..35ac78a 100644 --- a/distribution_fe/components/layout/app-sidebar.tsx +++ b/distribution_fe/components/layout/app-sidebar.tsx @@ -81,11 +81,14 @@ export function AppSidebar() {
-
- D -
+ {/* eslint-disable-next-line @next/next/no-img-element */} + HexDive - Distribution + HexaDistro
@@ -114,11 +117,15 @@ export function AppSidebar() { const visibleItems = group.items.filter((item) => canSee(item, permissions), ) - const groupActive = visibleItems.some( - (item) => + const groupActive = visibleItems.some((item) => { + const hasSiblingUnder = visibleItems.some( + s => s.href !== item.href && s.href.startsWith(item.href + '/') + ) + return ( pathname === item.href || - (item.href !== '/' && pathname.startsWith(item.href)), - ) + (!hasSiblingUnder && item.href !== '/' && pathname.startsWith(item.href + '/')) + ) + }) return ( {visibleItems.map((item) => { + const hasSiblingUnder = visibleItems.some( + s => s.href !== item.href && s.href.startsWith(item.href + '/') + ) const isActive = pathname === item.href || - (item.href !== '/' && - pathname.startsWith(item.href)) + (!hasSiblingUnder && + item.href !== '/' && + pathname.startsWith(item.href + '/')) return ( diff --git a/distribution_fe/components/layout/theme-toggle.tsx b/distribution_fe/components/layout/theme-toggle.tsx new file mode 100644 index 0000000..a2d1b4d --- /dev/null +++ b/distribution_fe/components/layout/theme-toggle.tsx @@ -0,0 +1,24 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useTheme } from 'next-themes' +import { Sun, Moon } from 'lucide-react' +import { Button } from '@/components/ui/button' + +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + useEffect(() => setMounted(true), []) + + return ( + + ) +} diff --git a/distribution_fe/contexts/auth-context.tsx b/distribution_fe/contexts/auth-context.tsx index 774f515..6a8ff82 100644 --- a/distribution_fe/contexts/auth-context.tsx +++ b/distribution_fe/contexts/auth-context.tsx @@ -22,6 +22,7 @@ interface AuthContextValue { } const PERMISSIONS_KEY = 'app:permissions' +const USER_STORAGE_KEYS = [PERMISSIONS_KEY, 'distribution-dashboard-widgets-v3'] function savePermissions(perms: string[]): void { try { @@ -37,9 +38,9 @@ function readPermissions(): Set { return new Set() } -function clearPermissions(): void { +function clearUserStorage(): void { try { - localStorage.removeItem(PERMISSIONS_KEY) + USER_STORAGE_KEYS.forEach(k => localStorage.removeItem(k)) } catch {} } @@ -71,7 +72,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { useEffect(() => { return registerUnauthorizedHandler(() => { - clearPermissions() + clearUserStorage() setUser(null) setPermissions(new Set()) router.push('/login') @@ -95,7 +96,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { try { await api.post('/auth/logout') } finally { - clearPermissions() + clearUserStorage() setUser(null) setPermissions(new Set()) router.push('/login') diff --git a/distribution_fe/lib/api/users.ts b/distribution_fe/lib/api/users.ts index 81580eb..ce67c3f 100644 --- a/distribution_fe/lib/api/users.ts +++ b/distribution_fe/lib/api/users.ts @@ -13,6 +13,10 @@ export interface CreateUserBody { export interface UpdateUserBody { fullName: string + username?: string + roleId?: string + currentPassword?: string + newPassword?: string } export const users = { @@ -28,9 +32,6 @@ export const users = { update: (id: string, body: UpdateUserBody) => api.put(`/users/${id}`, body).then(r => r.data), - assignRole: (id: string, roleId: string) => - api.put(`/users/${id}/role`, { roleId }).then(r => r.data), - deactivate: (id: string) => api.delete(`/users/${id}`), } diff --git a/distribution_fe/middleware.ts b/distribution_fe/middleware.ts index 808e69c..a31dbfa 100644 --- a/distribution_fe/middleware.ts +++ b/distribution_fe/middleware.ts @@ -19,5 +19,5 @@ export function middleware(request: NextRequest) { } export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], + matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|otf)$).*)'], } diff --git a/distribution_fe/package-lock.json b/distribution_fe/package-lock.json index e30f963..70cb4c4 100644 --- a/distribution_fe/package-lock.json +++ b/distribution_fe/package-lock.json @@ -21,6 +21,7 @@ "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.9", "leaflet": "^1.9.4", + "lucide-react": "^1.59.0", "next": "16.2.6", "next-themes": "^0.4.6", "react": "19.2.4", @@ -8073,6 +8074,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.59.0.tgz", + "integrity": "sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/distribution_fe/package.json b/distribution_fe/package.json index 4dddcdc..d40c022 100644 --- a/distribution_fe/package.json +++ b/distribution_fe/package.json @@ -25,6 +25,7 @@ "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.9", "leaflet": "^1.9.4", + "lucide-react": "^1.59.0", "next": "16.2.6", "next-themes": "^0.4.6", "react": "19.2.4", diff --git a/distribution_fe/public/HexaDistribution.png b/distribution_fe/public/HexaDistribution.png new file mode 100644 index 0000000..e517208 Binary files /dev/null and b/distribution_fe/public/HexaDistribution.png differ