From b8133ffe9fb883349371b48b4e2afbeec3642933 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 8 May 2026 14:20:28 +0000 Subject: [PATCH] 3 --- .../src/components/Accounts/CardAccounts.tsx | 18 +- .../Accounts/configureAccountsCols.tsx | 18 +- frontend/src/components/AsideMenuLayer.tsx | 20 +- frontend/src/components/Orders/CardOrders.tsx | 24 +- .../src/components/Orders/TableOrders.tsx | 14 +- .../components/Orders/configureOrdersCols.tsx | 24 +- .../src/components/Products/CardProducts.tsx | 430 ++--- .../Products/configureProductsCols.tsx | 28 +- .../Sample_requests/TableSample_requests.tsx | 18 +- .../configureSample_requestsCols.tsx | 18 +- frontend/src/helpers/userPermissions.ts | 7 +- frontend/src/layouts/Authenticated.tsx | 2 +- frontend/src/menuAside.ts | 59 +- frontend/src/pages/accounts/accounts-list.tsx | 23 +- .../src/pages/accounts/accounts-table.tsx | 25 +- frontend/src/pages/accounts/accounts-view.tsx | 1689 ++++------------- frontend/src/pages/buyer-login.tsx | 289 +-- frontend/src/pages/buyer-portal.tsx | 117 +- frontend/src/pages/dashboard.tsx | 1029 ++++++++-- frontend/src/pages/index.tsx | 60 +- frontend/src/pages/login.tsx | 226 ++- frontend/src/pages/orders/orders-list.tsx | 23 +- frontend/src/pages/orders/orders-table.tsx | 23 +- frontend/src/pages/orders/orders-view.tsx | 1233 ++++-------- frontend/src/pages/products/products-list.tsx | 107 +- .../src/pages/products/products-table.tsx | 28 +- .../sample_requests/sample_requests-list.tsx | 603 ++++-- .../sample_requests/sample_requests-table.tsx | 24 +- .../sample_requests/sample_requests-view.tsx | 1057 +++++------ 29 files changed, 3217 insertions(+), 4019 deletions(-) diff --git a/frontend/src/components/Accounts/CardAccounts.tsx b/frontend/src/components/Accounts/CardAccounts.tsx index 1ebfb75..66f274b 100644 --- a/frontend/src/components/Accounts/CardAccounts.tsx +++ b/frontend/src/components/Accounts/CardAccounts.tsx @@ -78,7 +78,7 @@ const CardAccounts = ({
-
Accountname
+
Account name
{ item.account_name } @@ -90,7 +90,7 @@ const CardAccounts = ({
-
Accounttype
+
Account type
{ item.account_type } @@ -102,7 +102,7 @@ const CardAccounts = ({
-
Accountnumber
+
Account number
{ item.account_number } @@ -114,7 +114,7 @@ const CardAccounts = ({
-
Taxexemptnumber
+
Tax exempt number
{ item.tax_exempt_number } @@ -126,7 +126,7 @@ const CardAccounts = ({
-
Taxexempt
+
Tax exempt
{ dataFormatter.booleanFormatter(item.is_tax_exempt) } @@ -138,7 +138,7 @@ const CardAccounts = ({
-
Creditstatus
+
Credit status
{ item.credit_status } @@ -150,7 +150,7 @@ const CardAccounts = ({
-
Creditlimit
+
Credit limit
{ item.credit_limit } @@ -162,7 +162,7 @@ const CardAccounts = ({
-
Defaultpricelist
+
Default price list
{ dataFormatter.price_listsOneListFormatter(item.default_price_list) } @@ -174,7 +174,7 @@ const CardAccounts = ({
-
Assignedsalesrep
+
Assigned sales rep
{ dataFormatter.usersOneListFormatter(item.assigned_sales_rep) } diff --git a/frontend/src/components/Accounts/configureAccountsCols.tsx b/frontend/src/components/Accounts/configureAccountsCols.tsx index 4ecb509..7c40caa 100644 --- a/frontend/src/components/Accounts/configureAccountsCols.tsx +++ b/frontend/src/components/Accounts/configureAccountsCols.tsx @@ -43,7 +43,7 @@ export const loadColumns = async ( { field: 'account_name', - headerName: 'Accountname', + headerName: 'Account name', flex: 1, minWidth: 120, filterable: false, @@ -58,7 +58,7 @@ export const loadColumns = async ( { field: 'account_type', - headerName: 'Accounttype', + headerName: 'Account type', flex: 1, minWidth: 120, filterable: false, @@ -73,7 +73,7 @@ export const loadColumns = async ( { field: 'account_number', - headerName: 'Accountnumber', + headerName: 'Account number', flex: 1, minWidth: 120, filterable: false, @@ -88,7 +88,7 @@ export const loadColumns = async ( { field: 'tax_exempt_number', - headerName: 'Taxexemptnumber', + headerName: 'Tax exempt number', flex: 1, minWidth: 120, filterable: false, @@ -103,7 +103,7 @@ export const loadColumns = async ( { field: 'is_tax_exempt', - headerName: 'Taxexempt', + headerName: 'Tax exempt', flex: 1, minWidth: 120, filterable: false, @@ -119,7 +119,7 @@ export const loadColumns = async ( { field: 'credit_status', - headerName: 'Creditstatus', + headerName: 'Credit status', flex: 1, minWidth: 120, filterable: false, @@ -134,7 +134,7 @@ export const loadColumns = async ( { field: 'credit_limit', - headerName: 'Creditlimit', + headerName: 'Credit limit', flex: 1, minWidth: 120, filterable: false, @@ -150,7 +150,7 @@ export const loadColumns = async ( { field: 'default_price_list', - headerName: 'Defaultpricelist', + headerName: 'Default price list', flex: 1, minWidth: 120, filterable: false, @@ -172,7 +172,7 @@ export const loadColumns = async ( { field: 'assigned_sales_rep', - headerName: 'Assignedsalesrep', + headerName: 'Assigned sales rep', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index 65fa6b2..19d87f9 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -4,6 +4,7 @@ import BaseIcon from './BaseIcon'; import AsideMenuList from './AsideMenuList'; import { MenuAsideItem } from '../interfaces'; import { useAppSelector } from '../stores/hooks'; +import { hasPermission } from '../helpers/userPermissions'; type Props = { menu: MenuAsideItem[]; @@ -21,6 +22,23 @@ export default function AsideMenuLayer({ (state) => state.style.asideScrollbarsStyle, ); const darkMode = useAppSelector((state) => state.style.darkMode); + const { currentUser } = useAppSelector((state) => state.auth); + const canUseBuyerPortal = hasPermission(currentUser, 'READ_BUYER_PORTAL'); + const canReadBuyerTeamQueue = hasPermission( + currentUser, + 'READ_BUYER_TEAM_QUEUE', + ); + const hasSupplierAccess = hasPermission(currentUser, [ + 'READ_ACCOUNTS', + 'READ_PRODUCTS', + 'READ_ORDERS', + 'READ_INVENTORY_ITEMS', + ]); + const workspaceLabel = canUseBuyerPortal && !hasSupplierAccess + ? canReadBuyerTeamQueue + ? 'Buyer admin queue' + : 'Buyer ordering workspace' + : 'Supplier operations'; const handleAsideLgCloseClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -70,7 +88,7 @@ export default function AsideMenuLayer({ Workspace

- Contract catalog + orders + {workspaceLabel}

diff --git a/frontend/src/components/Orders/CardOrders.tsx b/frontend/src/components/Orders/CardOrders.tsx index 64c7dbd..b83c5b6 100644 --- a/frontend/src/components/Orders/CardOrders.tsx +++ b/frontend/src/components/Orders/CardOrders.tsx @@ -78,7 +78,7 @@ const CardOrders = ({
-
Ordernumber
+
Order number
{ item.order_number } @@ -126,7 +126,7 @@ const CardOrders = ({
-
POnumber
+
PO number
{ item.po_number } @@ -138,7 +138,7 @@ const CardOrders = ({
-
Orderedat
+
Ordered at
{ dataFormatter.dateTimeFormatter(item.ordered_at) } @@ -150,7 +150,7 @@ const CardOrders = ({
-
Requesteddeliverydate
+
Requested delivery date
{ dataFormatter.dateTimeFormatter(item.requested_delivery_date) } @@ -162,7 +162,7 @@ const CardOrders = ({
-
Promiseddeliverydate
+
Promised delivery date
{ dataFormatter.dateTimeFormatter(item.promised_delivery_date) } @@ -174,7 +174,7 @@ const CardOrders = ({
-
Orderstatus
+
Order status
{ item.order_status } @@ -186,7 +186,7 @@ const CardOrders = ({
-
Paymentterms
+
Payment terms
{ item.payment_terms } @@ -210,7 +210,7 @@ const CardOrders = ({
-
Taxtotal
+
Tax total
{ item.tax_total } @@ -222,7 +222,7 @@ const CardOrders = ({
-
Shippingtotal
+
Shipping total
{ item.shipping_total } @@ -234,7 +234,7 @@ const CardOrders = ({
-
Ordertotal
+
Order total
{ item.order_total } @@ -246,7 +246,7 @@ const CardOrders = ({
-
Buyernotes
+
Buyer notes
{ item.buyer_notes } @@ -258,7 +258,7 @@ const CardOrders = ({
-
Internalnotes
+
Internal notes
{ item.internal_notes } diff --git a/frontend/src/components/Orders/TableOrders.tsx b/frontend/src/components/Orders/TableOrders.tsx index 3e06db7..72f3586 100644 --- a/frontend/src/components/Orders/TableOrders.tsx +++ b/frontend/src/components/Orders/TableOrders.tsx @@ -98,19 +98,19 @@ const TableSampleOrders = ({ filterItems, setFilterItems, filters, showGrid }) = setKanbanColumns([ - { id: "draft", label: "draft" }, + { id: "draft", label: "Draft" }, - { id: "submitted", label: "submitted" }, + { id: "submitted", label: "Submitted" }, - { id: "approved", label: "approved" }, + { id: "approved", label: "Approved" }, - { id: "picking", label: "picking" }, + { id: "picking", label: "Picking" }, - { id: "shipped", label: "shipped" }, + { id: "shipped", label: "Shipped" }, - { id: "delivered", label: "delivered" }, + { id: "delivered", label: "Delivered" }, - { id: "cancelled", label: "cancelled" }, + { id: "cancelled", label: "Cancelled" }, ]); diff --git a/frontend/src/components/Orders/configureOrdersCols.tsx b/frontend/src/components/Orders/configureOrdersCols.tsx index 8cc2122..f54d5d7 100644 --- a/frontend/src/components/Orders/configureOrdersCols.tsx +++ b/frontend/src/components/Orders/configureOrdersCols.tsx @@ -43,7 +43,7 @@ export const loadColumns = async ( { field: 'order_number', - headerName: 'Ordernumber', + headerName: 'Order number', flex: 1, minWidth: 120, filterable: false, @@ -124,7 +124,7 @@ export const loadColumns = async ( { field: 'po_number', - headerName: 'POnumber', + headerName: 'PO number', flex: 1, minWidth: 120, filterable: false, @@ -139,7 +139,7 @@ export const loadColumns = async ( { field: 'ordered_at', - headerName: 'Orderedat', + headerName: 'Ordered at', flex: 1, minWidth: 120, filterable: false, @@ -157,7 +157,7 @@ export const loadColumns = async ( { field: 'requested_delivery_date', - headerName: 'Requesteddeliverydate', + headerName: 'Requested delivery date', flex: 1, minWidth: 120, filterable: false, @@ -175,7 +175,7 @@ export const loadColumns = async ( { field: 'promised_delivery_date', - headerName: 'Promiseddeliverydate', + headerName: 'Promised delivery date', flex: 1, minWidth: 120, filterable: false, @@ -193,7 +193,7 @@ export const loadColumns = async ( { field: 'order_status', - headerName: 'Orderstatus', + headerName: 'Order status', flex: 1, minWidth: 120, filterable: false, @@ -208,7 +208,7 @@ export const loadColumns = async ( { field: 'payment_terms', - headerName: 'Paymentterms', + headerName: 'Payment terms', flex: 1, minWidth: 120, filterable: false, @@ -239,7 +239,7 @@ export const loadColumns = async ( { field: 'tax_total', - headerName: 'Taxtotal', + headerName: 'Tax total', flex: 1, minWidth: 120, filterable: false, @@ -255,7 +255,7 @@ export const loadColumns = async ( { field: 'shipping_total', - headerName: 'Shippingtotal', + headerName: 'Shipping total', flex: 1, minWidth: 120, filterable: false, @@ -271,7 +271,7 @@ export const loadColumns = async ( { field: 'order_total', - headerName: 'Ordertotal', + headerName: 'Order total', flex: 1, minWidth: 120, filterable: false, @@ -287,7 +287,7 @@ export const loadColumns = async ( { field: 'buyer_notes', - headerName: 'Buyernotes', + headerName: 'Buyer notes', flex: 1, minWidth: 120, filterable: false, @@ -302,7 +302,7 @@ export const loadColumns = async ( { field: 'internal_notes', - headerName: 'Internalnotes', + headerName: 'Internal notes', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/components/Products/CardProducts.tsx b/frontend/src/components/Products/CardProducts.tsx index 9021335..1143c01 100644 --- a/frontend/src/components/Products/CardProducts.tsx +++ b/frontend/src/components/Products/CardProducts.tsx @@ -4,11 +4,11 @@ import ListActionsPopover from '../ListActionsPopover'; import { useAppSelector } from '../../stores/hooks'; import dataFormatter from '../../helpers/dataFormatter'; import { Pagination } from '../Pagination'; -import {saveFile} from "../../helpers/fileSaver"; +import { saveFile } from '../../helpers/fileSaver'; import LoadingSpinner from "../LoadingSpinner"; import Link from 'next/link'; -import {hasPermission} from "../../helpers/userPermissions"; +import { hasPermission } from "../../helpers/userPermissions"; type Props = { @@ -20,6 +20,51 @@ type Props = { onPageChange: (page: number) => void; }; +const readableValue = (value) => { + if (!value) { + return 'Not set'; + } + + return String(value).replace(/_/g, ' '); +}; + +const temperatureClasses = (value) => { + if (value === 'refrigerated') { + return 'border-sky-200 bg-sky-50 text-sky-800 dark:border-sky-800 dark:bg-sky-950 dark:text-sky-100'; + } + + if (value === 'frozen') { + return 'border-cyan-200 bg-cyan-50 text-cyan-800 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-100'; + } + + return 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100'; +}; + +const statusClasses = (value) => { + if (value === 'active') { + return 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100'; + } + + if (value === 'seasonal') { + return 'border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100'; + } + + if (value === 'out_of_stock') { + return 'border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-100'; + } + + return 'border-slate-200 bg-slate-50 text-slate-700 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-200'; +}; + +const fallbackProductImages = [ + 'https://images.unsplash.com/photo-1452195100486-9cc805987862?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1501443762994-82bd5dace89a?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1544025162-d76694265947?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1486297678162-eb2a19b0a32d?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?auto=format&fit=crop&w=900&q=80', + 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=900&q=80', +]; + const CardProducts = ({ products, loading, @@ -28,312 +73,161 @@ const CardProducts = ({ numPages, onPageChange, }: Props) => { - const asideScrollbarsStyle = useAppSelector( - (state) => state.style.asideScrollbarsStyle, - ); - const bgColor = useAppSelector((state) => state.style.cardsColor); - const darkMode = useAppSelector((state) => state.style.darkMode); - const corners = useAppSelector((state) => state.style.corners); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const currentUser = useAppSelector((state) => state.auth.currentUser); const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PRODUCTS') return ( -
+
{loading && }
    - {!loading && products.map((item, index) => ( + {!loading && products.map((item, index) => { + const category = dataFormatter.product_categoriesOneListFormatter(item.category) || 'Uncategorized'; + const specSheetLinks = dataFormatter.filesFormatter(item.spec_sheet); + const sampleEligible = item.is_sample_eligible === true || item.is_sample_eligible === 'true'; + const hasProductImage = Array.isArray(item.images) ? item.images.length > 0 : Boolean(item.images); + const fallbackImage = fallbackProductImages[index % fallbackProductImages.length]; + + return (
  • - -
    - - +
    + + {hasProductImage ? ( -

    {item.product_name}

    - - - -
    + ) : ( +
    + )} + +
    + {item.sku} +
    +
    -
    - - -
    -
    SKU
    -
    -
    - { item.sku } -
    -
    -
    - - - -
    -
    Productname
    -
    -
    - { item.product_name } -
    -
    -
    - +
    +
    + + {readableValue(item.product_status)} + + + {readableValue(item.temperature_zone)} + + {sampleEligible && ( + + Sample-ready + + )} +
    - - -
    -
    Brand
    -
    -
    - { item.brand } -
    -
    -
    - +
    + {category} +
    + +

    + {item.product_name} +

    + +
    + {item.brand} +
    +

    + {item.short_description || item.long_description || 'No product description yet.'} +

    - - -
    -
    Category
    -
    -
    - { dataFormatter.product_categoriesOneListFormatter(item.category) } -
    -
    +
    +
    +
    Pack
    +
    {item.pack_size || 'Not set'}
    - - - - -
    -
    Shortdescription
    -
    -
    - { item.short_description } -
    -
    +
    +
    MOQ
    +
    {item.moq_cases || 0} cases
    - - - - -
    -
    Longdescription
    -
    -
    - { item.long_description } -
    -
    +
    +
    Units
    +
    {item.units_per_case || 0} / case
    - - - - -
    -
    Images
    -
    -
    - -
    -
    +
    +
    Case wt.
    +
    {item.case_weight_lbs || 0} lbs
    - +
    - - -
    -
    Specsheet
    -
    -
    - {dataFormatter.filesFormatter(item.spec_sheet).map(link => ( - - ))} -
    -
    +
    +
    +
    Allergens
    +
    {item.allergens || 'None listed'}
    - - - - -
    -
    Packsize
    -
    -
    - { item.pack_size } -
    -
    +
    +
    Certifications
    +
    {item.certifications || 'None listed'}
    - +
    - - -
    -
    Unitspercase
    -
    -
    - { item.units_per_case } -
    -
    -
    - - - - -
    -
    Unitweight(lbs)
    -
    -
    - { item.unit_weight_lbs } -
    -
    -
    - - - - -
    -
    Caseweight(lbs)
    -
    -
    - { item.case_weight_lbs } -
    -
    -
    - - - - -
    -
    Unitofmeasure
    -
    -
    - { item.uom } -
    -
    -
    - - - - -
    -
    MOQ(cases)
    -
    -
    - { item.moq_cases } -
    -
    -
    - - - - -
    -
    Temperaturezone
    -
    -
    - { item.temperature_zone } -
    -
    -
    - - - - -
    -
    Productstatus
    -
    -
    - { item.product_status } -
    -
    -
    - - - - -
    -
    Allergens
    -
    -
    - { item.allergens } -
    -
    -
    - - - - -
    -
    Certifications
    -
    -
    - { item.certifications } -
    -
    -
    - - - - -
    -
    Sampleeligible
    -
    -
    - { dataFormatter.booleanFormatter(item.is_sample_eligible) } -
    -
    -
    - - - -
    +
    + + Open SKU + + {hasUpdatePermission && ( + + Edit + + )} + {specSheetLinks.length > 0 && ( + + )} +
    +
  • - ))} + )})} {!loading && products.length === 0 && ( -
    -

    No data to display

    +
    +
    +
    No catalog items yet
    +

    + Add SKUs or import a CSV to start building the distributor catalog buyers will reorder from. +

    +
    )}
-
+
{ const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); @@ -65,6 +72,13 @@ const TableSampleSample_requests = ({ filterItems, setFilterItems, filters, show dispatch(fetch({ limit: perPage, page, query })); }; + const visibleSampleRequests = useMemo(() => { + return (sample_requests ?? []).filter((item) => { + const searchableText = JSON.stringify(item); + return !generatedSeedNames.some((name) => searchableText.includes(name)); + }); + }, [sample_requests]); + useEffect(() => { if (sample_requestsNotify.showNotification) { notify(sample_requestsNotify.typeNotification, sample_requestsNotify.textNotification); @@ -250,7 +264,7 @@ const TableSampleSample_requests = ({ filterItems, setFilterItems, filters, show sx={dataGridStyles} className={'datagrid--table'} getRowClassName={() => `datagrid--row`} - rows={sample_requests ?? []} + rows={visibleSampleRequests} columns={columns} initialState={{ pagination: { @@ -283,7 +297,7 @@ const TableSampleSample_requests = ({ filterItems, setFilterItems, filters, show ? setSortModel(params) : setSortModel([{ field: '', sort: 'desc' }]); }} - rowCount={count} + rowCount={visibleSampleRequests.length} pageSizeOptions={[10]} paginationMode={'server'} loading={loading} diff --git a/frontend/src/components/Sample_requests/configureSample_requestsCols.tsx b/frontend/src/components/Sample_requests/configureSample_requestsCols.tsx index 502314f..621e27e 100644 --- a/frontend/src/components/Sample_requests/configureSample_requestsCols.tsx +++ b/frontend/src/components/Sample_requests/configureSample_requestsCols.tsx @@ -43,7 +43,7 @@ export const loadColumns = async ( { field: 'sample_request_number', - headerName: 'Samplerequestnumber', + headerName: 'Sample request #', flex: 1, minWidth: 120, filterable: false, @@ -58,7 +58,7 @@ export const loadColumns = async ( { field: 'account', - headerName: 'Account', + headerName: 'Buyer account', flex: 1, minWidth: 120, filterable: false, @@ -80,7 +80,7 @@ export const loadColumns = async ( { field: 'location', - headerName: 'Location', + headerName: 'Ship-to location', flex: 1, minWidth: 120, filterable: false, @@ -102,7 +102,7 @@ export const loadColumns = async ( { field: 'requested_by', - headerName: 'Requestedby', + headerName: 'Requested by', flex: 1, minWidth: 120, filterable: false, @@ -146,7 +146,7 @@ export const loadColumns = async ( { field: 'sample_quantity', - headerName: 'Samplequantity', + headerName: 'Quantity', flex: 1, minWidth: 120, filterable: false, @@ -162,7 +162,7 @@ export const loadColumns = async ( { field: 'requested_at', - headerName: 'Requestedat', + headerName: 'Requested at', flex: 1, minWidth: 120, filterable: false, @@ -180,7 +180,7 @@ export const loadColumns = async ( { field: 'needed_by', - headerName: 'Neededby', + headerName: 'Needed by', flex: 1, minWidth: 120, filterable: false, @@ -198,7 +198,7 @@ export const loadColumns = async ( { field: 'sample_status', - headerName: 'Samplestatus', + headerName: 'Status', flex: 1, minWidth: 120, filterable: false, @@ -213,7 +213,7 @@ export const loadColumns = async ( { field: 'notes', - headerName: 'Notes', + headerName: 'Tasting notes', flex: 1, minWidth: 120, filterable: false, diff --git a/frontend/src/helpers/userPermissions.ts b/frontend/src/helpers/userPermissions.ts index 2f9c9f9..63e520d 100644 --- a/frontend/src/helpers/userPermissions.ts +++ b/frontend/src/helpers/userPermissions.ts @@ -4,15 +4,18 @@ export function hasPermission(user, permission_name: string | string[]) { if (!permission_name) { return true; } + if (user.app_role.name === 'Administrator') { + return true; + } + const permissions = new Set([ ...(user?.custom_permissions ?? []).map((p) => p.name), ...(user?.app_role_permissions ?? []).map((p) => p.name), ]); if (typeof permission_name === 'string') { - return permissions.has(permission_name) || user.app_role.name === 'Administrator' + return permissions.has(permission_name); } else { return permission_name.some((permission) => permissions.has(permission)); } } - diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index d1d3508..cfee38c 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -18,7 +18,7 @@ import { hasPermission } from '../helpers/userPermissions'; type Props = { children: ReactNode; - permission?: string; + permission?: string | string[]; }; export default function LayoutAuthenticated({ diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 74f69d5..cc867b8 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -6,12 +6,67 @@ const menuAside: MenuAsideItem[] = [ href: '/dashboard', icon: icon.mdiViewDashboardOutline, label: 'Operations dashboard', + permissions: [ + 'READ_ACCOUNTS', + 'READ_PRODUCTS', + 'READ_ORDERS', + 'READ_INVENTORY_ITEMS', + ], }, { - href: '/buyer-portal', + label: 'Buyer workspace', icon: icon.mdiCart, - label: 'Buyer portal', permissions: 'READ_BUYER_PORTAL', + menu: [ + { + href: '/buyer-portal', + icon: icon.mdiViewDashboardOutline, + label: 'Portal overview', + permissions: 'READ_BUYER_PORTAL', + }, + { + href: '/buyer-portal#catalog', + icon: icon.mdiPackageVariantClosed, + label: 'Contract catalog', + permissions: 'READ_BUYER_PORTAL', + }, + { + href: '/buyer-portal#purchase-order', + icon: icon.mdiClipboardTextOutline, + label: 'Draft purchase order', + permissions: 'CREATE_ORDERS', + }, + { + href: '/buyer-portal#samples', + icon: icon.mdiPackageVariant, + label: 'Sample request', + permissions: 'CREATE_SAMPLE_REQUESTS', + }, + { + href: '/buyer-portal#orders', + icon: icon.mdiHistory, + label: 'Order history', + permissions: 'READ_BUYER_PORTAL', + }, + { + href: '/buyer-portal#team-queue', + icon: icon.mdiClipboardList, + label: 'Team queues', + permissions: 'READ_BUYER_TEAM_QUEUE', + }, + { + href: '/buyer-portal#buyer-quotes', + icon: icon.mdiFileDocumentOutline, + label: 'Quote follow-up', + permissions: 'READ_BUYER_TEAM_QUEUE', + }, + { + href: '/buyer-portal#saved-guides', + icon: icon.mdiBookmarkMultiple, + label: 'Saved order guides', + permissions: 'READ_BUYER_TEAM_QUEUE', + }, + ], }, { label: 'Customer operations', diff --git a/frontend/src/pages/accounts/accounts-list.tsx b/frontend/src/pages/accounts/accounts-list.tsx index ce3c208..e8a0889 100644 --- a/frontend/src/pages/accounts/accounts-list.tsx +++ b/frontend/src/pages/accounts/accounts-list.tsx @@ -25,7 +25,6 @@ const AccountsTablesPage = () => { const [filterItems, setFilterItems] = useState([]); const [csvFile, setCsvFile] = useState(null); const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); const { currentUser } = useAppSelector((state) => state.auth); @@ -34,21 +33,21 @@ const AccountsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{label: 'Accountname', title: 'account_name'},{label: 'Accountnumber', title: 'account_number'},{label: 'Taxexemptnumber', title: 'tax_exempt_number'},{label: 'Notes', title: 'notes'}, + const [filters] = useState([{label: 'Account name', title: 'account_name'},{label: 'Account number', title: 'account_number'},{label: 'Tax exempt number', title: 'tax_exempt_number'},{label: 'Notes', title: 'notes'}, - {label: 'Creditlimit', title: 'credit_limit', number: 'true'}, + {label: 'Credit limit', title: 'credit_limit', number: 'true'}, - {label: 'Defaultpricelist', title: 'default_price_list'}, + {label: 'Default price list', title: 'default_price_list'}, - {label: 'Assignedsalesrep', title: 'assigned_sales_rep'}, + {label: 'Assigned sales rep', title: 'assigned_sales_rep'}, - {label: 'Accounttype', title: 'account_type', type: 'enum', options: ['restaurant','caterer','hotel','corporate','other']},{label: 'Creditstatus', title: 'credit_status', type: 'enum', options: ['good','hold','delinquent']}, + {label: 'Account type', title: 'account_type', type: 'enum', options: ['restaurant','caterer','hotel','corporate','other']},{label: 'Credit status', title: 'credit_status', type: 'enum', options: ['good','hold','delinquent']}, ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ACCOUNTS'); @@ -94,28 +93,28 @@ const AccountsTablesPage = () => { return ( <> - {getPageTitle('Accounts')} + {getPageTitle('Buyer accounts')} - + {''} - {hasCreatePermission && } + {hasCreatePermission && } - + {hasCreatePermission && ( setIsModalActive(true)} /> )} diff --git a/frontend/src/pages/accounts/accounts-table.tsx b/frontend/src/pages/accounts/accounts-table.tsx index fc753d2..d35077e 100644 --- a/frontend/src/pages/accounts/accounts-table.tsx +++ b/frontend/src/pages/accounts/accounts-table.tsx @@ -25,7 +25,6 @@ const AccountsTablesPage = () => { const [filterItems, setFilterItems] = useState([]); const [csvFile, setCsvFile] = useState(null); const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); const { currentUser } = useAppSelector((state) => state.auth); @@ -34,21 +33,21 @@ const AccountsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{label: 'Accountname', title: 'account_name'},{label: 'Accountnumber', title: 'account_number'},{label: 'Taxexemptnumber', title: 'tax_exempt_number'},{label: 'Notes', title: 'notes'}, + const [filters] = useState([{label: 'Account name', title: 'account_name'},{label: 'Account number', title: 'account_number'},{label: 'Tax exempt number', title: 'tax_exempt_number'},{label: 'Notes', title: 'notes'}, - {label: 'Creditlimit', title: 'credit_limit', number: 'true'}, + {label: 'Credit limit', title: 'credit_limit', number: 'true'}, - {label: 'Defaultpricelist', title: 'default_price_list'}, + {label: 'Default price list', title: 'default_price_list'}, - {label: 'Assignedsalesrep', title: 'assigned_sales_rep'}, + {label: 'Assigned sales rep', title: 'assigned_sales_rep'}, - {label: 'Accounttype', title: 'account_type', type: 'enum', options: ['restaurant','caterer','hotel','corporate','other']},{label: 'Creditstatus', title: 'credit_status', type: 'enum', options: ['good','hold','delinquent']}, + {label: 'Account type', title: 'account_type', type: 'enum', options: ['restaurant','caterer','hotel','corporate','other']},{label: 'Credit status', title: 'credit_status', type: 'enum', options: ['good','hold','delinquent']}, ]); const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ACCOUNTS'); @@ -94,28 +93,28 @@ const AccountsTablesPage = () => { return ( <> - {getPageTitle('Accounts')} + {getPageTitle('Buyer accounts table')} - + {''} - {hasCreatePermission && } + {hasCreatePermission && } - + {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -124,7 +123,7 @@ const AccountsTablesPage = () => {
- Back to table + Card view
diff --git a/frontend/src/pages/accounts/accounts-view.tsx b/frontend/src/pages/accounts/accounts-view.tsx index dbe8b85..ae3c957 100644 --- a/frontend/src/pages/accounts/accounts-view.tsx +++ b/frontend/src/pages/accounts/accounts-view.tsx @@ -1,1388 +1,409 @@ import React, { ReactElement, useEffect } from 'react'; import Head from 'next/head' -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import dayjs from "dayjs"; -import {useAppDispatch, useAppSelector} from "../../stores/hooks"; -import {useRouter} from "next/router"; +import { useAppDispatch, useAppSelector } from "../../stores/hooks"; +import { useRouter } from "next/router"; import { fetch } from '../../stores/accounts/accountsSlice' -import {saveFile} from "../../helpers/fileSaver"; import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from "../../components/ImageField"; import LayoutAuthenticated from "../../layouts/Authenticated"; -import {getPageTitle} from "../../config"; +import { getPageTitle } from "../../config"; import SectionTitleLineWithButton from "../../components/SectionTitleLineWithButton"; import SectionMain from "../../components/SectionMain"; -import CardBox from "../../components/CardBox"; import BaseButton from "../../components/BaseButton"; -import BaseDivider from "../../components/BaseDivider"; -import {mdiChartTimelineVariant} from "@mdi/js"; -import {SwitchField} from "../../components/SwitchField"; -import FormField from "../../components/FormField"; +import { mdiChartTimelineVariant } from "@mdi/js"; +const readableValue = (value) => { + if (!value) { + return 'Not set'; + } + + return String(value).replace(/_/g, ' '); +}; + +const relatedArray = (value) => { + if (Array.isArray(value)) { + return value; + } + + return []; +}; + +const formatMoney = (value) => { + if (value === null || value === undefined || value === '') { + return 'No data'; + } + + const amount = Number(value); + if (Number.isNaN(amount)) { + return value; + } + + return amount.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, + }); +}; + +const formatDate = (value) => { + if (!value) { + return 'No date'; + } + + return dataFormatter.dateTimeFormatter(value); +}; + +const booleanLabel = (value) => dataFormatter.booleanFormatter(value); + +const badgeClasses = (value) => { + if (value === 'good' || value === 'active' || value === 'approved' || value === 'submitted') { + return 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100'; + } + + if (value === 'hold' || value === 'pending' || value === 'draft' || value === 'requested') { + return 'border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100'; + } + + if (value === 'delinquent' || value === 'cancelled' || value === 'rejected') { + return 'border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-100'; + } + + return 'border-slate-200 bg-slate-50 text-slate-700 dark:border-dark-700 dark:bg-dark-800 dark:text-slate-200'; +}; + +const StatusBadge = ({ value }) => ( + + {readableValue(value)} + +); + +const InfoCard = ({ label, value }) => ( +
+
{label}
+
{value}
+
+); + +const SectionPanel = ({ title, subtitle, children }) => ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
{children}
+
+); + +const EmptyState = ({ label }) => ( +
+ {label} +
+); + +const AccountMeta = ({ label, value }) => ( +
+
{label}
+
{value}
+
+); + const AccountsView = () => { const router = useRouter() const dispatch = useAppDispatch() const { accounts } = useAppSelector((state) => state.accounts) - const { id } = router.query; - - function removeLastCharacter(str) { - console.log(str,`str`) - return str.slice(0, -1); - } useEffect(() => { dispatch(fetch({ id })); }, [dispatch, id]); + const locations = relatedArray(accounts?.locations_account); + const contacts = relatedArray(accounts?.contacts_account); + const accountPriceLists = relatedArray(accounts?.account_price_lists_account); + const carts = relatedArray(accounts?.carts_account); + const orders = relatedArray(accounts?.orders_account); + const quotes = relatedArray(accounts?.quotes_account); + const sampleRequests = relatedArray(accounts?.sample_requests_account); + const savedLists = relatedArray(accounts?.saved_lists_account); + const accountName = accounts?.account_name || 'Buyer account'; + const defaultPriceList = accounts?.default_price_list?.price_list_name || 'No price list'; + const assignedRep = accounts?.assigned_sales_rep?.firstName || 'No rep assigned'; return ( <> - {getPageTitle('View accounts')} + {getPageTitle(accountName)} - + - - - -
-

Accountname

-

{accounts?.account_name}

+
+
+
+
+
+ + + + {readableValue(accounts?.account_type)} + +
+
+ Foodservice buyer profile +
+

+ {accountName} +

+

+ Purchasing account for contract pricing, location-specific delivery, buyer contacts, saved order guides, sample requests, quotes, and purchase orders. +

+ +
+ + + + +
+
+ +
+ + + + +
- +
- +
+ +
+ } /> + + + +
+
- + +
+ {accounts?.notes || 'No account notes yet.'} +
+
+
- + + {locations.length > 0 ? ( +
+ {locations.map((item: any) => ( + + ))} +
+ ) : ( + + )} +
- + + {contacts.length > 0 ? ( +
+ {contacts.map((item: any) => ( + + ))} +
+ ) : ( + + )} +
- +
+ + {accountPriceLists.length > 0 ? ( +
+ {accountPriceLists.map((item: any) => ( + + ))} +
+ ) : ( + + )} +
- + + {savedLists.length > 0 ? ( +
+ {savedLists.map((item: any) => ( + + ))} +
+ ) : ( + + )} +
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Accounttype

-

{accounts?.account_type ?? 'No data'}

+ +
+ + + +
- - - - - - - - - - - - - - -
-

Accountnumber

-

{accounts?.account_number}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Taxexemptnumber

-

{accounts?.tax_exempt_number}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - null}} - disabled - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Creditstatus

-

{accounts?.credit_status ?? 'No data'}

-
- - - - - - - - - - - - - - - - - - - - -
-

Creditlimit

-

{accounts?.credit_limit || 'No data'}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Defaultpricelist

- - - - - - - - - - - - - - - - - - - - -

{accounts?.default_price_list?.price_list_name ?? 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Assignedsalesrep

- - -

{accounts?.assigned_sales_rep?.firstName ?? 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - -