This commit is contained in:
Flatlogic Bot 2026-05-08 14:20:28 +00:00
parent e8ed8d174b
commit b8133ffe9f
29 changed files with 3217 additions and 4019 deletions

View File

@ -78,7 +78,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Accountname</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Account name</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.account_name }
@ -90,7 +90,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Accounttype</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Account type</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.account_type }
@ -102,7 +102,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Accountnumber</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Account number</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.account_number }
@ -114,7 +114,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Taxexemptnumber</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Tax exempt number</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.tax_exempt_number }
@ -126,7 +126,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Taxexempt</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Tax exempt</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.is_tax_exempt) }
@ -138,7 +138,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Creditstatus</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Credit status</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.credit_status }
@ -150,7 +150,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Creditlimit</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Credit limit</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.credit_limit }
@ -162,7 +162,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Defaultpricelist</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Default price list</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.price_listsOneListFormatter(item.default_price_list) }
@ -174,7 +174,7 @@ const CardAccounts = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Assignedsalesrep</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Assigned sales rep</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.usersOneListFormatter(item.assigned_sales_rep) }

View File

@ -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,

View File

@ -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
</p>
<p className='mt-1 text-sm font-semibold text-slate-700 dark:text-slate-200'>
Contract catalog + orders
{workspaceLabel}
</p>
</div>
</div>

View File

@ -78,7 +78,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Ordernumber</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Order number</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.order_number }
@ -126,7 +126,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>POnumber</dt>
<dt className=' text-gray-500 dark:text-dark-600'>PO number</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.po_number }
@ -138,7 +138,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Orderedat</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Ordered at</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.ordered_at) }
@ -150,7 +150,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Requesteddeliverydate</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Requested delivery date</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.requested_delivery_date) }
@ -162,7 +162,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Promiseddeliverydate</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Promised delivery date</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.dateTimeFormatter(item.promised_delivery_date) }
@ -174,7 +174,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Orderstatus</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Order status</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.order_status }
@ -186,7 +186,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Paymentterms</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Payment terms</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.payment_terms }
@ -210,7 +210,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Taxtotal</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Tax total</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.tax_total }
@ -222,7 +222,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Shippingtotal</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Shipping total</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.shipping_total }
@ -234,7 +234,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Ordertotal</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Order total</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.order_total }
@ -246,7 +246,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Buyernotes</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Buyer notes</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.buyer_notes }
@ -258,7 +258,7 @@ const CardOrders = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Internalnotes</dt>
<dt className=' text-gray-500 dark:text-dark-600'>Internal notes</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.internal_notes }

View File

@ -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" },
]);

View File

@ -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,

View File

@ -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 (
<div className={'p-4'}>
<div className='p-4 md:p-6'>
{loading && <LoadingSpinner />}
<ul
role='list'
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
className='grid grid-cols-1 gap-5 lg:grid-cols-2 2xl:grid-cols-3'
>
{!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 (
<li
key={item.id}
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
className='overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:border-emerald-200 hover:shadow-md dark:border-dark-700 dark:bg-dark-900'
>
<div className={`flex items-center ${bgColor} p-6 md:p-0 md:block gap-x-4 border-b border-gray-900/5 bg-gray-50 dark:bg-dark-800 relative`}>
<Link
href={`/products/products-view/?id=${item.id}`}
className={'cursor-pointer'}
>
<div className='relative border-b border-slate-200 bg-slate-100 dark:border-dark-700 dark:bg-dark-800'>
<Link href={`/products/products-view/?id=${item.id}`} className='block'>
{hasProductImage ? (
<ImageField
name={'Avatar'}
name={'Product image'}
image={item.images}
className='w-12 h-12 md:w-full md:h-44 rounded-lg md:rounded-b-none overflow-hidden ring-1 ring-gray-900/10'
imageClassName='h-full w-full flex-none rounded-lg md:rounded-b-none bg-white object-cover'
className='h-48 w-full overflow-hidden'
imageClassName='h-full w-full bg-white object-cover'
/>
<p className={'px-6 py-2 font-semibold'}>{item.product_name}</p>
</Link>
<div className='ml-auto md:absolute md:top-0 md:right-0 '>
) : (
<div
role='img'
aria-label={item.product_name}
className='h-48 w-full bg-cover bg-center bg-white'
style={{ backgroundImage: `url(${fallbackImage})` }}
/>
)}
</Link>
<div className='absolute left-4 top-4 rounded-full border border-white/70 bg-white/95 px-3 py-1 text-xs font-bold uppercase tracking-[0.16em] text-slate-700 shadow-sm dark:border-dark-700 dark:bg-dark-900/95 dark:text-slate-200'>
{item.sku}
</div>
<div className='absolute right-3 top-3'>
<ListActionsPopover
onDelete={onDelete}
itemId={item.id}
pathEdit={`/products/products-edit/?id=${item.id}`}
pathView={`/products/products-view/?id=${item.id}`}
hasUpdatePermission={hasUpdatePermission}
/>
</div>
</div>
<dl className='divide-y divide-stone-300 dark:divide-dark-700 px-6 py-4 text-sm leading-6 h-64 overflow-y-auto'>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>SKU</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.sku }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Productname</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.product_name }
</div>
</dd>
</div>
<div className='p-5'>
<div className='mb-3 flex flex-wrap items-center gap-2'>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold capitalize ${statusClasses(item.product_status)}`}>
{readableValue(item.product_status)}
</span>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold capitalize ${temperatureClasses(item.temperature_zone)}`}>
{readableValue(item.temperature_zone)}
</span>
{sampleEligible && (
<span className='rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100'>
Sample-ready
</span>
)}
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Brand</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.brand }
</div>
</dd>
</div>
<div className='mb-2 text-xs font-bold uppercase tracking-[0.18em] text-emerald-700 dark:text-emerald-300'>
{category}
</div>
<Link href={`/products/products-view/?id=${item.id}`} className='block'>
<h2 className='text-xl font-semibold leading-7 text-slate-950 hover:text-emerald-700 dark:text-white dark:hover:text-emerald-300'>
{item.product_name}
</h2>
</Link>
<div className='mt-1 text-sm font-medium text-slate-500 dark:text-slate-400'>
{item.brand}
</div>
<p className='mt-3 min-h-[3.5rem] text-sm leading-6 text-slate-600 line-clamp-3 dark:text-slate-300'>
{item.short_description || item.long_description || 'No product description yet.'}
</p>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Category</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.product_categoriesOneListFormatter(item.category) }
</div>
</dd>
<div className='mt-5 grid grid-cols-2 gap-3 text-sm'>
<div className='rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-dark-700 dark:bg-dark-800'>
<div className='text-xs font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400'>Pack</div>
<div className='mt-1 font-semibold text-slate-950 dark:text-white'>{item.pack_size || 'Not set'}</div>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Shortdescription</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.short_description }
</div>
</dd>
<div className='rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-dark-700 dark:bg-dark-800'>
<div className='text-xs font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400'>MOQ</div>
<div className='mt-1 font-semibold text-slate-950 dark:text-white'>{item.moq_cases || 0} cases</div>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Longdescription</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.long_description }
</div>
</dd>
<div className='rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-dark-700 dark:bg-dark-800'>
<div className='text-xs font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400'>Units</div>
<div className='mt-1 font-semibold text-slate-950 dark:text-white'>{item.units_per_case || 0} / case</div>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Images</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium'>
<ImageField
name={'Avatar'}
image={item.images}
className='mx-auto w-8 h-8'
/>
</div>
</dd>
<div className='rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-dark-700 dark:bg-dark-800'>
<div className='text-xs font-bold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400'>Case wt.</div>
<div className='mt-1 font-semibold text-slate-950 dark:text-white'>{item.case_weight_lbs || 0} lbs</div>
</div>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Specsheet</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium'>
{dataFormatter.filesFormatter(item.spec_sheet).map(link => (
<button
key={link.publicUrl}
onClick={(e) => saveFile(e, link.publicUrl, link.name)}
>
{link.name}
</button>
))}
</div>
</dd>
<div className='mt-5 space-y-3 border-t border-slate-200 pt-4 text-sm dark:border-dark-700'>
<div>
<div className='mb-1 font-semibold text-slate-700 dark:text-slate-200'>Allergens</div>
<div className='text-slate-600 dark:text-slate-300'>{item.allergens || 'None listed'}</div>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Packsize</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.pack_size }
</div>
</dd>
<div>
<div className='mb-1 font-semibold text-slate-700 dark:text-slate-200'>Certifications</div>
<div className='text-slate-600 dark:text-slate-300'>{item.certifications || 'None listed'}</div>
</div>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Unitspercase</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.units_per_case }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Unitweight(lbs)</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.unit_weight_lbs }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Caseweight(lbs)</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.case_weight_lbs }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Unitofmeasure</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.uom }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>MOQ(cases)</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.moq_cases }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Temperaturezone</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.temperature_zone }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Productstatus</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.product_status }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Allergens</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.allergens }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Certifications</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ item.certifications }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Sampleeligible</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ dataFormatter.booleanFormatter(item.is_sample_eligible) }
</div>
</dd>
</div>
</dl>
<div className='mt-5 flex flex-wrap items-center gap-3'>
<Link
href={`/products/products-view/?id=${item.id}`}
className='rounded-lg bg-slate-950 px-4 py-2 text-sm font-semibold text-white transition hover:bg-emerald-700 dark:bg-white dark:text-slate-950 dark:hover:bg-emerald-100'
>
Open SKU
</Link>
{hasUpdatePermission && (
<Link
href={`/products/products-edit/?id=${item.id}`}
className='rounded-lg border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-emerald-300 hover:text-emerald-700 dark:border-dark-700 dark:text-slate-200 dark:hover:border-emerald-700 dark:hover:text-emerald-300'
>
Edit
</Link>
)}
{specSheetLinks.length > 0 && (
<button
type='button'
className='text-sm font-semibold text-slate-600 underline-offset-4 hover:text-emerald-700 hover:underline dark:text-slate-300 dark:hover:text-emerald-300'
onClick={(e) => saveFile(e, specSheetLinks[0].publicUrl, specSheetLinks[0].name)}
>
Spec sheet
</button>
)}
</div>
</div>
</li>
))}
)})}
{!loading && products.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>
<div className='col-span-full flex min-h-64 items-center justify-center rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center dark:border-dark-700 dark:bg-dark-900'>
<div>
<div className='text-lg font-semibold text-slate-950 dark:text-white'>No catalog items yet</div>
<p className='mt-2 max-w-md text-sm text-slate-500 dark:text-slate-400'>
Add SKUs or import a CSV to start building the distributor catalog buyers will reorder from.
</p>
</div>
</div>
)}
</ul>
<div className={'flex items-center justify-center my-6'}>
<div className='flex items-center justify-center my-6'>
<Pagination
currentPage={currentPage}
numPages={numPages}

View File

@ -58,7 +58,7 @@ export const loadColumns = async (
{
field: 'product_name',
headerName: 'Productname',
headerName: 'Product name',
flex: 1,
minWidth: 120,
filterable: false,
@ -110,7 +110,7 @@ export const loadColumns = async (
{
field: 'short_description',
headerName: 'Shortdescription',
headerName: 'Short description',
flex: 1,
minWidth: 120,
filterable: false,
@ -125,7 +125,7 @@ export const loadColumns = async (
{
field: 'long_description',
headerName: 'Longdescription',
headerName: 'Long description',
flex: 1,
minWidth: 120,
filterable: false,
@ -140,7 +140,7 @@ export const loadColumns = async (
{
field: 'images',
headerName: 'Images',
headerName: 'Image',
flex: 1,
minWidth: 120,
filterable: false,
@ -161,7 +161,7 @@ export const loadColumns = async (
{
field: 'spec_sheet',
headerName: 'Specsheet',
headerName: 'Spec sheet',
flex: 1,
minWidth: 120,
filterable: false,
@ -187,7 +187,7 @@ export const loadColumns = async (
{
field: 'pack_size',
headerName: 'Packsize',
headerName: 'Pack size',
flex: 1,
minWidth: 120,
filterable: false,
@ -202,7 +202,7 @@ export const loadColumns = async (
{
field: 'units_per_case',
headerName: 'Unitspercase',
headerName: 'Units per case',
flex: 1,
minWidth: 120,
filterable: false,
@ -218,7 +218,7 @@ export const loadColumns = async (
{
field: 'unit_weight_lbs',
headerName: 'Unitweight(lbs)',
headerName: 'Unit weight lbs',
flex: 1,
minWidth: 120,
filterable: false,
@ -234,7 +234,7 @@ export const loadColumns = async (
{
field: 'case_weight_lbs',
headerName: 'Caseweight(lbs)',
headerName: 'Case weight lbs',
flex: 1,
minWidth: 120,
filterable: false,
@ -250,7 +250,7 @@ export const loadColumns = async (
{
field: 'uom',
headerName: 'Unitofmeasure',
headerName: 'Unit of measure',
flex: 1,
minWidth: 120,
filterable: false,
@ -265,7 +265,7 @@ export const loadColumns = async (
{
field: 'moq_cases',
headerName: 'MOQ(cases)',
headerName: 'MOQ cases',
flex: 1,
minWidth: 120,
filterable: false,
@ -281,7 +281,7 @@ export const loadColumns = async (
{
field: 'temperature_zone',
headerName: 'Temperaturezone',
headerName: 'Temperature zone',
flex: 1,
minWidth: 120,
filterable: false,
@ -296,7 +296,7 @@ export const loadColumns = async (
{
field: 'product_status',
headerName: 'Productstatus',
headerName: 'Product status',
flex: 1,
minWidth: 120,
filterable: false,
@ -341,7 +341,7 @@ export const loadColumns = async (
{
field: 'is_sample_eligible',
headerName: 'Sampleeligible',
headerName: 'Sample eligible',
flex: 1,
minWidth: 120,
filterable: false,

View File

@ -24,6 +24,13 @@ import axios from 'axios';
const perPage = 10
const generatedSeedNames = [
'Ada Lovelace',
'Alan Turing',
'Grace Hopper',
'Marie Curie',
];
const TableSampleSample_requests = ({ filterItems, setFilterItems, filters, showGrid }) => {
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}

View File

@ -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,

View File

@ -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<string>([
...(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));
}
}

View File

@ -18,7 +18,7 @@ import { hasPermission } from '../helpers/userPermissions';
type Props = {
children: ReactNode;
permission?: string;
permission?: string | string[];
};
export default function LayoutAuthenticated({

View File

@ -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',

View File

@ -25,7 +25,6 @@ const AccountsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(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 (
<>
<Head>
<title>{getPageTitle('Accounts')}</title>
<title>{getPageTitle('Buyer accounts')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Accounts" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Buyer accounts" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/accounts/accounts-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/accounts/accounts-new'} color='info' label='Add account'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getAccountsCSV} />
<BaseButton className={'mr-3'} color='info' label='Export CSV' onClick={getAccountsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}

View File

@ -25,7 +25,6 @@ const AccountsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(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 (
<>
<Head>
<title>{getPageTitle('Accounts')}</title>
<title>{getPageTitle('Buyer accounts table')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Accounts" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Buyer accounts table" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/accounts/accounts-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/accounts/accounts-new'} color='info' label='Add account'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getAccountsCSV} />
<BaseButton className={'mr-3'} color='info' label='Export CSV' onClick={getAccountsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -124,7 +123,7 @@ const AccountsTablesPage = () => {
<div id='delete-rows-button'></div>
<Link href={'/accounts/accounts-list'}>
Back to <span className='capitalize'>table</span>
Card view
</Link>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,279 +1,32 @@
import type { ReactElement } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { useState } from "react";
import axios from "axios";
import jwt from "jsonwebtoken";
import type { ReactElement } from 'react';
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import LayoutGuest from "../layouts/Guest";
import { getPageTitle } from "../config";
import LayoutGuest from '../layouts/Guest';
const heroImage =
"https://images.pexels.com/photos/1126728/pexels-photo-1126728.jpeg?auto=compress&cs=tinysrgb&w=1800";
const sampleImage =
"https://images.pexels.com/photos/4198018/pexels-photo-4198018.jpeg?auto=compress&cs=tinysrgb&w=900";
const buyerCredentials = [
{
label: "Buyer admin",
role: "Customer Buyer Admin",
name: "Maria Alvarez",
email: "maria.alvarez@harbortable.com",
password: "a04a876c6d59",
note: "Purchasing lead with order, sample, quote, and location workflow access.",
},
{
label: "Buyer user",
role: "Customer Buyer",
name: "Owen Price",
email: "owen.price@harbortable.com",
password: "a04a876c6d59",
note: "Restaurant buyer focused on catalog, reorder history, and PO checkout.",
},
];
export default function BuyerLoginPage() {
export default function BuyerLoginRedirect() {
const router = useRouter();
const [email, setEmail] = useState(buyerCredentials[0].email);
const [password, setPassword] = useState(buyerCredentials[0].password);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const getReturnTo = () => {
const value = Array.isArray(router.query.returnTo)
useEffect(() => {
if (!router.isReady) {
return;
}
const returnTo = Array.isArray(router.query.returnTo)
? router.query.returnTo[0]
: router.query.returnTo;
: router.query.returnTo || '/buyer-portal/';
if (value && value.startsWith("/")) {
return value;
}
router.replace({
pathname: '/login',
query: {
returnTo,
},
});
}, [router]);
return "/buyer-portal/";
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
setErrorMessage("");
try {
const { data: token } = await axios.post("/auth/signin/local", {
email,
password,
});
const user = jwt.decode(token);
localStorage.setItem("token", token);
localStorage.setItem("user", JSON.stringify(user));
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
router.push(getReturnTo());
} catch (error: any) {
console.error("Buyer login failed", error);
setErrorMessage(
error?.response?.data ||
"Unable to sign in with those buyer credentials."
);
} finally {
setIsSubmitting(false);
}
};
const useCredentials = (credentials: typeof buyerCredentials[number]) => {
setEmail(credentials.email);
setPassword(credentials.password);
setErrorMessage("");
};
return (
<>
<Head>
<title>{getPageTitle("Buyer sign in")}</title>
<meta
name="description"
content="Buyer sign in for the Northstar Foodservice B2B supplier and distributor portal."
/>
</Head>
<main className="min-h-screen bg-stone-50 text-slate-950">
<section className="grid min-h-screen lg:grid-cols-[minmax(0,1.1fr)_520px]">
<div className="relative hidden overflow-hidden lg:block">
<img
src={heroImage}
alt="Restaurant table prepared for foodservice buyers"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-r from-slate-950/80 via-slate-950/35 to-transparent" />
<div className="relative flex h-full flex-col justify-between p-12 text-white">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-emerald-200">
Northstar Foodservice
</p>
<h1 className="mt-5 max-w-2xl text-5xl font-semibold leading-tight">
Contract ordering for restaurant buyers
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-stone-100">
Browse account pricing, request samples, build purchase
orders, and reorder from history without chasing PDF sheets.
</p>
</div>
<div className="grid max-w-2xl gap-3 sm:grid-cols-3">
<div className="border-l-4 border-emerald-300 bg-slate-950/45 p-4 backdrop-blur">
<p className="text-xs uppercase tracking-[0.18em] text-stone-300">
Price file
</p>
<p className="mt-2 font-semibold">Northstar Contract</p>
</div>
<div className="border-l-4 border-amber-300 bg-slate-950/45 p-4 backdrop-blur">
<p className="text-xs uppercase tracking-[0.18em] text-stone-300">
Cutoff
</p>
<p className="mt-2 font-semibold">Today 3 PM</p>
</div>
<div className="border-l-4 border-sky-300 bg-slate-950/45 p-4 backdrop-blur">
<p className="text-xs uppercase tracking-[0.18em] text-stone-300">
Workflow
</p>
<p className="mt-2 font-semibold">Catalog to PO</p>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center px-6 py-10">
<div className="w-full max-w-md">
<a
href="/"
className="inline-flex text-sm font-semibold text-emerald-700"
>
Back to portal overview
</a>
<div className="mt-8 overflow-hidden border border-stone-200 bg-white shadow-sm">
<img
src={sampleImage}
alt="Sample-ready specialty ingredient"
className="h-44 w-full object-cover"
/>
<div className="p-6">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700">
Buyer sign in
</p>
<h2 className="mt-3 text-3xl font-semibold">
Open your order guide
</h2>
<p className="mt-3 text-sm leading-7 text-slate-600">
Sign in to see contract pricing, saved order behavior,
sample requests, and purchase-order checkout.
</p>
<div className="mt-5 grid gap-3">
{buyerCredentials.map((credentials) => {
const isActive =
email === credentials.email &&
password === credentials.password;
return (
<button
key={credentials.email}
type="button"
onClick={() => useCredentials(credentials)}
className={`rounded-2xl border p-4 text-left transition ${
isActive
? "border-emerald-300 bg-emerald-50"
: "border-stone-200 bg-stone-50 hover:border-emerald-200 hover:bg-emerald-50"
}`}
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-bold text-slate-950">
{credentials.label}
</p>
<p className="mt-1 text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700">
{credentials.role}
</p>
</div>
<span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-600 shadow-sm">
Use creds
</span>
</div>
<p className="mt-3 text-sm font-semibold text-slate-900">
{credentials.name}
</p>
<p className="mt-1 break-all font-mono text-xs text-slate-600">
{credentials.email}
</p>
<p className="mt-1 font-mono text-xs text-slate-500">
{credentials.password}
</p>
<p className="mt-3 text-xs leading-5 text-slate-500">
{credentials.note}
</p>
</button>
);
})}
</div>
{errorMessage && (
<div className="mt-5 rounded-2xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800">
{errorMessage}
</div>
)}
<form className="mt-6 space-y-4" onSubmit={handleSubmit}>
<label className="block">
<span className="text-sm font-semibold text-slate-900">
Email
</span>
<input
value={email}
onChange={(event) => setEmail(event.target.value)}
className="mt-2 h-12 w-full rounded-xl border border-stone-300 bg-white px-4 text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100"
type="email"
autoComplete="email"
/>
</label>
<label className="block">
<span className="text-sm font-semibold text-slate-900">
Password
</span>
<input
value={password}
onChange={(event) => setPassword(event.target.value)}
className="mt-2 h-12 w-full rounded-xl border border-stone-300 bg-white px-4 text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100"
type="password"
autoComplete="current-password"
/>
</label>
<button
type="submit"
disabled={isSubmitting}
className="h-12 w-full rounded-xl bg-emerald-500 px-5 text-sm font-bold text-slate-950 shadow-sm transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-70"
>
{isSubmitting
? "Signing in..."
: "Sign in to buyer portal"}
</button>
</form>
<div className="mt-5 rounded-2xl bg-stone-50 p-4 text-sm leading-7 text-slate-600">
Demo workspace is prefilled for the generated project. A
production supplier would connect this page to buyer
accounts, SSO, or invited customer contacts.
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</>
);
return null;
}
BuyerLoginPage.getLayout = function getLayout(page: ReactElement) {
BuyerLoginRedirect.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -304,6 +304,15 @@ const statusClasses: Record<string, string> = {
backordered: 'bg-amber-50 text-amber-700 border-amber-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 StatusPill = ({ label }: { label?: string | null }) => {
const normalized = label || 'unknown';
const colorClass =
@ -1225,6 +1234,29 @@ const BuyerPortalPage = () => {
[workspace],
);
const readinessSteps = [
{
label: 'Account',
value: currentAccount?.account_name || 'Choose account',
ready: Boolean(selectedAccountId),
},
{
label: 'Ship-to',
value: currentLocation?.location_name || 'Choose location',
ready: Boolean(selectedLocationId),
},
{
label: 'PO',
value: poNumber || 'Required',
ready: Boolean(poNumber.trim()),
},
{
label: 'Draft',
value: `${cartItems.length} line(s)`,
ready: cartItems.length > 0,
},
];
if (!currentUser) {
return (
<>
@ -1341,6 +1373,39 @@ const BuyerPortalPage = () => {
</FormField>
</div>
<div className='mt-5 grid gap-3 lg:grid-cols-4'>
{readinessSteps.map((step, index) => (
<div
key={step.label}
className={`rounded-2xl border p-4 ${
step.ready
? 'border-emerald-200 bg-emerald-50'
: 'border-amber-200 bg-amber-50'
}`}
>
<div className='flex items-center gap-3'>
<span
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold ${
step.ready
? 'bg-emerald-600 text-white'
: 'bg-amber-500 text-white'
}`}
>
{index + 1}
</span>
<div className='min-w-0'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-slate-500'>
{step.label}
</p>
<p className='mt-1 truncate text-sm font-semibold text-slate-950'>
{step.value}
</p>
</div>
</div>
</div>
))}
</div>
<div className='mt-5 grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-4'>
<div className='rounded-2xl bg-blue-50 p-4'>
<p className='text-xs uppercase tracking-[0.16em] text-blue-700'>
@ -1397,7 +1462,10 @@ const BuyerPortalPage = () => {
)}
{canReadBuyerTeamQueue && (
<div className='mb-6 grid gap-4 xl:grid-cols-4'>
<div
id='team-queue'
className='mb-6 grid gap-4 scroll-mt-24 xl:grid-cols-4'
>
<CardBox className='border border-slate-200 bg-white shadow-sm'>
<div className='flex items-start justify-between gap-3'>
<div>
@ -1486,7 +1554,10 @@ const BuyerPortalPage = () => {
</div>
</CardBox>
<CardBox className='border border-slate-200 bg-white shadow-sm'>
<CardBox
id='buyer-quotes'
className='border border-slate-200 bg-white shadow-sm'
>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-violet-600'>
@ -1530,7 +1601,10 @@ const BuyerPortalPage = () => {
</div>
</CardBox>
<CardBox className='border border-slate-200 bg-white shadow-sm'>
<CardBox
id='saved-guides'
className='border border-slate-200 bg-white shadow-sm'
>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-emerald-600'>
@ -1576,7 +1650,7 @@ const BuyerPortalPage = () => {
<div className='mb-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_420px]'>
<CardBox
id='catalog'
className='border border-slate-200 bg-white shadow-sm'
className='scroll-mt-24 border border-slate-200 bg-white shadow-sm'
>
<div className='mb-5 flex flex-col gap-4 border-b border-slate-100 pb-5 lg:flex-row lg:items-center lg:justify-between'>
<div>
@ -1593,9 +1667,9 @@ const BuyerPortalPage = () => {
</div>
<div className='flex flex-wrap gap-2'>
{[
['order_guide', 'Order Guide'],
['full_catalog', 'Full Catalog'],
['previously_purchased', 'Previously Purchased'],
['order_guide', 'Order guide'],
['full_catalog', 'Full catalog'],
['previously_purchased', 'Previously purchased'],
].map(([mode, label]) => (
<button
key={mode}
@ -1680,17 +1754,25 @@ const BuyerPortalPage = () => {
</div>
<div className='space-y-3'>
{filteredCatalog.map((product) => {
{filteredCatalog.map((product, index) => {
const minQty =
product.contract_min_case_qty || product.moq_cases || 1;
const qtyValue =
catalogQuantities[product.id] || String(minQty);
const currentQty = Number.parseInt(qtyValue, 10) || minQty;
const fallbackImage =
fallbackProductImages[index % fallbackProductImages.length];
return (
<div
key={product.id}
className='rounded-2xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-blue-200 hover:shadow-md'
>
<div
role='img'
aria-label={product.product_name}
className='mb-4 h-40 rounded-2xl bg-slate-100 bg-cover bg-center'
style={{ backgroundImage: `url(${fallbackImage})` }}
/>
<div className='grid gap-4'>
<div className='min-w-0'>
<div className='flex flex-wrap items-center gap-2'>
@ -1991,7 +2073,7 @@ const BuyerPortalPage = () => {
<div className='space-y-6 xl:sticky xl:top-6 xl:self-start'>
<CardBox
id='purchase-order'
className='border border-slate-200 bg-white shadow-sm'
className='scroll-mt-24 border border-slate-200 bg-white shadow-sm'
>
<div className='mb-4 flex items-start justify-between gap-4'>
<div>
@ -2200,7 +2282,7 @@ const BuyerPortalPage = () => {
<CardBox
id='samples'
className='border border-slate-200 bg-white shadow-sm'
className='scroll-mt-24 border border-slate-200 bg-white shadow-sm'
>
<div className='mb-4 flex items-start justify-between gap-4'>
<div>
@ -2319,7 +2401,7 @@ const BuyerPortalPage = () => {
<div className='grid gap-6 lg:grid-cols-[1.5fr_1fr]'>
<CardBox
id='orders'
className='border border-slate-200 bg-white shadow-sm'
className='scroll-mt-24 border border-slate-200 bg-white shadow-sm'
>
<div className='mb-5 flex items-start justify-between gap-4'>
<div>
@ -2330,9 +2412,8 @@ const BuyerPortalPage = () => {
Load a previous order into a new draft
</h3>
<p className='mt-2 text-sm text-slate-500'>
This is the thinnest end-to-end buyer slice: select an
account, reuse past purchasing behavior, submit a fresh PO,
and jump into the order detail screen.
Review delivered and active orders, then reload familiar
purchasing patterns into a fresh draft PO.
</p>
</div>
</div>
@ -2430,10 +2511,10 @@ const BuyerPortalPage = () => {
</CardBox>
<div className='space-y-6'>
<CardBox
id='quotes'
className='border border-slate-200 bg-white shadow-sm'
>
<CardBox
id='quotes'
className='scroll-mt-24 border border-slate-200 bg-white shadow-sm'
>
<div className='flex items-start justify-between gap-4'>
<div>
<p className='text-sm font-semibold uppercase tracking-[0.22em] text-blue-600'>

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import type { ReactElement } from "react";
import Head from "next/head";
import Link from "next/link";
import React from "react";
import LayoutGuest from "../layouts/Guest";
@ -182,14 +183,14 @@ export default function HomePage() {
<div className="min-h-screen bg-stone-50 text-slate-950">
<header className="border-b border-stone-200 bg-stone-50/95 backdrop-blur">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-4 px-6 py-4">
<a href="/" className="group block">
<Link href="/" className="group block">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-700">
Northstar Foodservice
</p>
<p className="mt-1 text-lg font-semibold text-slate-950">
Supplier / Distributor B2B Portal
</p>
</a>
</Link>
<nav className="flex flex-wrap items-center gap-2 text-sm font-semibold">
<a
href="#catalog"
@ -203,18 +204,12 @@ export default function HomePage() {
>
Operations
</a>
<a
<Link
href="/login"
className="rounded-full border border-slate-300 bg-white px-4 py-2 text-slate-900 shadow-sm"
>
Login
</a>
<a
href="/buyer-login?returnTo=%2Fbuyer-portal%2F"
className="rounded-full bg-slate-950 px-5 py-2 text-white shadow-sm"
>
Open portal
</a>
Sign in
</Link>
</nav>
</div>
</header>
@ -243,23 +238,23 @@ export default function HomePage() {
spreadsheets or PDF price sheets.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<a
href="/buyer-login?returnTo=%2Fbuyer-portal%2F"
<Link
href="/login"
className="rounded-full bg-emerald-400 px-6 py-3 text-sm font-bold text-slate-950 shadow-lg"
>
Open buyer portal
</a>
Sign in
</Link>
<a
href="/dashboard"
href="#catalog"
className="rounded-full border border-slate-300 bg-white/80 px-6 py-3 text-sm font-bold text-slate-950 shadow-sm backdrop-blur"
>
Admin workspace
View catalog
</a>
<a
href="/login"
href="#operations"
className="rounded-full border border-slate-300 bg-white/55 px-6 py-3 text-sm font-bold text-slate-900 backdrop-blur"
>
Sign in
Operations
</a>
</div>
<div className="mt-10 grid max-w-5xl gap-3 sm:grid-cols-3">
@ -516,12 +511,12 @@ export default function HomePage() {
Checkout review flags PO, delivery location, MOQ, and item
availability before the order reaches the distributor team.
</div>
<a
href="/buyer-login?returnTo=%2Fbuyer-portal%2F"
<Link
href="/login"
className="mt-4 block rounded-xl bg-emerald-500 px-5 py-4 text-center text-sm font-bold text-slate-950"
>
Continue in real buyer portal
</a>
Sign in to continue
</Link>
</div>
<div className="overflow-hidden border border-stone-200 bg-white shadow-sm">
@ -615,8 +610,7 @@ export default function HomePage() {
Try the vertical slice
</p>
<h2 className="mt-3 text-3xl font-semibold">
Open the buyer portal, then inspect the generated admin
modules.
Sign in once, then land in the workspace your role allows.
</h2>
<p className="mt-4 text-base leading-8 text-slate-300">
This gives us a believable first version for the restaurant
@ -626,23 +620,23 @@ export default function HomePage() {
</p>
</div>
<div className="grid gap-3">
<a
href="/buyer-login?returnTo=%2Fbuyer-portal%2F"
<Link
href="/login"
className="rounded-xl bg-emerald-400 px-5 py-4 text-center text-sm font-bold text-slate-950"
>
Open buyer portal
</a>
Sign in
</Link>
<a
href="/dashboard"
href="#catalog"
className="rounded-xl border border-white/20 px-5 py-4 text-center text-sm font-bold text-white"
>
Admin dashboard
Review catalog flow
</a>
<a
href="/products/products-list"
href="#operations"
className="rounded-xl border border-white/20 px-5 py-4 text-center text-sm font-bold text-white"
>
Product admin
Review operations flow
</a>
</div>
</div>

View File

@ -9,14 +9,12 @@ import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
import { findMe, loginUser, resetAction } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { hasPermission } from '../helpers/userPermissions';
const heroImage =
'https://images.pexels.com/photos/1126728/pexels-photo-1126728.jpeg?auto=compress&cs=tinysrgb&w=1800';
const teamImage =
'https://images.pexels.com/photos/6169659/pexels-photo-6169659.jpeg?auto=compress&cs=tinysrgb&w=900';
const staffCredentials = [
const demoCredentials = [
{
label: 'Supplier admin',
role: 'Administrator',
@ -26,20 +24,28 @@ const staffCredentials = [
note: 'Full access to catalog, buyer accounts, price lists, orders, samples, and fulfillment workflows.',
},
{
label: 'Demo user',
role: 'User',
name: 'Standard SaaS user',
email: 'client@hello.com',
label: 'Buyer admin',
role: 'Customer Buyer Admin',
name: 'Maria Alvarez',
email: 'maria.alvarez@harbortable.com',
password: 'a04a876c6d59',
note: 'Lightweight generated SaaS user for smoke checks outside the buyer-specific workspace.',
note: 'Purchasing lead with order, sample, quote, saved guide, and team queue access.',
},
{
label: 'Buyer user',
role: 'Customer Buyer',
name: 'Owen Price',
email: 'owen.price@harbortable.com',
password: 'a04a876c6d59',
note: 'Restaurant buyer focused on contract catalog, reorder history, sample requests, and PO checkout.',
},
];
export default function Login() {
const router = useRouter();
const dispatch = useAppDispatch();
const [email, setEmail] = useState(staffCredentials[0].email);
const [password, setPassword] = useState(staffCredentials[0].password);
const [email, setEmail] = useState(demoCredentials[0].email);
const [password, setPassword] = useState(demoCredentials[0].password);
const [remember, setRemember] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const {
@ -50,7 +56,28 @@ export default function Login() {
notify: notifyState,
} = useAppSelector((state) => state.auth);
const title = 'B2B Distributor Portal';
const getReturnTo = () => {
const value = Array.isArray(router.query.returnTo)
? router.query.returnTo[0]
: router.query.returnTo;
if (value && value.startsWith('/') && !value.startsWith('//')) {
return value;
}
return '';
};
useEffect(() => {
if (!router.isReady) {
return;
}
if (getReturnTo().startsWith('/buyer-portal')) {
setEmail(demoCredentials[1].email);
setPassword(demoCredentials[1].password);
}
}, [router.isReady]);
useEffect(() => {
if (token) {
@ -60,18 +87,31 @@ export default function Login() {
useEffect(() => {
if (currentUser?.id) {
if (
['Customer Buyer Admin', 'Customer Buyer'].includes(
currentUser.app_role?.name,
)
) {
router.push('/buyer-portal');
const returnTo = getReturnTo();
if (returnTo) {
router.push(returnTo);
return;
}
router.push('/dashboard');
const hasSupplierAccess = hasPermission(currentUser, [
'READ_ACCOUNTS',
'READ_PRODUCTS',
'READ_ORDERS',
'READ_INVENTORY_ITEMS',
]);
if (
hasPermission(currentUser, 'READ_BUYER_PORTAL') &&
!hasSupplierAccess
) {
router.push('/buyer-portal/');
return;
}
router.push('/dashboard/');
}
}, [currentUser?.id, router]);
}, [currentUser, router]);
useEffect(() => {
if (errorMessage) {
@ -86,7 +126,7 @@ export default function Login() {
}
}, [notifyState?.showNotification, notifyState?.textNotification, dispatch]);
const applyCredentials = (credentials: (typeof staffCredentials)[number]) => {
const applyCredentials = (credentials: (typeof demoCredentials)[number]) => {
setEmail(credentials.email);
setPassword(credentials.password);
setRemember(true);
@ -103,12 +143,12 @@ export default function Login() {
<title>{getPageTitle('Login')}</title>
<meta
name='description'
content='Staff sign in for the Northstar Foodservice supplier and distributor portal.'
content='Unified sign in for the Northstar Foodservice supplier and distributor portal.'
/>
</Head>
<main className='min-h-screen bg-stone-50 text-slate-950'>
<section className='grid min-h-screen lg:grid-cols-[minmax(0,1.1fr)_520px]'>
<section className='grid min-h-screen lg:grid-cols-[minmax(0,1.1fr)_500px]'>
<div className='relative hidden overflow-hidden lg:block'>
<img
src={heroImage}
@ -122,12 +162,12 @@ export default function Login() {
Northstar Foodservice
</p>
<h1 className='mt-5 max-w-2xl text-5xl font-semibold leading-tight'>
Staff workspace for distributor operations
One sign-in for supplier teams and foodservice buyers
</h1>
<p className='mt-5 max-w-xl text-base leading-8 text-stone-100'>
Manage account-specific pricing, product catalogs, buyer
orders, sample requests, saved lists, and fulfillment handoff
from one generated SaaS admin.
Route each role into the right workspace for contract
pricing, product catalogs, purchase orders, sample requests,
saved lists, and fulfillment handoff.
</p>
</div>
@ -154,7 +194,7 @@ export default function Login() {
</div>
</div>
<div className='flex items-center justify-center px-6 py-10'>
<div className='flex items-center justify-center px-5 py-4 lg:py-2'>
<div className='w-full max-w-md'>
<div className='flex items-center justify-between gap-4'>
<Link
@ -163,35 +203,27 @@ export default function Login() {
>
Back to portal overview
</Link>
<Link
href='/buyer-login/?returnTo=%2Fbuyer-portal%2F'
className='text-sm font-semibold text-slate-500 hover:text-emerald-700'
>
Buyer login
</Link>
<span className='text-sm font-semibold text-slate-500'>
One login for every role
</span>
</div>
<div className='mt-8 overflow-hidden border border-stone-200 bg-white shadow-sm'>
<img
src={teamImage}
alt='Foodservice operations team preparing restaurant orders'
className='h-36 w-full object-cover sm:h-44'
/>
<div className='p-5 sm:p-6'>
<div className='mt-3 overflow-hidden border border-stone-200 bg-white shadow-sm'>
<div className='h-1 bg-emerald-500' />
<div className='p-3 sm:p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-700'>
Staff sign in
Unified sign in
</p>
<h2 className='mt-3 text-2xl font-semibold sm:text-3xl'>
Open the supplier admin
<h2 className='mt-2 text-2xl font-semibold'>
Open your workspace
</h2>
<p className='mt-3 text-sm leading-6 text-slate-600 sm:leading-7'>
Sign in to manage the operational side of the B2B
distributor portal: products, customer accounts, contract
pricing, orders, quotes, and sample workflows.
<p className='mt-2 text-sm leading-6 text-slate-600'>
Use supplier or buyer credentials. The app routes each user
into the right workspace based on role permissions.
</p>
<div className='mt-4 grid gap-2 sm:mt-5 sm:gap-3'>
{staffCredentials.map((credentials) => {
<div className='mt-3 grid gap-2'>
{demoCredentials.map((credentials) => {
const isActive =
email === credentials.email &&
password === credentials.password;
@ -201,49 +233,65 @@ export default function Login() {
key={credentials.email}
type='button'
onClick={() => applyCredentials(credentials)}
className={`rounded-2xl border p-3 text-left transition sm:p-4 ${
className={`rounded-xl border px-3 py-2.5 text-left transition ${
isActive
? 'border-emerald-300 bg-emerald-50'
: 'border-stone-200 bg-stone-50 hover:border-emerald-200 hover:bg-emerald-50'
}`}
>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='text-sm font-bold text-slate-950'>
{credentials.label}
</p>
<p className='mt-1 text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700'>
<div className='grid gap-2 sm:grid-cols-[minmax(0,1fr)_170px] sm:items-center'>
<div className='min-w-0'>
<div className='flex items-center gap-2'>
<p className='truncate text-sm font-bold text-slate-950'>
{credentials.label}
</p>
{isActive && (
<span className='rounded-full bg-emerald-600 px-2 py-0.5 text-[10px] font-bold uppercase tracking-[0.12em] text-white'>
Active
</span>
)}
</div>
<p className='mt-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-emerald-700'>
{credentials.role}
</p>
</div>
<span className='rounded-full bg-white px-3 py-1 text-xs font-semibold text-slate-600 shadow-sm'>
Use creds
<div className='flex min-w-0 items-center justify-between gap-2 sm:block'>
<div className='min-w-0'>
<p className='truncate text-xs font-semibold text-slate-900 sm:hidden'>
{credentials.name}
</p>
<p className='truncate font-mono text-xs text-slate-600'>
{credentials.email}
</p>
<p className='font-mono text-xs text-slate-500'>
{credentials.password}
</p>
</div>
<span className='shrink-0 rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-slate-600 shadow-sm sm:hidden'>
Use
</span>
</div>
</div>
<div className='mt-1 hidden items-center justify-between gap-2 sm:flex'>
<p className='truncate text-xs font-semibold text-slate-900'>
{credentials.name}
</p>
<span className='rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-slate-600 shadow-sm'>
Use
</span>
</div>
<p className='mt-3 text-sm font-semibold text-slate-900'>
{credentials.name}
</p>
<p className='mt-1 break-all font-mono text-xs text-slate-600'>
{credentials.email}
</p>
<p className='mt-1 font-mono text-xs text-slate-500'>
{credentials.password}
</p>
<p className='mt-3 hidden text-xs leading-5 text-slate-500 sm:block'>
{credentials.note}
</p>
</button>
);
})}
</div>
{errorMessage && (
<div className='mt-5 rounded-2xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-800'>
<div className='mt-4 rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-800'>
{errorMessage}
</div>
)}
<form className='mt-6 space-y-4' onSubmit={handleSubmit}>
<form className='mt-4 space-y-3' onSubmit={handleSubmit}>
<label className='block'>
<span className='text-sm font-semibold text-slate-900'>
Email
@ -254,7 +302,7 @@ export default function Login() {
onChange={(event) => setEmail(event.target.value)}
type='email'
autoComplete='email'
className='mt-2 h-12 w-full rounded-xl border border-stone-300 bg-white px-4 text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100'
className='mt-1.5 h-11 w-full rounded-xl border border-stone-300 bg-white px-4 text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100'
/>
</label>
@ -262,7 +310,7 @@ export default function Login() {
<span className='text-sm font-semibold text-slate-900'>
Password
</span>
<div className='mt-2 flex h-12 overflow-hidden rounded-xl border border-stone-300 bg-white focus-within:border-emerald-500 focus-within:ring-2 focus-within:ring-emerald-100'>
<div className='mt-1.5 flex h-11 overflow-hidden rounded-xl border border-stone-300 bg-white focus-within:border-emerald-500 focus-within:ring-2 focus:ring-emerald-100'>
<input
name='password'
value={password}
@ -304,38 +352,14 @@ export default function Login() {
<button
type='submit'
disabled={isFetching}
className='h-12 w-full rounded-xl bg-emerald-500 px-5 text-sm font-bold text-slate-950 shadow-sm transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-70'
className='h-11 w-full rounded-xl bg-emerald-500 px-5 text-sm font-bold text-slate-950 shadow-sm transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-70'
>
{isFetching ? 'Signing in...' : 'Sign in to admin'}
{isFetching ? 'Signing in...' : 'Sign in'}
</button>
</form>
<div className='mt-5 rounded-2xl bg-stone-50 p-4 text-sm leading-7 text-slate-600'>
Need buyer access instead? Use the dedicated buyer login for
customer-side catalog, reorder, sample, and PO workflows.
</div>
<p className='mt-5 text-center text-sm text-slate-500'>
Do not have an account yet?{' '}
<Link
className='font-semibold text-emerald-700'
href='/register'
>
New Account
</Link>
</p>
</div>
</div>
<div className='mt-6 flex justify-center gap-4 text-xs text-slate-500'>
<span>© 2026 {title}</span>
<Link
href='/privacy-policy/'
className='hover:text-emerald-700'
>
Privacy Policy
</Link>
</div>
</div>
</div>
</section>

View File

@ -25,7 +25,6 @@ const OrdersTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
@ -34,10 +33,10 @@ const OrdersTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Ordernumber', title: 'order_number'},{label: 'POnumber', title: 'po_number'},{label: 'Buyernotes', title: 'buyer_notes'},{label: 'Internalnotes', title: 'internal_notes'},
const [filters] = useState([{label: 'Order number', title: 'order_number'},{label: 'PO number', title: 'po_number'},{label: 'Buyer notes', title: 'buyer_notes'},{label: 'Internal notes', title: 'internal_notes'},
{label: 'Subtotal', title: 'subtotal', number: 'true'},{label: 'Taxtotal', title: 'tax_total', number: 'true'},{label: 'Shippingtotal', title: 'shipping_total', number: 'true'},{label: 'Ordertotal', title: 'order_total', number: 'true'},
{label: 'Orderedat', title: 'ordered_at', date: 'true'},{label: 'Requesteddeliverydate', title: 'requested_delivery_date', date: 'true'},{label: 'Promiseddeliverydate', title: 'promised_delivery_date', date: 'true'},
{label: 'Subtotal', title: 'subtotal', number: 'true'},{label: 'Tax total', title: 'tax_total', number: 'true'},{label: 'Shipping total', title: 'shipping_total', number: 'true'},{label: 'Order total', title: 'order_total', number: 'true'},
{label: 'Ordered at', title: 'ordered_at', date: 'true'},{label: 'Requested delivery date', title: 'requested_delivery_date', date: 'true'},{label: 'Promised delivery date', title: 'promised_delivery_date', date: 'true'},
{label: 'Account', title: 'account'},
@ -52,7 +51,7 @@ const OrdersTablesPage = () => {
{label: 'Orderstatus', title: 'order_status', type: 'enum', options: ['draft','submitted','approved','picking','shipped','delivered','cancelled']},{label: 'Paymentterms', title: 'payment_terms', type: 'enum', options: ['net_15','net_30','net_45','prepaid']},
{label: 'Order status', title: 'order_status', type: 'enum', options: ['draft','submitted','approved','picking','shipped','delivered','cancelled']},{label: 'Payment terms', title: 'payment_terms', type: 'enum', options: ['net_15','net_30','net_45','prepaid']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ORDERS');
@ -98,28 +97,28 @@ const OrdersTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Orders')}</title>
<title>{getPageTitle('Purchase orders')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Orders" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Purchase orders" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/orders/orders-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/orders/orders-new'} color='info' label='Add order'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getOrdersCSV} />
<BaseButton className={'mr-3'} color='info' label='Export CSV' onClick={getOrdersCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -129,7 +128,7 @@ const OrdersTablesPage = () => {
</div>
<div className='md:inline-flex items-center ms-auto'>
<Link href={'/orders/orders-table'}>Switch to Table</Link>
<Link href={'/orders/orders-table'}>Table view</Link>
</div>
</CardBox>

View File

@ -25,7 +25,6 @@ const OrdersTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
@ -34,10 +33,10 @@ const OrdersTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Ordernumber', title: 'order_number'},{label: 'POnumber', title: 'po_number'},{label: 'Buyernotes', title: 'buyer_notes'},{label: 'Internalnotes', title: 'internal_notes'},
const [filters] = useState([{label: 'Order number', title: 'order_number'},{label: 'PO number', title: 'po_number'},{label: 'Buyer notes', title: 'buyer_notes'},{label: 'Internal notes', title: 'internal_notes'},
{label: 'Subtotal', title: 'subtotal', number: 'true'},{label: 'Taxtotal', title: 'tax_total', number: 'true'},{label: 'Shippingtotal', title: 'shipping_total', number: 'true'},{label: 'Ordertotal', title: 'order_total', number: 'true'},
{label: 'Orderedat', title: 'ordered_at', date: 'true'},{label: 'Requesteddeliverydate', title: 'requested_delivery_date', date: 'true'},{label: 'Promiseddeliverydate', title: 'promised_delivery_date', date: 'true'},
{label: 'Subtotal', title: 'subtotal', number: 'true'},{label: 'Tax total', title: 'tax_total', number: 'true'},{label: 'Shipping total', title: 'shipping_total', number: 'true'},{label: 'Order total', title: 'order_total', number: 'true'},
{label: 'Ordered at', title: 'ordered_at', date: 'true'},{label: 'Requested delivery date', title: 'requested_delivery_date', date: 'true'},{label: 'Promised delivery date', title: 'promised_delivery_date', date: 'true'},
{label: 'Account', title: 'account'},
@ -52,7 +51,7 @@ const OrdersTablesPage = () => {
{label: 'Orderstatus', title: 'order_status', type: 'enum', options: ['draft','submitted','approved','picking','shipped','delivered','cancelled']},{label: 'Paymentterms', title: 'payment_terms', type: 'enum', options: ['net_15','net_30','net_45','prepaid']},
{label: 'Order status', title: 'order_status', type: 'enum', options: ['draft','submitted','approved','picking','shipped','delivered','cancelled']},{label: 'Payment terms', title: 'payment_terms', type: 'enum', options: ['net_15','net_30','net_45','prepaid']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ORDERS');
@ -98,28 +97,28 @@ const OrdersTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Orders')}</title>
<title>{getPageTitle('Purchase orders table')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Orders" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Purchase orders table" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/orders/orders-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/orders/orders-new'} color='info' label='Add order'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getOrdersCSV} />
<BaseButton className={'mr-3'} color='info' label='Export CSV' onClick={getOrdersCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -128,7 +127,7 @@ const OrdersTablesPage = () => {
<div id='delete-rows-button'></div>
<Link href={'/orders/orders-list'}>
Back to <span className='capitalize'>kanban</span>
Card view
</Link>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -25,18 +25,18 @@ const ProductsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
const { count } = useAppSelector((state) => state.products);
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'SKU', title: 'sku'},{label: 'Productname', title: 'product_name'},{label: 'Brand', title: 'brand'},{label: 'Shortdescription', title: 'short_description'},{label: 'Longdescription', title: 'long_description'},{label: 'Packsize', title: 'pack_size'},{label: 'Unitofmeasure', title: 'uom'},{label: 'Allergens', title: 'allergens'},{label: 'Certifications', title: 'certifications'},
{label: 'Unitspercase', title: 'units_per_case', number: 'true'},{label: 'MOQ(cases)', title: 'moq_cases', number: 'true'},
{label: 'Unitweight(lbs)', title: 'unit_weight_lbs', number: 'true'},{label: 'Caseweight(lbs)', title: 'case_weight_lbs', number: 'true'},
const [filters] = useState([{label: 'SKU', title: 'sku'},{label: 'Product name', title: 'product_name'},{label: 'Brand', title: 'brand'},{label: 'Short description', title: 'short_description'},{label: 'Long description', title: 'long_description'},{label: 'Pack size', title: 'pack_size'},{label: 'Unit of measure', title: 'uom'},{label: 'Allergens', title: 'allergens'},{label: 'Certifications', title: 'certifications'},
{label: 'Units per case', title: 'units_per_case', number: 'true'},{label: 'MOQ cases', title: 'moq_cases', number: 'true'},
{label: 'Unit weight lbs', title: 'unit_weight_lbs', number: 'true'},{label: 'Case weight lbs', title: 'case_weight_lbs', number: 'true'},
@ -44,7 +44,7 @@ const ProductsTablesPage = () => {
{label: 'Temperaturezone', title: 'temperature_zone', type: 'enum', options: ['ambient','refrigerated','frozen']},{label: 'Productstatus', title: 'product_status', type: 'enum', options: ['active','discontinued','seasonal','out_of_stock']},
{label: 'Temperature zone', title: 'temperature_zone', type: 'enum', options: ['ambient','refrigerated','frozen']},{label: 'Product status', title: 'product_status', type: 'enum', options: ['active','discontinued','seasonal','out_of_stock']},{label: 'Sample eligible', title: 'is_sample_eligible', type: 'enum', options: ['true','false']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS');
@ -64,6 +64,19 @@ const ProductsTablesPage = () => {
setFilterItems([...filterItems, newItem]);
};
const addPresetFilter = (selectedField: string, filterValue: string) => {
const newItem = {
id: uniqueId(),
fields: {
filterValue,
filterValueFrom: '',
filterValueTo: '',
selectedField,
},
};
setFilterItems([...filterItems, newItem]);
};
const getProductsCSV = async () => {
const response = await axios({url: '/products?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
@ -93,35 +106,99 @@ const ProductsTablesPage = () => {
<title>{getPageTitle('Products')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Contract catalog" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
<div className='mb-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-dark-700 dark:bg-dark-900'>
<div className='grid gap-0 lg:grid-cols-[1.4fr_1fr]'>
<div className='p-6 lg:p-8'>
<div className='mb-3 text-xs font-bold uppercase tracking-[0.24em] text-emerald-700 dark:text-emerald-300'>
Foodservice supplier catalog
</div>
<h1 className='mb-4 max-w-3xl text-3xl font-semibold tracking-normal text-slate-950 dark:text-white'>
Manage contract SKUs with the operational detail buyers expect.
</h1>
<p className='max-w-3xl text-base leading-7 text-slate-600 dark:text-slate-300'>
Keep pack size, storage zone, allergens, certifications, MOQ, sample readiness, and spec sheets visible before a restaurant buyer places a purchase order.
</p>
<div className='mt-6 flex flex-wrap gap-2'>
<button
type='button'
className='rounded-full border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800 transition hover:border-emerald-300 hover:bg-emerald-100 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-100'
onClick={() => addPresetFilter('product_status', 'active')}
>
Active SKUs
</button>
<button
type='button'
className='rounded-full border border-sky-200 bg-sky-50 px-4 py-2 text-sm font-semibold text-sky-800 transition hover:border-sky-300 hover:bg-sky-100 dark:border-sky-800 dark:bg-sky-950 dark:text-sky-100'
onClick={() => addPresetFilter('temperature_zone', 'refrigerated')}
>
Refrigerated
</button>
<button
type='button'
className='rounded-full border border-cyan-200 bg-cyan-50 px-4 py-2 text-sm font-semibold text-cyan-800 transition hover:border-cyan-300 hover:bg-cyan-100 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-100'
onClick={() => addPresetFilter('temperature_zone', 'frozen')}
>
Frozen
</button>
<button
type='button'
className='rounded-full border border-amber-200 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-800 transition hover:border-amber-300 hover:bg-amber-100 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-100'
onClick={() => addPresetFilter('is_sample_eligible', 'true')}
>
Sample-ready
</button>
</div>
</div>
<div className='border-t border-slate-200 bg-slate-50 p-6 dark:border-dark-700 dark:bg-dark-800 lg:border-l lg:border-t-0'>
<div className='grid h-full grid-cols-2 gap-3'>
<div className='rounded-xl border border-slate-200 bg-white p-4 dark:border-dark-700 dark:bg-dark-900'>
<div className='text-xs font-bold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>Catalog SKUs</div>
<div className='mt-3 text-3xl font-semibold text-slate-950 dark:text-white'>{count || 0}</div>
</div>
<div className='rounded-xl border border-slate-200 bg-white p-4 dark:border-dark-700 dark:bg-dark-900'>
<div className='text-xs font-bold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>Buyer use</div>
<div className='mt-3 text-lg font-semibold text-slate-950 dark:text-white'>Reorder + PO</div>
</div>
<div className='rounded-xl border border-slate-200 bg-white p-4 dark:border-dark-700 dark:bg-dark-900'>
<div className='text-xs font-bold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>Controls</div>
<div className='mt-3 text-lg font-semibold text-slate-950 dark:text-white'>Allergens</div>
</div>
<div className='rounded-xl border border-slate-200 bg-white p-4 dark:border-dark-700 dark:bg-dark-900'>
<div className='text-xs font-bold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>Docs</div>
<div className='mt-3 text-lg font-semibold text-slate-950 dark:text-white'>Spec sheets</div>
</div>
</div>
</div>
</div>
</div>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap items-center gap-3'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton href={'/products/products-new'} color='info' label='Add SKU'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getProductsCSV} />
<BaseButton color='info' label='Export CSV' onClick={getProductsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div className='md:inline-flex items-center md:ml-auto'>
<div id='delete-rows-button'></div>
</div>
<div className='md:inline-flex items-center ms-auto'>
<Link href={'/products/products-table'}>Switch to Table</Link>
<div className='md:inline-flex items-center'>
<Link className='font-semibold text-slate-700 underline-offset-4 hover:underline dark:text-slate-200' href={'/products/products-table'}>Table view</Link>
</div>
</CardBox>

View File

@ -25,7 +25,6 @@ const ProductsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
const { currentUser } = useAppSelector((state) => state.auth);
@ -34,9 +33,9 @@ const ProductsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'SKU', title: 'sku'},{label: 'Productname', title: 'product_name'},{label: 'Brand', title: 'brand'},{label: 'Shortdescription', title: 'short_description'},{label: 'Longdescription', title: 'long_description'},{label: 'Packsize', title: 'pack_size'},{label: 'Unitofmeasure', title: 'uom'},{label: 'Allergens', title: 'allergens'},{label: 'Certifications', title: 'certifications'},
{label: 'Unitspercase', title: 'units_per_case', number: 'true'},{label: 'MOQ(cases)', title: 'moq_cases', number: 'true'},
{label: 'Unitweight(lbs)', title: 'unit_weight_lbs', number: 'true'},{label: 'Caseweight(lbs)', title: 'case_weight_lbs', number: 'true'},
const [filters] = useState([{label: 'SKU', title: 'sku'},{label: 'Product name', title: 'product_name'},{label: 'Brand', title: 'brand'},{label: 'Short description', title: 'short_description'},{label: 'Long description', title: 'long_description'},{label: 'Pack size', title: 'pack_size'},{label: 'Unit of measure', title: 'uom'},{label: 'Allergens', title: 'allergens'},{label: 'Certifications', title: 'certifications'},
{label: 'Units per case', title: 'units_per_case', number: 'true'},{label: 'MOQ cases', title: 'moq_cases', number: 'true'},
{label: 'Unit weight lbs', title: 'unit_weight_lbs', number: 'true'},{label: 'Case weight lbs', title: 'case_weight_lbs', number: 'true'},
@ -44,7 +43,7 @@ const ProductsTablesPage = () => {
{label: 'Temperaturezone', title: 'temperature_zone', type: 'enum', options: ['ambient','refrigerated','frozen']},{label: 'Productstatus', title: 'product_status', type: 'enum', options: ['active','discontinued','seasonal','out_of_stock']},
{label: 'Temperature zone', title: 'temperature_zone', type: 'enum', options: ['ambient','refrigerated','frozen']},{label: 'Product status', title: 'product_status', type: 'enum', options: ['active','discontinued','seasonal','out_of_stock']},{label: 'Sample eligible', title: 'is_sample_eligible', type: 'enum', options: ['true','false']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PRODUCTS');
@ -93,34 +92,33 @@ const ProductsTablesPage = () => {
<title>{getPageTitle('Products')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Products" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Catalog table" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap items-center gap-3'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/products/products-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton href={'/products/products-new'} color='info' label='Add SKU'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getProductsCSV} />
<BaseButton color='info' label='Export CSV' onClick={getProductsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}
<div className='md:inline-flex items-center ms-auto'>
<div className='md:inline-flex items-center md:ml-auto'>
<div id='delete-rows-button'></div>
<Link href={'/products/products-list'}>
Back to <span className='capitalize'>card</span>
<Link className='font-semibold text-slate-700 underline-offset-4 hover:underline dark:text-slate-200' href={'/products/products-list'}>
Card catalog
</Link>
</div>

View File

@ -1,180 +1,481 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import { uniqueId } from 'lodash';
import React, { ReactElement, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableSample_requests from '../../components/Sample_requests/TableSample_requests'
import BaseButton from '../../components/BaseButton'
import axios from "axios";
import Link from "next/link";
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
import CardBoxModal from "../../components/CardBoxModal";
import DragDropFilePicker from "../../components/DragDropFilePicker";
import {setRefetch, uploadCsv} from '../../stores/sample_requests/sample_requestsSlice';
import * as icon from '@mdi/js';
import Head from 'next/head';
import Link from 'next/link';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import LoadingSpinner from '../../components/LoadingSpinner';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import { hasPermission } from '../../helpers/userPermissions';
import { useAppSelector } from '../../stores/hooks';
import {hasPermission} from "../../helpers/userPermissions";
type UserSummary = {
firstName?: string;
lastName?: string;
email?: string;
};
type AccountSummary = {
account_name?: string;
account_type?: string;
};
type LocationSummary = {
location_name?: string;
city?: string;
state?: string;
};
const Sample_requestsTablesPage = () => {
const [filterItems, setFilterItems] = useState([]);
const [csvFile, setCsvFile] = useState<File | null>(null);
const [isModalActive, setIsModalActive] = useState(false);
const [showTableView, setShowTableView] = useState(false);
type ProductSummary = {
sku?: string;
product_name?: string;
brand?: string;
pack_size?: string;
temperature_zone?: string;
category?: {
category_name?: string;
};
};
type SampleRequest = {
id: string;
sample_request_number: string;
sample_quantity?: number;
requested_at?: string;
needed_by?: string;
sample_status?: string;
notes?: string;
account?: AccountSummary;
location?: LocationSummary;
product?: ProductSummary;
requested_by?: UserSummary;
};
const dateFormatter = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
});
const statusOptions = [
{ label: 'All', value: 'all' },
{ label: 'Requested', value: 'requested' },
{ label: 'Approved', value: 'approved' },
{ label: 'Shipped', value: 'shipped' },
{ label: 'Delivered', value: 'delivered' },
];
const sampleFlow = [
'Requested by buyer',
'Sales review',
'Sample shipped',
'Chef feedback',
'Quote or PO follow-up',
];
const generatedSeedNames = [
'Ada Lovelace',
'Alan Turing',
'Grace Hopper',
'Marie Curie',
];
const formatDate = (value?: string) => {
if (!value) {
return 'Not set';
}
return dateFormatter.format(new Date(value));
};
const formatUserName = (user?: UserSummary) => {
if (!user) {
return 'Unassigned';
}
const name = [user.firstName, user.lastName].filter(Boolean).join(' ');
return name || user.email || 'Unassigned';
};
const isGeneratedSeedRequest = (item: SampleRequest) => {
const searchableText = [
item.sample_request_number,
item.notes,
item.account?.account_name,
item.account?.account_type,
item.location?.location_name,
item.location?.city,
item.location?.state,
item.product?.sku,
item.product?.product_name,
item.product?.brand,
item.product?.pack_size,
item.requested_by?.firstName,
item.requested_by?.lastName,
item.requested_by?.email,
]
.filter(Boolean)
.join(' ');
return generatedSeedNames.some((name) => searchableText.includes(name));
};
const humanizeStatus = (status?: string) => {
if (!status) {
return 'Unassigned';
}
return status
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
};
const getStatusClass = (status?: string) => {
if (['approved', 'delivered'].includes(status || '')) {
return 'border-emerald-200 bg-emerald-50 text-emerald-800';
}
if (status === 'shipped') {
return 'border-blue-200 bg-blue-50 text-blue-800';
}
if (status === 'requested') {
return 'border-amber-200 bg-amber-50 text-amber-800';
}
if (['declined', 'cancelled'].includes(status || '')) {
return 'border-rose-200 bg-rose-50 text-rose-800';
}
return 'border-slate-200 bg-slate-50 text-slate-700';
};
const StatusPill = ({ status }: { status?: string }) => (
<span
className={`inline-flex rounded-full border px-2.5 py-1 text-xs font-bold ${getStatusClass(
status,
)}`}
>
{humanizeStatus(status)}
</span>
);
const SampleRequestsListPage = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const [sampleRequests, setSampleRequests] = useState<SampleRequest[]>([]);
const [selectedStatus, setSelectedStatus] = useState('all');
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const dispatch = useAppDispatch();
const hasCreatePermission =
!!currentUser && hasPermission(currentUser, 'CREATE_SAMPLE_REQUESTS');
const loadSampleRequests = async () => {
setIsLoading(true);
setErrorMessage('');
const [filters] = useState([{label: 'Samplerequestnumber', title: 'sample_request_number'},{label: 'Notes', title: 'notes'},
{label: 'Samplequantity', title: 'sample_quantity', number: 'true'},
{label: 'Requestedat', title: 'requested_at', date: 'true'},{label: 'Neededby', title: 'needed_by', date: 'true'},
{label: 'Account', title: 'account'},
{label: 'Location', title: 'location'},
{label: 'Requestedby', title: 'requested_by'},
{label: 'Product', title: 'product'},
{label: 'Samplestatus', title: 'sample_status', type: 'enum', options: ['requested','approved','shipped','delivered','declined','cancelled']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_SAMPLE_REQUESTS');
try {
const { data } = await axios.get('/sample_requests', {
params: { page: 0, limit: 30 },
});
const businessRows = (data.rows || []).filter(
(item: SampleRequest) => !isGeneratedSeedRequest(item),
);
setSampleRequests(businessRows);
} catch (error) {
console.error('Failed to load sample requests', error);
setErrorMessage('Unable to load sample requests.');
} finally {
setIsLoading(false);
}
};
const addFilter = () => {
const newItem = {
id: uniqueId(),
fields: {
filterValue: '',
filterValueFrom: '',
filterValueTo: '',
selectedField: '',
},
};
newItem.fields.selectedField = filters[0].title;
setFilterItems([...filterItems, newItem]);
};
useEffect(() => {
if (!currentUser) {
return;
}
const getSample_requestsCSV = async () => {
const response = await axios({url: '/sample_requests?filetype=csv', method: 'GET',responseType: 'blob'});
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'sample_requestsCSV.csv'
link.click()
};
void loadSampleRequests();
}, [currentUser?.id]);
const onModalConfirm = async () => {
if (!csvFile) return;
await dispatch(uploadCsv(csvFile));
dispatch(setRefetch(true));
setCsvFile(null);
setIsModalActive(false);
};
const filteredRequests = useMemo(() => {
if (selectedStatus === 'all') {
return sampleRequests;
}
const onModalCancel = () => {
setCsvFile(null);
setIsModalActive(false);
};
return sampleRequests.filter(
(item) => item.sample_status === selectedStatus,
);
}, [sampleRequests, selectedStatus]);
const requestedCount = sampleRequests.filter(
(item) => item.sample_status === 'requested',
).length;
const approvedCount = sampleRequests.filter(
(item) => item.sample_status === 'approved',
).length;
const deliveredCount = sampleRequests.filter(
(item) => item.sample_status === 'delivered',
).length;
const activeCount = sampleRequests.filter(
(item) => !['delivered', 'declined', 'cancelled'].includes(item.sample_status || ''),
).length;
return (
<>
<Head>
<title>{getPageTitle('Sample_requests')}</title>
<title>{getPageTitle('Sample requests')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Sample_requests" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/sample_requests/sample_requests-new'} color='info' label='New Item'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getSample_requestsCSV} />
<SectionTitleLineWithButton
icon={icon.mdiPackageVariant}
title='Sample requests'
main
>
<div className='flex flex-wrap gap-2'>
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
onClick={() => setIsModalActive(true)}
/>
<Link
href='/sample_requests/sample_requests-new'
className='inline-flex h-10 items-center rounded-xl bg-slate-950 px-4 text-sm font-semibold text-white hover:bg-slate-800'
>
New sample
</Link>
)}
<div className='md:inline-flex items-center ms-auto'>
<div id='delete-rows-button'></div>
</div>
<div className='md:inline-flex items-center ms-auto'>
<Link href={'/sample_requests/sample_requests-table'}>Switch to Table</Link>
<Link
href='/sample_requests/sample_requests-table'
className='inline-flex h-10 items-center rounded-xl border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-700 hover:bg-slate-50'
>
Table view
</Link>
</div>
</SectionTitleLineWithButton>
<CardBox className='mb-6 border border-slate-200 bg-white shadow-sm'>
<div className='grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px]'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-amber-700'>
Chef evaluation queue
</p>
<h1 className='mt-3 text-3xl font-semibold text-slate-950'>
Track samples from buyer request to menu decision
</h1>
<p className='mt-3 max-w-3xl text-sm leading-7 text-slate-600'>
Foodservice suppliers use samples to move chefs from curiosity
to a quote, LTO, banquet menu, or recurring PO. This queue keeps
product, buyer, ship-to, timing, and tasting notes together.
</p>
</div>
<div className='grid gap-3 sm:grid-cols-2 lg:grid-cols-1'>
<div className='rounded-xl border border-amber-200 bg-amber-50 p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-amber-700'>
Active samples
</p>
<p className='mt-2 text-2xl font-semibold text-slate-950'>
{activeCount}
</p>
</div>
<div className='rounded-xl border border-emerald-200 bg-emerald-50 p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700'>
Ready for follow-up
</p>
<p className='mt-2 text-2xl font-semibold text-slate-950'>
{approvedCount + deliveredCount}
</p>
</div>
</div>
</div>
<div className='mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4'>
<div className='rounded-xl bg-slate-50 p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-slate-400'>
Total requests
</p>
<p className='mt-2 text-2xl font-semibold text-slate-950'>
{sampleRequests.length}
</p>
</div>
<div className='rounded-xl bg-amber-50 p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-amber-700'>
Requested
</p>
<p className='mt-2 text-2xl font-semibold text-slate-950'>
{requestedCount}
</p>
</div>
<div className='rounded-xl bg-emerald-50 p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-emerald-700'>
Approved
</p>
<p className='mt-2 text-2xl font-semibold text-slate-950'>
{approvedCount}
</p>
</div>
<div className='rounded-xl bg-blue-50 p-4'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-blue-700'>
Delivered
</p>
<p className='mt-2 text-2xl font-semibold text-slate-950'>
{deliveredCount}
</p>
</div>
</div>
</CardBox>
<TableSample_requests
filterItems={filterItems}
setFilterItems={setFilterItems}
filters={filters}
showGrid={false}
/>
{errorMessage && (
<div className='mb-5 rounded-2xl border border-rose-200 bg-rose-50 p-4 text-sm font-semibold text-rose-800'>
{errorMessage}
</div>
)}
<div className='mb-5 flex flex-wrap gap-2'>
{statusOptions.map((option) => (
<button
key={option.value}
type='button'
onClick={() => setSelectedStatus(option.value)}
className={`rounded-full border px-4 py-2 text-sm font-semibold transition ${
selectedStatus === option.value
? 'border-emerald-500 bg-emerald-500 text-white'
: 'border-slate-200 bg-white text-slate-700 hover:bg-slate-50'
}`}
>
{option.label}
</button>
))}
</div>
<div className='grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_340px]'>
<div className='grid gap-4 lg:grid-cols-2'>
{isLoading && (
<div className='col-span-full flex h-40 items-center justify-center rounded-2xl border border-slate-200 bg-white'>
<LoadingSpinner />
</div>
)}
{!isLoading &&
filteredRequests.map((item) => (
<Link
key={item.id}
href={`/sample_requests/sample_requests-view/?id=${item.id}`}
className='group block h-full overflow-hidden rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:-translate-y-0.5 hover:border-amber-200 hover:shadow-md'
>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='min-w-0'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-amber-700'>
{item.sample_request_number}
</p>
<h2 className='mt-2 truncate text-xl font-semibold text-slate-950'>
{item.product?.product_name || 'Sample product'}
</h2>
<p className='mt-1 text-sm text-slate-500'>
{item.product?.sku || 'No SKU'} -{' '}
{item.product?.pack_size || 'Pack pending'}
</p>
</div>
<StatusPill status={item.sample_status} />
</div>
<div className='mt-5 grid gap-3 rounded-xl bg-slate-50 p-4 text-sm text-slate-600 sm:grid-cols-2'>
<div>
<p className='text-xs uppercase tracking-[0.14em] text-slate-400'>
Buyer account
</p>
<p className='mt-1 font-semibold text-slate-950'>
{item.account?.account_name || 'Unassigned'}
</p>
</div>
<div>
<p className='text-xs uppercase tracking-[0.14em] text-slate-400'>
Needed by
</p>
<p className='mt-1 font-semibold text-slate-950'>
{formatDate(item.needed_by)}
</p>
</div>
<div>
<p className='text-xs uppercase tracking-[0.14em] text-slate-400'>
Ship-to
</p>
<p className='mt-1 font-semibold text-slate-950'>
{item.location?.location_name || 'Not selected'}
</p>
</div>
<div>
<p className='text-xs uppercase tracking-[0.14em] text-slate-400'>
Quantity
</p>
<p className='mt-1 font-semibold text-slate-950'>
{item.sample_quantity || 0} unit(s)
</p>
</div>
</div>
<p className='mt-4 line-clamp-2 text-sm leading-6 text-slate-600'>
{item.notes || 'No tasting notes yet.'}
</p>
<div className='mt-5 flex flex-col items-start gap-2 border-t border-slate-100 pt-4 text-sm sm:flex-row sm:items-center sm:justify-between sm:gap-3'>
<span className='font-semibold text-slate-500'>
Requested by {formatUserName(item.requested_by)}
</span>
<span className='font-bold text-emerald-700 group-hover:text-emerald-800'>
Review
</span>
</div>
</Link>
))}
{!isLoading && !filteredRequests.length && (
<div className='col-span-full rounded-2xl border border-dashed border-slate-300 bg-white p-8 text-center text-sm text-slate-500'>
No sample requests match this status.
</div>
)}
</div>
<aside className='rounded-2xl border border-slate-200 bg-slate-950 p-5 text-white shadow-sm'>
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-emerald-300'>
Workflow
</p>
<h3 className='mt-3 text-2xl font-semibold'>
Sample-to-order path
</h3>
<div className='mt-5 space-y-3'>
{sampleFlow.map((step, index) => (
<div
key={step}
className='flex items-center gap-3 rounded-xl border border-white/10 bg-white/5 p-3'
>
<span className='flex h-8 w-8 items-center justify-center rounded-full bg-emerald-400 text-sm font-bold text-slate-950'>
{index + 1}
</span>
<span className='text-sm font-semibold text-slate-100'>
{step}
</span>
</div>
))}
</div>
<div className='mt-5 rounded-xl border border-white/10 bg-white/5 p-4 text-sm leading-6 text-slate-300'>
A strong sample workflow gives sales and operations one place to
see what was requested, why it matters, and when the buyer needs a
decision.
</div>
</aside>
</div>
</SectionMain>
<CardBoxModal
title='Upload CSV'
buttonColor='info'
buttonLabel={'Confirm'}
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
isActive={isModalActive}
onConfirm={onModalConfirm}
onCancel={onModalCancel}
>
<DragDropFilePicker
file={csvFile}
setFile={setCsvFile}
formats={'.csv'}
/>
</CardBoxModal>
</>
)
}
);
};
Sample_requestsTablesPage.getLayout = function getLayout(page: ReactElement) {
SampleRequestsListPage.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'READ_SAMPLE_REQUESTS'}
>
{page}
</LayoutAuthenticated>
)
}
<LayoutAuthenticated permission='READ_SAMPLE_REQUESTS'>
{page}
</LayoutAuthenticated>
);
};
export default Sample_requestsTablesPage
export default SampleRequestsListPage;

View File

@ -34,10 +34,10 @@ const Sample_requestsTablesPage = () => {
const dispatch = useAppDispatch();
const [filters] = useState([{label: 'Samplerequestnumber', title: 'sample_request_number'},{label: 'Notes', title: 'notes'},
{label: 'Samplequantity', title: 'sample_quantity', number: 'true'},
const [filters] = useState([{label: 'Sample request #', title: 'sample_request_number'},{label: 'Tasting notes', title: 'notes'},
{label: 'Sample quantity', title: 'sample_quantity', number: 'true'},
{label: 'Requestedat', title: 'requested_at', date: 'true'},{label: 'Neededby', title: 'needed_by', date: 'true'},
{label: 'Requested at', title: 'requested_at', date: 'true'},{label: 'Needed by', title: 'needed_by', date: 'true'},
{label: 'Account', title: 'account'},
@ -48,7 +48,7 @@ const Sample_requestsTablesPage = () => {
{label: 'Requestedby', title: 'requested_by'},
{label: 'Requested by', title: 'requested_by'},
@ -56,7 +56,7 @@ const Sample_requestsTablesPage = () => {
{label: 'Samplestatus', title: 'sample_status', type: 'enum', options: ['requested','approved','shipped','delivered','declined','cancelled']},
{label: 'Sample status', title: 'sample_status', type: 'enum', options: ['requested','approved','shipped','delivered','declined','cancelled']},
]);
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_SAMPLE_REQUESTS');
@ -102,28 +102,28 @@ const Sample_requestsTablesPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Sample_requests')}</title>
<title>{getPageTitle('Sample request table')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Sample_requests" main>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Sample request table" main>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/sample_requests/sample_requests-new'} color='info' label='New Item'/>}
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/sample_requests/sample_requests-new'} color='info' label='New sample'/>}
<BaseButton
className={'mr-3'}
color='info'
label='Filter'
label='Add filter'
onClick={addFilter}
/>
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getSample_requestsCSV} />
<BaseButton className={'mr-3'} color='info' label='Export CSV' onClick={getSample_requestsCSV} />
{hasCreatePermission && (
<BaseButton
color='info'
label='Upload CSV'
label='Import CSV'
onClick={() => setIsModalActive(true)}
/>
)}
@ -132,7 +132,7 @@ const Sample_requestsTablesPage = () => {
<div id='delete-rows-button'></div>
<Link href={'/sample_requests/sample_requests-list'}>
Back to <span className='capitalize'>kanban</span>
Back to workflow
</Link>
</div>

File diff suppressed because it is too large Load Diff