feat: Implement core application structure including Dashboard, Orders, Login, and Profile modules.

This commit is contained in:
JuruSysadmin 2026-01-15 10:42:56 -03:00
parent 5d469f08a7
commit 02aaae0cd3
156 changed files with 22253 additions and 16957 deletions

3
.gitignore vendored
View File

@ -39,3 +39,6 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
*storybook.log
storybook-static

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80
}

20
.storybook/main.ts Normal file
View File

@ -0,0 +1,20 @@
import type { StorybookConfig } from '@storybook/nextjs-vite';
const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs",
"@storybook/addon-onboarding"
],
"framework": "@storybook/nextjs-vite",
"staticDirs": [
"..\\public"
]
};
export default config;

14
.storybook/preview.ts Normal file
View File

@ -0,0 +1,14 @@
import type { Preview } from '@storybook/nextjs-vite'
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

View File

@ -23,11 +23,13 @@ The application is designed to provide a robust and scalable frontend interface
## Installation ## Installation
1. Clone the repository: 1. Clone the repository:
```bash ```bash
git clone <repository-url> git clone <repository-url>
``` ```
2. Navigate to the project directory: 2. Navigate to the project directory:
```bash ```bash
cd portal-web-v2 cd portal-web-v2
``` ```
@ -42,7 +44,7 @@ The application is designed to provide a robust and scalable frontend interface
The following scripts are available in `package.json` for development and operations: The following scripts are available in `package.json` for development and operations:
| Script | Description | | Script | Description |
|--------|-------------| | ----------------------- | --------------------------------------------------- |
| `npm run dev` | Starts the development server with hot-reloading. | | `npm run dev` | Starts the development server with hot-reloading. |
| `npm run build` | Compiles the application for production deployment. | | `npm run build` | Compiles the application for production deployment. |
| `npm start` | Runs the compiled production build locally. | | `npm start` | Runs the compiled production build locally. |

View File

@ -3,7 +3,10 @@
import createCache from '@emotion/cache'; import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation'; import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider as DefaultCacheProvider } from '@emotion/react'; import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
import type { EmotionCache, Options as OptionsOfCreateCache } from '@emotion/cache'; import type {
EmotionCache,
Options as OptionsOfCreateCache,
} from '@emotion/cache';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import React from 'react'; import React from 'react';
@ -20,7 +23,9 @@ export type NextAppDirEmotionCacheProviderProps = {
}; };
// Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx // Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx
export default function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) { export default function NextAppDirEmotionCacheProvider(
props: NextAppDirEmotionCacheProviderProps
) {
const { options, CacheProvider = DefaultCacheProvider, children } = props; const { options, CacheProvider = DefaultCacheProvider, children } = props;
const [registry] = useState(() => { const [registry] = useState(() => {

View File

@ -8,15 +8,17 @@ import { useCustomizerStore } from '@/features/dashboard/store/useCustomizerStor
import { LicenseInfo } from '@mui/x-license'; import { LicenseInfo } from '@mui/x-license';
import { AuthInitializer } from '@/features/login/components/AuthInitializer'; import { AuthInitializer } from '@/features/login/components/AuthInitializer';
const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'; const PERPETUAL_LICENSE_KEY =
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
try { try {
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY); LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
} catch (error) { } catch (error) {
console.error('Failed to set MUI license key:', error); console.error('Failed to set MUI license key:', error);
} }
export default function Providers({
export default function Providers({ children }: Readonly<{ children: React.ReactNode }>) { children,
}: Readonly<{ children: React.ReactNode }>) {
const [queryClient] = useState(() => new QueryClient()); const [queryClient] = useState(() => new QueryClient());
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const activeMode = useCustomizerStore((state) => state.activeMode); const activeMode = useCustomizerStore((state) => state.activeMode);
@ -75,7 +77,8 @@ export default function Providers({ children }: Readonly<{ children: React.React
MuiCssBaseline: { MuiCssBaseline: {
styleOverrides: { styleOverrides: {
body: { body: {
fontFamily: "var(--font-plus-jakarta), 'Plus Jakarta Sans', sans-serif", fontFamily:
"var(--font-plus-jakarta), 'Plus Jakarta Sans', sans-serif",
}, },
}, },
}, },

View File

@ -17,13 +17,21 @@ function OrdersContent() {
export default function OrdersPageRoute() { export default function OrdersPageRoute() {
return ( return (
<Suspense fallback={ <Suspense
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> fallback={
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
}}
>
<CircularProgress /> <CircularProgress />
</Box> </Box>
}> }
>
<OrdersContent /> <OrdersContent />
</Suspense> </Suspense>
); );
} }

View File

@ -1,2 +1 @@
export { default } from '../../src/features/dashboard/pages/DashboardPage'; export { default } from '../../src/features/dashboard/pages/DashboardPage';

View File

@ -20,13 +20,21 @@ function ProfileContent() {
export default function ProfilePageRoute() { export default function ProfilePageRoute() {
return ( return (
<Suspense fallback={ <Suspense
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}> fallback={
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
}}
>
<CircularProgress /> <CircularProgress />
</Box> </Box>
}> }
>
<ProfileContent /> <ProfileContent />
</Suspense> </Suspense>
); );
} }

View File

@ -1,4 +1,4 @@
@import "tailwindcss"; @import 'tailwindcss';
:root { :root {
--background: #ffffff; --background: #ffffff;

View File

@ -1,29 +1,29 @@
import type { Metadata } from "next"; import type { Metadata } from 'next';
import { Geist, Geist_Mono, Plus_Jakarta_Sans } from "next/font/google"; import { Geist, Geist_Mono, Plus_Jakarta_Sans } from 'next/font/google';
import "./globals.css"; import './globals.css';
import Providers from "./components/Providers"; import Providers from './components/Providers';
import EmotionRegistry from "./components/EmotionRegistry"; import EmotionRegistry from './components/EmotionRegistry';
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from 'nuqs/adapters/next/app';
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: '--font-geist-sans',
subsets: ["latin"], subsets: ['latin'],
}); });
const geistMono = Geist_Mono({ const geistMono = Geist_Mono({
variable: "--font-geist-mono", variable: '--font-geist-mono',
subsets: ["latin"], subsets: ['latin'],
}); });
const plusJakartaSans = Plus_Jakarta_Sans({ const plusJakartaSans = Plus_Jakarta_Sans({
variable: "--font-plus-jakarta", variable: '--font-plus-jakarta',
subsets: ["latin"], subsets: ['latin'],
weight: ['300', '400', '500', '600', '700', '800'], weight: ['300', '400', '500', '600', '700', '800'],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Portal Jurunense - Login Page", title: 'Portal Jurunense - Login Page',
description: "Login page for Modernize dashboard", description: 'Login page for Modernize dashboard',
}; };
export default function RootLayout({ export default function RootLayout({
@ -36,7 +36,6 @@ export default function RootLayout({
<body <body
className={`${geistSans.variable} ${geistMono.variable} ${plusJakartaSans.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} ${plusJakartaSans.variable} antialiased`}
> >
<EmotionRegistry options={{ key: 'mui' }}> <EmotionRegistry options={{ key: 'mui' }}>
<NuqsAdapter> <NuqsAdapter>
<Providers>{children}</Providers> <Providers>{children}</Providers>

View File

@ -1,6 +1,5 @@
import Login from "../../src/features/login/components/LoginForm"; import Login from '../../src/features/login/components/LoginForm';
export default function LoginPage() { export default function LoginPage() {
return <Login />; return <Login />;
} }

View File

@ -1,4 +1,4 @@
import Login from "../src/features/login/components/LoginForm"; import Login from '../src/features/login/components/LoginForm';
export default function Home() { export default function Home() {
return <Login />; return <Login />;

View File

@ -1,6 +1,9 @@
import { defineConfig, globalIgnores } from "eslint/config"; // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import nextVitals from "eslint-config-next/core-web-vitals"; import storybook from "eslint-plugin-storybook";
import nextTs from "eslint-config-next/typescript";
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTs from 'eslint-config-next/typescript';
const eslintConfig = defineConfig([ const eslintConfig = defineConfig([
...nextVitals, ...nextVitals,
@ -8,10 +11,10 @@ const eslintConfig = defineConfig([
// Override default ignores of eslint-config-next. // Override default ignores of eslint-config-next.
globalIgnores([ globalIgnores([
// Default ignores of eslint-config-next: // Default ignores of eslint-config-next:
".next/**", '.next/**',
"out/**", 'out/**',
"build/**", 'build/**',
"next-env.d.ts", 'next-env.d.ts',
]), ]),
]); ]);

View File

@ -22,7 +22,7 @@ jest.mock('next/navigation', () => ({
// Mock window.matchMedia // Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,
value: jest.fn().mockImplementation(query => ({ value: jest.fn().mockImplementation((query) => ({
matches: false, matches: false,
media: query, media: query,
onchange: null, onchange: null,

View File

@ -1,8 +1,9 @@
import { LicenseInfo } from '@mui/x-license'; import { LicenseInfo } from '@mui/x-license';
const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'; const PERPETUAL_LICENSE_KEY =
const ALTERNATIVE_LICENSE_KEY = '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI='; 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
const ALTERNATIVE_LICENSE_KEY =
'61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=';
try { try {
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY); LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);

View File

@ -1,18 +1,18 @@
import type { NextConfig } from "next"; import type { NextConfig } from 'next';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
// ... suas outras configurações (como rewrites) // ... suas outras configurações (como rewrites)
allowedDevOrigins: ["portalconsulta.jurunense.com"], allowedDevOrigins: ['portalconsulta.jurunense.com'],
transpilePackages: ['@mui/material', '@emotion/react', '@emotion/styled'], transpilePackages: ['@mui/material', '@emotion/react', '@emotion/styled'],
async rewrites() { async rewrites() {
return [ return [
{ {
source: "/api/auth/:path*", source: '/api/auth/:path*',
destination: "https://api.auth.jurunense.com/api/v1/:path*", destination: 'https://api.auth.jurunense.com/api/v1/:path*',
}, },
{ {
source: "/api/report-viewer/:path*", source: '/api/report-viewer/:path*',
destination: "http://10.1.1.205:8068/Viewer/:path*", destination: 'http://10.1.1.205:8068/Viewer/:path*',
}, },
]; ];
}, },

3016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,9 @@
"lint": "eslint", "lint": "eslint",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:coverage": "jest --coverage" "test:coverage": "jest --coverage",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}, },
"dependencies": { "dependencies": {
"@emotion/cache": "^11.14.0", "@emotion/cache": "^11.14.0",
@ -42,6 +44,7 @@
"next": "16.1.1", "next": "16.1.1",
"next-auth": "latest", "next-auth": "latest",
"nuqs": "^2.8.6", "nuqs": "^2.8.6",
"prettier": "^3.7.4",
"react": "19.2.3", "react": "19.2.3",
"react-big-calendar": "^1.19.4", "react-big-calendar": "^1.19.4",
"react-dom": "19.2.3", "react-dom": "19.2.3",
@ -64,6 +67,19 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.1", "eslint-config-next": "16.1.1",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5",
"storybook": "^10.1.11",
"@storybook/nextjs-vite": "^10.1.11",
"@chromatic-com/storybook": "^5.0.0",
"@storybook/addon-vitest": "^10.1.11",
"@storybook/addon-a11y": "^10.1.11",
"@storybook/addon-docs": "^10.1.11",
"@storybook/addon-onboarding": "^10.1.11",
"vite": "^7.3.1",
"eslint-plugin-storybook": "^10.1.11",
"vitest": "^4.0.17",
"playwright": "^1.57.0",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17"
} }
} }

View File

@ -1,6 +1,6 @@
const config = { const config = {
plugins: { plugins: {
"@tailwindcss/postcss": {}, '@tailwindcss/postcss': {},
}, },
}; };

View File

@ -28,7 +28,6 @@ const StyledMain = styled(Box, {
}), }),
})); }));
export default function DashboardLayout({ children }: DashboardLayoutProps) { export default function DashboardLayout({ children }: DashboardLayoutProps) {
const theme = useTheme(); const theme = useTheme();
const lgUp = useMediaQuery(theme.breakpoints.up('lg')); const lgUp = useMediaQuery(theme.breakpoints.up('lg'));
@ -37,10 +36,15 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
if (!isHydrated) { if (!isHydrated) {
return ( return (
<Box sx={{ display: 'flex', height: '100vh' }}> <Box sx={{ display: 'flex', height: '100vh' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}> <Box
<StyledMain isMobile={!lgUp}> sx={{
{children} display: 'flex',
</StyledMain> flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
}}
>
<StyledMain isMobile={!lgUp}>{children}</StyledMain>
</Box> </Box>
</Box> </Box>
); );
@ -49,13 +53,17 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
return ( return (
<Box sx={{ display: 'flex', height: '100vh' }}> <Box sx={{ display: 'flex', height: '100vh' }}>
<Sidebar /> <Sidebar />
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
}}
>
<Header /> <Header />
<StyledMain isMobile={!lgUp}> <StyledMain isMobile={!lgUp}>{children}</StyledMain>
{children}
</StyledMain>
</Box> </Box>
</Box> </Box>
); );
} }

View File

@ -37,4 +37,3 @@ export default function DashboardOverview() {
</Grid> </Grid>
); );
} }

View File

@ -13,4 +13,3 @@ export default function Logo() {
</Link> </Link>
); );
} }

View File

@ -1,12 +1,12 @@
import SimpleBar from "simplebar-react"; import SimpleBar from 'simplebar-react';
import "simplebar-react/dist/simplebar.min.css"; import 'simplebar-react/dist/simplebar.min.css';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { SxProps } from '@mui/system'; import { SxProps } from '@mui/system';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { useMediaQuery } from "@mui/material"; import { useMediaQuery } from '@mui/material';
const SimpleBarStyle = styled(SimpleBar)(() => ({ const SimpleBarStyle = styled(SimpleBar)(() => ({
maxHeight: "100%", maxHeight: '100%',
})); }));
interface PropsType { interface PropsType {
@ -19,7 +19,7 @@ const Scrollbar = (props: PropsType) => {
const lgDown = useMediaQuery((theme: any) => theme.breakpoints.down('lg')); const lgDown = useMediaQuery((theme: any) => theme.breakpoints.down('lg'));
if (lgDown) { if (lgDown) {
return <Box sx={{ overflowX: "auto" }}>{children}</Box>; return <Box sx={{ overflowX: 'auto' }}>{children}</Box>;
} }
return ( return (
@ -30,4 +30,3 @@ const Scrollbar = (props: PropsType) => {
}; };
export default Scrollbar; export default Scrollbar;

View File

@ -1,46 +1,46 @@
'use client'; 'use client';
import AppBar from "@mui/material/AppBar"; import AppBar from '@mui/material/AppBar';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import IconButton from "@mui/material/IconButton"; import IconButton from '@mui/material/IconButton';
import Stack from "@mui/material/Stack"; import Stack from '@mui/material/Stack';
import Toolbar from "@mui/material/Toolbar"; import Toolbar from '@mui/material/Toolbar';
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from '@mui/material/useMediaQuery';
import { styled, Theme } from "@mui/material/styles"; import { styled, Theme } from '@mui/material/styles';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import DarkModeIcon from '@mui/icons-material/DarkMode'; import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode'; import LightModeIcon from '@mui/icons-material/LightMode';
import { useCustomizerStore } from "../store/useCustomizerStore"; import { useCustomizerStore } from '../store/useCustomizerStore';
import Notifications from "./Notification"; import Notifications from './Notification';
import Profile from "./Profile"; import Profile from './Profile';
import Search from "./Search"; import Search from './Search';
import Navigation from "./Navigation"; import Navigation from './Navigation';
import MobileRightSidebar from "./MobileRightSidebar"; import MobileRightSidebar from './MobileRightSidebar';
const AppBarStyled = styled(AppBar)(({ theme }) => ({ const AppBarStyled = styled(AppBar)(({ theme }) => ({
boxShadow: "none", boxShadow: 'none',
background: theme.palette.background.paper, background: theme.palette.background.paper,
justifyContent: "center", justifyContent: 'center',
backdropFilter: "blur(4px)", backdropFilter: 'blur(4px)',
zIndex: theme.zIndex.drawer + 1, zIndex: theme.zIndex.drawer + 1,
})); }));
const ToolbarStyled = styled(Toolbar)(({ theme }) => ({ const ToolbarStyled = styled(Toolbar)(({ theme }) => ({
width: "100%", width: '100%',
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
})); }));
const Header = () => { const Header = () => {
const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up("lg")); const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down("lg")); const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'));
const { const {
activeMode, activeMode,
toggleSidebar, toggleSidebar,
toggleMobileSidebar, toggleMobileSidebar,
setDarkMode, setDarkMode,
isHydrated isHydrated,
} = useCustomizerStore(); } = useCustomizerStore();
if (!isHydrated) { if (!isHydrated) {
@ -70,21 +70,18 @@ const Header = () => {
<Box flexGrow={1} /> <Box flexGrow={1} />
<Stack spacing={1} direction="row" alignItems="center"> <Stack spacing={1} direction="row" alignItems="center">
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{/* Theme Toggle (Dark/Light) */} {/* Theme Toggle (Dark/Light) */}
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
<IconButton <IconButton
size="large" size="large"
color="inherit" color="inherit"
onClick={() => setDarkMode(activeMode === "light" ? "dark" : "light")} onClick={() =>
setDarkMode(activeMode === 'light' ? 'dark' : 'light')
}
aria-label="alternar tema" aria-label="alternar tema"
> >
{activeMode === "light" ? ( {activeMode === 'light' ? <DarkModeIcon /> : <LightModeIcon />}
<DarkModeIcon />
) : (
<LightModeIcon />
)}
</IconButton> </IconButton>
<Notifications /> <Notifications />

View File

@ -85,8 +85,7 @@ const MobileRightSidebar = () => {
</List> </List>
</Box> </Box>
<Box px={3} mt={3}> <Box px={3} mt={3}></Box>
</Box>
</Box> </Box>
); );

View File

@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from 'react';
import { Box, Menu, Typography, Button, Divider, Grid } from "@mui/material"; import { Box, Menu, Typography, Button, Divider, Grid } from '@mui/material';
import Link from "next/link"; import Link from 'next/link';
import { IconChevronDown, IconHelp } from "@tabler/icons-react"; import { IconChevronDown, IconHelp } from '@tabler/icons-react';
import AppLinks from "./AppLinks"; import AppLinks from './AppLinks';
const AppDD = () => { const AppDD = () => {
const [anchorEl2, setAnchorEl2] = useState(null); const [anchorEl2, setAnchorEl2] = useState(null);
@ -25,17 +25,17 @@ const AppDD = () => {
aria-controls="msgs-menu" aria-controls="msgs-menu"
aria-haspopup="true" aria-haspopup="true"
sx={{ sx={{
bgcolor: anchorEl2 ? "primary.light" : "", bgcolor: anchorEl2 ? 'primary.light' : '',
color: anchorEl2 color: anchorEl2
? "primary.main" ? 'primary.main'
: (theme) => theme.palette.text.secondary, : (theme) => theme.palette.text.secondary,
fontSize: "13px", fontSize: '13px',
}} }}
onClick={handleClick2} onClick={handleClick2}
endIcon={ endIcon={
<IconChevronDown <IconChevronDown
size="15" size="15"
style={{ marginLeft: "-5px", marginTop: "2px" }} style={{ marginLeft: '-5px', marginTop: '2px' }}
/> />
} }
> >
@ -50,13 +50,13 @@ const AppDD = () => {
keepMounted keepMounted
open={Boolean(anchorEl2)} open={Boolean(anchorEl2)}
onClose={handleClose2} onClose={handleClose2}
anchorOrigin={{ horizontal: "left", vertical: "bottom" }} anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
transformOrigin={{ horizontal: "left", vertical: "top" }} transformOrigin={{ horizontal: 'left', vertical: 'top' }}
sx={{ sx={{
"& .MuiMenu-paper": { '& .MuiMenu-paper': {
width: "850px", width: '850px',
}, },
"& .MuiMenu-paper ul": { '& .MuiMenu-paper ul': {
p: 0, p: 0,
}, },
}} }}
@ -69,8 +69,8 @@ const AppDD = () => {
<Box <Box
sx={{ sx={{
display: { display: {
xs: "none", xs: 'none',
sm: "flex", sm: 'flex',
}, },
}} }}
alignItems="center" alignItems="center"
@ -99,15 +99,17 @@ const AppDD = () => {
<Divider orientation="vertical" /> <Divider orientation="vertical" />
</Grid> </Grid>
<Grid size={{ sm: 4 }}> <Grid size={{ sm: 4 }}>
<Box p={4}> <Box p={4}></Box>
</Box>
</Grid> </Grid>
</Grid> </Grid>
</Menu> </Menu>
</Box> </Box>
<Button <Button
color="inherit" color="inherit"
sx={{ color: (theme) => theme.palette.text.secondary, fontSize: "13px" }} sx={{
color: (theme) => theme.palette.text.secondary,
fontSize: '13px',
}}
variant="text" variant="text"
href="/apps/chats" href="/apps/chats"
component={Link} component={Link}
@ -116,7 +118,10 @@ const AppDD = () => {
</Button> </Button>
<Button <Button
color="inherit" color="inherit"
sx={{ color: (theme) => theme.palette.text.secondary, fontSize: "13px" }} sx={{
color: (theme) => theme.palette.text.secondary,
fontSize: '13px',
}}
variant="text" variant="text"
href="/apps/email" href="/apps/email"
component={Link} component={Link}

View File

@ -11,7 +11,7 @@ import {
Chip, Chip,
} from '@mui/material'; } from '@mui/material';
import * as dropdownData from './data'; import * as dropdownData from './data';
import Scrollbar from '../components/Scrollbar'; import { Scrollbar } from '@/shared/components';
import { IconBellRinging } from '@tabler/icons-react'; import { IconBellRinging } from '@tabler/icons-react';
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
@ -62,7 +62,13 @@ const Notifications = () => {
}, },
}} }}
> >
<Stack direction="row" py={2} px={4} justifyContent="space-between" alignItems="center"> <Stack
direction="row"
py={2}
px={4}
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">Notifications</Typography> <Typography variant="h6">Notifications</Typography>
<Chip label="5 new" color="primary" size="small" /> <Chip label="5 new" color="primary" size="small" />
</Stack> </Stack>
@ -108,7 +114,13 @@ const Notifications = () => {
))} ))}
</Scrollbar> </Scrollbar>
<Box p={3} pb={1}> <Box p={3} pb={1}>
<Button href="/apps/email" variant="outlined" component={Link} color="primary" fullWidth> <Button
href="/apps/email"
variant="outlined"
component={Link}
color="primary"
fullWidth
>
See all Notifications See all Notifications
</Button> </Button>
</Box> </Box>

View File

@ -15,7 +15,6 @@ import { IconMail } from '@tabler/icons-react';
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
import Image from 'next/image'; import Image from 'next/image';
const Profile = () => { const Profile = () => {
const [anchorEl2, setAnchorEl2] = useState(null); const [anchorEl2, setAnchorEl2] = useState(null);
const handleClick2 = (event: any) => { const handleClick2 = (event: any) => {
@ -40,7 +39,7 @@ const Profile = () => {
onClick={handleClick2} onClick={handleClick2}
> >
<Avatar <Avatar
src={"/images/profile/user-1.jpg"} src={'/images/profile/user-1.jpg'}
alt={'ProfileImg'} alt={'ProfileImg'}
sx={{ sx={{
width: 35, width: 35,
@ -68,9 +67,17 @@ const Profile = () => {
> >
<Typography variant="h5">User Profile</Typography> <Typography variant="h5">User Profile</Typography>
<Stack direction="row" py={3} spacing={2} alignItems="center"> <Stack direction="row" py={3} spacing={2} alignItems="center">
<Avatar src={"/images/profile/user-1.jpg"} alt={"ProfileImg"} sx={{ width: 95, height: 95 }} /> <Avatar
src={'/images/profile/user-1.jpg'}
alt={'ProfileImg'}
sx={{ width: 95, height: 95 }}
/>
<Box> <Box>
<Typography variant="subtitle2" color="textPrimary" fontWeight={600}> <Typography
variant="subtitle2"
color="textPrimary"
fontWeight={600}
>
Mathew Anderson Mathew Anderson
</Typography> </Typography>
<Typography variant="subtitle2" color="textSecondary"> <Typography variant="subtitle2" color="textSecondary">
@ -100,7 +107,8 @@ const Profile = () => {
bgcolor="primary.light" bgcolor="primary.light"
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="center" flexShrink="0" justifyContent="center"
flexShrink="0"
> >
<Avatar <Avatar
src={profile.icon} src={profile.icon}
@ -142,7 +150,13 @@ const Profile = () => {
</Box> </Box>
))} ))}
<Box mt={2}> <Box mt={2}>
<Box bgcolor="primary.light" p={3} mb={3} overflow="hidden" position="relative"> <Box
bgcolor="primary.light"
p={3}
mb={3}
overflow="hidden"
position="relative"
>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Box> <Box>
<Typography variant="h5" mb={2}> <Typography variant="h5" mb={2}>
@ -153,10 +167,23 @@ const Profile = () => {
Upgrade Upgrade
</Button> </Button>
</Box> </Box>
<Image src={"/images/backgrounds/unlimited-bg.png"} width={150} height={183} style={{ height: 'auto', width: 'auto' }} alt="unlimited" className="signup-bg" /> <Image
src={'/images/backgrounds/unlimited-bg.png'}
width={150}
height={183}
style={{ height: 'auto', width: 'auto' }}
alt="unlimited"
className="signup-bg"
/>
</Box> </Box>
</Box> </Box>
<Button href="/auth/auth1/login" variant="outlined" color="primary" component={Link} fullWidth> <Button
href="/auth/auth1/login"
variant="outlined"
color="primary"
component={Link}
fullWidth
>
Logout Logout
</Button> </Button>
</Box> </Box>

View File

@ -36,7 +36,9 @@ const Search = () => {
const filterRoutes = (rotr: any, cSearch: string) => { const filterRoutes = (rotr: any, cSearch: string) => {
if (rotr.length > 1) if (rotr.length > 1)
return rotr.filter((t: any) => return rotr.filter((t: any) =>
t.title ? t.href.toLocaleLowerCase().includes(cSearch.toLocaleLowerCase()) : '', t.title
? t.href.toLocaleLowerCase().includes(cSearch.toLocaleLowerCase())
: ''
); );
return rotr; return rotr;
@ -89,7 +91,11 @@ const Search = () => {
return ( return (
<Box key={menu.title ? menu.id : menu.subheader}> <Box key={menu.title ? menu.id : menu.subheader}>
{menu.title && !menu.children ? ( {menu.title && !menu.children ? (
<ListItemButton sx={{ py: 0.5, px: 1 }} href={menu?.href} component={Link}> <ListItemButton
sx={{ py: 0.5, px: 1 }}
href={menu?.href}
component={Link}
>
<ListItemText <ListItemText
primary={menu.title} primary={menu.title}
secondary={menu?.href} secondary={menu?.href}

View File

@ -10,51 +10,51 @@ interface NotificationType {
const notifications: NotificationType[] = [ const notifications: NotificationType[] = [
{ {
id: '1', id: '1',
avatar: "/images/profile/user-2.jpg", avatar: '/images/profile/user-2.jpg',
title: "Roman Joined the Team!", title: 'Roman Joined the Team!',
subtitle: "Congratulate him", subtitle: 'Congratulate him',
}, },
{ {
id: '2', id: '2',
avatar: "/images/profile/user-3.jpg", avatar: '/images/profile/user-3.jpg',
title: "New message received", title: 'New message received',
subtitle: "Salma sent you new message", subtitle: 'Salma sent you new message',
}, },
{ {
id: '3', id: '3',
avatar: "/images/profile/user-4.jpg", avatar: '/images/profile/user-4.jpg',
title: "New Payment received", title: 'New Payment received',
subtitle: "Check your earnings", subtitle: 'Check your earnings',
}, },
{ {
id: '4', id: '4',
avatar: "/images/profile/user-5.jpg", avatar: '/images/profile/user-5.jpg',
title: "Jolly completed tasks", title: 'Jolly completed tasks',
subtitle: "Assign her new tasks", subtitle: 'Assign her new tasks',
}, },
{ {
id: '5', id: '5',
avatar: "/images/profile/user-6.jpg", avatar: '/images/profile/user-6.jpg',
title: "Roman Joined the Team!", title: 'Roman Joined the Team!',
subtitle: "Congratulate him", subtitle: 'Congratulate him',
}, },
{ {
id: '6', id: '6',
avatar: "/images/profile/user-7.jpg", avatar: '/images/profile/user-7.jpg',
title: "New message received", title: 'New message received',
subtitle: "Salma sent you new message", subtitle: 'Salma sent you new message',
}, },
{ {
id: '7', id: '7',
avatar: "/images/profile/user-8.jpg", avatar: '/images/profile/user-8.jpg',
title: "New Payment received", title: 'New Payment received',
subtitle: "Check your earnings", subtitle: 'Check your earnings',
}, },
{ {
id: '8', id: '8',
avatar: "/images/profile/user-9.jpg", avatar: '/images/profile/user-9.jpg',
title: "Jolly completed tasks", title: 'Jolly completed tasks',
subtitle: "Assign her new tasks", subtitle: 'Assign her new tasks',
}, },
]; ];
@ -69,22 +69,22 @@ interface ProfileType {
} }
const profile: ProfileType[] = [ const profile: ProfileType[] = [
{ {
href: "/dashboard/profile", href: '/dashboard/profile',
title: "My Profile", title: 'My Profile',
subtitle: "Account Settings", subtitle: 'Account Settings',
icon: "/images/svgs/icon-account.svg", icon: '/images/svgs/icon-account.svg',
}, },
{ {
href: "/apps/email", href: '/apps/email',
title: "My Inbox", title: 'My Inbox',
subtitle: "Messages & Emails", subtitle: 'Messages & Emails',
icon: "/images/svgs/icon-inbox.svg", icon: '/images/svgs/icon-inbox.svg',
}, },
{ {
href: "/apps/notes", href: '/apps/notes',
title: "My Tasks", title: 'My Tasks',
subtitle: "To-do and Daily Tasks", subtitle: 'To-do and Daily Tasks',
icon: "/images/svgs/icon-tasks.svg", icon: '/images/svgs/icon-tasks.svg',
}, },
]; ];
@ -99,46 +99,46 @@ interface AppsLinkType {
const appsLink: AppsLinkType[] = [ const appsLink: AppsLinkType[] = [
{ {
href: "/apps/chats", href: '/apps/chats',
title: "Chat Application", title: 'Chat Application',
subtext: "New messages arrived", subtext: 'New messages arrived',
avatar: "/images/svgs/icon-dd-chat.svg", avatar: '/images/svgs/icon-dd-chat.svg',
}, },
{ {
href: "/apps/ecommerce/shop", href: '/apps/ecommerce/shop',
title: "eCommerce App", title: 'eCommerce App',
subtext: "New stock available", subtext: 'New stock available',
avatar: "/images/svgs/icon-dd-cart.svg", avatar: '/images/svgs/icon-dd-cart.svg',
}, },
{ {
href: "/apps/notes", href: '/apps/notes',
title: "Notes App", title: 'Notes App',
subtext: "To-do and Daily tasks", subtext: 'To-do and Daily tasks',
avatar: "/images/svgs/icon-dd-invoice.svg", avatar: '/images/svgs/icon-dd-invoice.svg',
}, },
{ {
href: "/apps/contacts", href: '/apps/contacts',
title: "Contact Application", title: 'Contact Application',
subtext: "2 Unsaved Contacts", subtext: '2 Unsaved Contacts',
avatar: "/images/svgs/icon-dd-mobile.svg", avatar: '/images/svgs/icon-dd-mobile.svg',
}, },
{ {
href: "/apps/tickets", href: '/apps/tickets',
title: "Tickets App", title: 'Tickets App',
subtext: "Submit tickets", subtext: 'Submit tickets',
avatar: "/images/svgs/icon-dd-lifebuoy.svg", avatar: '/images/svgs/icon-dd-lifebuoy.svg',
}, },
{ {
href: "/apps/email", href: '/apps/email',
title: "Email App", title: 'Email App',
subtext: "Get new emails", subtext: 'Get new emails',
avatar: "/images/svgs/icon-dd-message-box.svg", avatar: '/images/svgs/icon-dd-message-box.svg',
}, },
{ {
href: "/apps/blog/post", href: '/apps/blog/post',
title: "Blog App", title: 'Blog App',
subtext: "added new blog", subtext: 'added new blog',
avatar: "/images/svgs/icon-dd-application.svg", avatar: '/images/svgs/icon-dd-application.svg',
}, },
]; ];
@ -149,36 +149,36 @@ interface LinkType {
const pageLinks: LinkType[] = [ const pageLinks: LinkType[] = [
{ {
href: "/theme-pages/pricing", href: '/theme-pages/pricing',
title: "Pricing Page", title: 'Pricing Page',
}, },
{ {
href: "/auth/auth1/login", href: '/auth/auth1/login',
title: "Authentication Design", title: 'Authentication Design',
}, },
{ {
href: "/auth/auth1/register", href: '/auth/auth1/register',
title: "Register Now", title: 'Register Now',
}, },
{ {
href: "/404", href: '/404',
title: "404 Error Page", title: '404 Error Page',
}, },
{ {
href: "/apps/note", href: '/apps/note',
title: "Notes App", title: 'Notes App',
}, },
{ {
href: "/apps/user-profile/profile", href: '/apps/user-profile/profile',
title: "User Application", title: 'User Application',
}, },
{ {
href: "/apps/blog/post", href: '/apps/blog/post',
title: "Blog Design", title: 'Blog Design',
}, },
{ {
href: "/apps/ecommerce/checkout", href: '/apps/ecommerce/checkout',
title: "Shopping Cart", title: 'Shopping Cart',
}, },
]; ];

View File

@ -1,3 +1,2 @@
export { default as Dashboard } from './components/Dashboard'; export { default as Dashboard } from './components/Dashboard';
export { default as DashboardPage } from './pages/DashboardPage'; export { default as DashboardPage } from './pages/DashboardPage';

View File

@ -8,4 +8,3 @@ export default function DashboardPage() {
</DashboardLayout> </DashboardLayout>
); );
} }

View File

@ -36,4 +36,3 @@ const Menuitems: MenuItemType[] = [
]; ];
export default Menuitems; export default Menuitems;

View File

@ -16,7 +16,7 @@ import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import SidebarItems from './SidebarItems'; import SidebarItems from './SidebarItems';
import Scrollbar from '../components/Scrollbar'; import { Scrollbar } from '@/shared/components';
import { useCustomizerStore } from '../store/useCustomizerStore'; import { useCustomizerStore } from '../store/useCustomizerStore';
// Mixins para transições // Mixins para transições
@ -44,8 +44,18 @@ interface StyledDrawerProps {
} }
const StyledDrawer = styled(Drawer, { const StyledDrawer = styled(Drawer, {
shouldForwardProp: (prop) => prop !== 'isCollapsed' && prop !== 'isMobile' && prop !== 'sidebarWidth' && prop !== 'miniSidebarWidth', shouldForwardProp: (prop) =>
})<StyledDrawerProps & { sidebarWidth?: number; miniSidebarWidth?: number }>(({ theme, isCollapsed, isMobile, sidebarWidth = 270, miniSidebarWidth = 87 }) => { prop !== 'isCollapsed' &&
prop !== 'isMobile' &&
prop !== 'sidebarWidth' &&
prop !== 'miniSidebarWidth',
})<StyledDrawerProps & { sidebarWidth?: number; miniSidebarWidth?: number }>(({
theme,
isCollapsed,
isMobile,
sidebarWidth = 270,
miniSidebarWidth = 87,
}) => {
const desktopWidth = isCollapsed ? miniSidebarWidth : sidebarWidth; const desktopWidth = isCollapsed ? miniSidebarWidth : sidebarWidth;
const drawerWidth = isMobile ? sidebarWidth : desktopWidth; const drawerWidth = isMobile ? sidebarWidth : desktopWidth;
@ -62,7 +72,9 @@ const StyledDrawer = styled(Drawer, {
}), }),
...(!isMobile && { ...(!isMobile && {
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': {
...(isCollapsed ? closedMixin(theme, miniSidebarWidth) : openedMixin(theme, sidebarWidth)), ...(isCollapsed
? closedMixin(theme, miniSidebarWidth)
: openedMixin(theme, sidebarWidth)),
boxSizing: 'border-box', boxSizing: 'border-box',
}, },
}), }),
@ -106,7 +118,6 @@ const StyledProfileBox = styled(Box, {
justifyContent: isCollapsed ? 'center' : 'flex-start', justifyContent: isCollapsed ? 'center' : 'flex-start',
})); }));
const StyledAvatar = styled(Avatar)(({ theme }) => ({ const StyledAvatar = styled(Avatar)(({ theme }) => ({
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
})); }));
@ -154,7 +165,7 @@ export default function Sidebar() {
toggleMobileSidebar, toggleMobileSidebar,
isHydrated, isHydrated,
sidebarWidth, sidebarWidth,
miniSidebarWidth miniSidebarWidth,
} = useCustomizerStore(); } = useCustomizerStore();
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const { logout } = useAuth(); const { logout } = useAuth();
@ -227,7 +238,11 @@ export default function Sidebar() {
<StyledProfileContainer> <StyledProfileContainer>
<StyledProfileBox isCollapsed={isCollapse && lgUp}> <StyledProfileBox isCollapsed={isCollapse && lgUp}>
<Tooltip <Tooltip
title={isCollapse && lgUp ? user?.nome || user?.userName || 'Usuário' : ''} title={
isCollapse && lgUp
? user?.nome || user?.userName || 'Usuário'
: ''
}
placement="right" placement="right"
> >
<StyledAvatar> <StyledAvatar>
@ -240,7 +255,10 @@ export default function Sidebar() {
noWrap noWrap
fontWeight={600} fontWeight={600}
sx={{ sx={{
color: (theme) => theme.palette.mode === 'dark' ? theme.palette.text.primary : '#1a1a1a', color: (theme) =>
theme.palette.mode === 'dark'
? theme.palette.text.primary
: '#1a1a1a',
}} }}
> >
{user?.nome || user?.userName || 'Usuário'} {user?.nome || user?.userName || 'Usuário'}
@ -249,7 +267,10 @@ export default function Sidebar() {
variant="caption" variant="caption"
noWrap noWrap
sx={{ sx={{
color: (theme) => theme.palette.mode === 'dark' ? theme.palette.text.secondary : '#4a5568', color: (theme) =>
theme.palette.mode === 'dark'
? theme.palette.text.secondary
: '#4a5568',
}} }}
> >
{user?.nomeFilial || 'Usuário'} {user?.nomeFilial || 'Usuário'}

View File

@ -43,7 +43,9 @@ const SidebarItems = ({ open, onItemClick }: SidebarItemsProps) => {
{Menuitems.map((item) => { {Menuitems.map((item) => {
// SubHeader // SubHeader
if (item.subheader) { if (item.subheader) {
return <NavGroup item={item} hideMenu={hideMenu} key={item.subheader} />; return (
<NavGroup item={item} hideMenu={hideMenu} key={item.subheader} />
);
// If Sub Menu // If Sub Menu
} else if (item.children) { } else if (item.children) {

View File

@ -31,8 +31,13 @@ interface StyledListItemButtonProps {
} }
const StyledListItemButton = styled(ListItemButton, { const StyledListItemButton = styled(ListItemButton, {
shouldForwardProp: (prop) => prop !== 'open' && prop !== 'active' && prop !== 'level' && prop !== 'hideMenu', shouldForwardProp: (prop) =>
})<StyledListItemButtonProps>(({ theme, open, active, level = 1, hideMenu }) => ({ prop !== 'open' &&
prop !== 'active' &&
prop !== 'level' &&
prop !== 'hideMenu',
})<StyledListItemButtonProps>(
({ theme, open, active, level = 1, hideMenu }) => ({
marginBottom: '2px', marginBottom: '2px',
padding: '8px 10px', padding: '8px 10px',
paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px', paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px',
@ -40,7 +45,8 @@ const StyledListItemButton = styled(ListItemButton, {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
borderRadius: '7px', borderRadius: '7px',
'&:hover': { '&:hover': {
backgroundColor: active || open backgroundColor:
active || open
? theme.palette.primary.main ? theme.palette.primary.main
: theme.palette.primary.light, : theme.palette.primary.light,
color: active || open ? 'white' : theme.palette.primary.main, color: active || open ? 'white' : theme.palette.primary.main,
@ -51,7 +57,8 @@ const StyledListItemButton = styled(ListItemButton, {
: level > 1 && open : level > 1 && open
? theme.palette.primary.main ? theme.palette.primary.main
: theme.palette.text.secondary, : theme.palette.text.secondary,
})); })
);
const StyledListItemIcon = styled(ListItemIcon)(() => ({ const StyledListItemIcon = styled(ListItemIcon)(() => ({
minWidth: '36px', minWidth: '36px',
@ -128,9 +135,7 @@ export default function NavCollapse({
hideMenu={hideMenu} hideMenu={hideMenu}
key={menu?.id} key={menu?.id}
> >
<StyledListItemIcon> <StyledListItemIcon>{menuIcon}</StyledListItemIcon>
{menuIcon}
</StyledListItemIcon>
<ListItemText color="inherit"> <ListItemText color="inherit">
{hideMenu ? '' : <>{menu.title}</>} {hideMenu ? '' : <>{menu.title}</>}
</ListItemText> </ListItemText>

View File

@ -31,4 +31,3 @@ export default function NavGroup({ item, hideMenu }: NavGroupProps) {
</ListSubheaderStyle> </ListSubheaderStyle>
); );
} }

View File

@ -28,14 +28,18 @@ interface StyledListItemButtonProps {
} }
const StyledListItemButton = styled(ListItemButton, { const StyledListItemButton = styled(ListItemButton, {
shouldForwardProp: (prop) => prop !== 'active' && prop !== 'level' && prop !== 'hideMenu', shouldForwardProp: (prop) =>
prop !== 'active' && prop !== 'level' && prop !== 'hideMenu',
})<StyledListItemButtonProps>(({ theme, active, level = 1, hideMenu }) => ({ })<StyledListItemButtonProps>(({ theme, active, level = 1, hideMenu }) => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
marginBottom: '2px', marginBottom: '2px',
padding: '8px 10px', padding: '8px 10px',
borderRadius: '7px', borderRadius: '7px',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit', backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
color: active && level > 1 ? `${theme.palette.primary.main}!important` : theme.palette.text.secondary, color:
active && level > 1
? `${theme.palette.primary.main}!important`
: theme.palette.text.secondary,
paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px', paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px',
'&:hover': { '&:hover': {
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
@ -61,7 +65,8 @@ const StyledListItemIcon = styled(ListItemIcon, {
})<StyledListItemIconProps>(({ theme, active, level = 1 }) => ({ })<StyledListItemIconProps>(({ theme, active, level = 1 }) => ({
minWidth: '36px', minWidth: '36px',
padding: '3px 0', padding: '3px 0',
color: active && level > 1 ? `${theme.palette.primary.main}!important` : 'inherit', color:
active && level > 1 ? `${theme.palette.primary.main}!important` : 'inherit',
})); }));
export default function NavItem({ export default function NavItem({

View File

@ -14,4 +14,3 @@ export interface MenuItemType {
disabled?: boolean; disabled?: boolean;
subtitle?: string; subtitle?: string;
} }

View File

@ -27,12 +27,14 @@ export const useCustomizerStore = create<CustomizerState>()(
setDarkMode: (mode) => set({ activeMode: mode }), setDarkMode: (mode) => set({ activeMode: mode }),
toggleSidebar: () => set((state) => ({ toggleSidebar: () =>
isCollapse: !state.isCollapse set((state) => ({
isCollapse: !state.isCollapse,
})), })),
toggleMobileSidebar: () => set((state) => ({ toggleMobileSidebar: () =>
isMobileSidebar: !state.isMobileSidebar set((state) => ({
isMobileSidebar: !state.isMobileSidebar,
})), })),
setHydrated: () => set({ isHydrated: true }), setHydrated: () => set({ isHydrated: true }),
@ -42,7 +44,7 @@ export const useCustomizerStore = create<CustomizerState>()(
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ partialize: (state) => ({
activeMode: state.activeMode, activeMode: state.activeMode,
isCollapse: state.isCollapse isCollapse: state.isCollapse,
}), }),
onRehydrateStorage: () => (state) => { onRehydrateStorage: () => (state) => {
state?.setHydrated(); state?.setHydrated();

View File

@ -1,4 +1,8 @@
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import axios, {
AxiosError,
AxiosInstance,
InternalAxiosRequestConfig,
} from 'axios';
import { getAccessToken, handleTokenRefresh } from './utils/tokenRefresh'; import { getAccessToken, handleTokenRefresh } from './utils/tokenRefresh';
const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL; const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL;
@ -24,7 +28,9 @@ const addToken = (config: InternalAxiosRequestConfig) => {
authApi.interceptors.request.use(addToken); authApi.interceptors.request.use(addToken);
const handleResponseError = async (error: AxiosError) => { const handleResponseError = async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
if (!originalRequest) { if (!originalRequest) {
throw error; throw error;
@ -34,4 +40,3 @@ const handleResponseError = async (error: AxiosError) => {
}; };
authApi.interceptors.response.use((response) => response, handleResponseError); authApi.interceptors.response.use((response) => response, handleResponseError);

View File

@ -15,9 +15,7 @@ const createWrapper = () => {
}); });
return ({ children }: { children: React.ReactNode }) => ( return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
{children}
</QueryClientProvider>
); );
}; };
@ -43,7 +41,9 @@ describe('AuthLogin Component', () => {
expect(screen.getByLabelText(/usuário/i)).toBeInTheDocument(); expect(screen.getByLabelText(/usuário/i)).toBeInTheDocument();
expect(screen.getByLabelText(/senha/i)).toBeInTheDocument(); expect(screen.getByLabelText(/senha/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument(); expect(
screen.getByRole('button', { name: /sign in/i })
).toBeInTheDocument();
}); });
it('deve renderizar título quando fornecido', () => { it('deve renderizar título quando fornecido', () => {
@ -87,7 +87,9 @@ describe('AuthLogin Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/senha deve ter no mínimo 4 caracteres/i)).toBeInTheDocument(); expect(
screen.getByText(/senha deve ter no mínimo 4 caracteres/i)
).toBeInTheDocument();
}); });
}); });
}); });
@ -170,7 +172,9 @@ describe('AuthLogin Component', () => {
render(<AuthLogin />, { wrapper: createWrapper() }); render(<AuthLogin />, { wrapper: createWrapper() });
// ❌ ESTE TESTE VAI FALHAR - erro ainda aparece durante loading! // ❌ ESTE TESTE VAI FALHAR - erro ainda aparece durante loading!
expect(screen.queryByText(/credenciais inválidas/i)).not.toBeInTheDocument(); expect(
screen.queryByText(/credenciais inválidas/i)
).not.toBeInTheDocument();
}); });
}); });
@ -189,7 +193,9 @@ describe('AuthLogin Component', () => {
it('🐛 BUG: checkbox "Manter-me conectado" deve ser controlado', () => { it('🐛 BUG: checkbox "Manter-me conectado" deve ser controlado', () => {
render(<AuthLogin />, { wrapper: createWrapper() }); render(<AuthLogin />, { wrapper: createWrapper() });
const checkbox = screen.getByRole('checkbox', { name: /manter-me conectado/i }); const checkbox = screen.getByRole('checkbox', {
name: /manter-me conectado/i,
});
// Checkbox está sempre marcado // Checkbox está sempre marcado
expect(checkbox).toBeChecked(); expect(checkbox).toBeChecked();

View File

@ -1,20 +1,20 @@
"use client" 'use client';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Divider from "@mui/material/Divider"; import Divider from '@mui/material/Divider';
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from "@mui/material/FormGroup"; import FormGroup from '@mui/material/FormGroup';
import Stack from "@mui/material/Stack"; import Stack from '@mui/material/Stack';
import { TextField, Alert } from "@mui/material"; import { TextField, Alert } from '@mui/material';
import Typography from "@mui/material/Typography"; import Typography from '@mui/material/Typography';
import NextLink from "next/link"; import NextLink from 'next/link';
import { useForm } from "react-hook-form"; import { useForm } from 'react-hook-form';
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema, LoginInput, AuthLoginProps } from "../interfaces/types"; import { loginSchema, LoginInput, AuthLoginProps } from '../interfaces/types';
import { useAuth } from "../hooks/useAuth"; import { useAuth } from '../hooks/useAuth';
import CustomCheckbox from "../components/forms/theme-elements/CustomCheckbox"; import CustomCheckbox from '../components/forms/theme-elements/CustomCheckbox';
import CustomFormLabel from "../components/forms/theme-elements/CustomFormLabel"; import CustomFormLabel from '../components/forms/theme-elements/CustomFormLabel';
import AuthSocialButtons from "./AuthSocialButtons"; import AuthSocialButtons from './AuthSocialButtons';
const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => { const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
const { loginMutation } = useAuth(); const { loginMutation } = useAuth();
@ -26,8 +26,8 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
} = useForm<LoginInput>({ } = useForm<LoginInput>({
resolver: zodResolver(loginSchema), resolver: zodResolver(loginSchema),
defaultValues: { defaultValues: {
username: "", username: '',
password: "", password: '',
}, },
}); });
@ -67,8 +67,12 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
{(() => { {(() => {
const error = loginMutation.error; const error = loginMutation.error;
if (error && typeof error === 'object' && 'response' in error) { if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: { message?: string } } }; const axiosError = error as {
return axiosError.response?.data?.message || 'Erro ao realizar login'; response?: { data?: { message?: string } };
};
return (
axiosError.response?.data?.message || 'Erro ao realizar login'
);
} }
return 'Erro ao realizar login'; return 'Erro ao realizar login';
})()} })()}
@ -119,11 +123,14 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
<Typography <Typography
fontWeight="500" fontWeight="500"
sx={{ sx={{
textDecoration: "none", textDecoration: 'none',
color: "primary.main", color: 'primary.main',
}} }}
> >
<NextLink href="/" style={{ textDecoration: 'none', color: 'inherit' }}> <NextLink
href="/"
style={{ textDecoration: 'none', color: 'inherit' }}
>
Esqueceu sua senha ? Esqueceu sua senha ?
</NextLink> </NextLink>
</Typography> </Typography>

View File

@ -1,6 +1,6 @@
"use client" 'use client';
import CustomSocialButton from "../components/forms/theme-elements/CustomSocialButton"; import CustomSocialButton from '../components/forms/theme-elements/CustomSocialButton';
import { Stack } from "@mui/system"; import { Stack } from '@mui/system';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
@ -18,11 +18,20 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
}; };
return ( return (
<> <>
<Stack direction="row" justifyContent="center" spacing={2} mt={3} flexWrap="wrap"> <Stack
<CustomSocialButton onClick={handleGoogleSignIn} sx={{ flex: 1, minWidth: '140px' }}> direction="row"
justifyContent="center"
spacing={2}
mt={3}
flexWrap="wrap"
>
<CustomSocialButton
onClick={handleGoogleSignIn}
sx={{ flex: 1, minWidth: '140px' }}
>
<Avatar <Avatar
src={"/images/svgs/google-icon.svg"} src={'/images/svgs/google-icon.svg'}
alt={"icon1"} alt={'icon1'}
sx={{ sx={{
width: 16, width: 16,
height: 16, height: 16,
@ -32,18 +41,21 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
/> />
<Box <Box
sx={{ sx={{
display: { xs: "none", sm: "flex" }, display: { xs: 'none', sm: 'flex' },
whiteSpace: "nowrap", whiteSpace: 'nowrap',
mr: { sm: "3px" }, mr: { sm: '3px' },
}} }}
> >
Google Google
</Box> </Box>
</CustomSocialButton> </CustomSocialButton>
<CustomSocialButton onClick={handleGithubSignIn} sx={{ flex: 1, minWidth: '140px' }}> <CustomSocialButton
onClick={handleGithubSignIn}
sx={{ flex: 1, minWidth: '140px' }}
>
<Avatar <Avatar
src={"/images/svgs/git-icon.svg"} src={'/images/svgs/git-icon.svg'}
alt={"icon2"} alt={'icon2'}
sx={{ sx={{
width: 16, width: 16,
height: 16, height: 16,
@ -53,9 +65,9 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
/> />
<Box <Box
sx={{ sx={{
display: { xs: "none", sm: "flex" }, display: { xs: 'none', sm: 'flex' },
whiteSpace: "nowrap", whiteSpace: 'nowrap',
mr: { sm: "3px" }, mr: { sm: '3px' },
}} }}
> >
GitHub GitHub
@ -64,6 +76,6 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
</Stack> </Stack>
</> </>
); );
} };
export default AuthSocialButtons; export default AuthSocialButtons;

View File

@ -17,7 +17,6 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) {
const validateSession = async () => { const validateSession = async () => {
try { try {
await loginService.refreshToken(); await loginService.refreshToken();
const profile = await profileService.getMe(); const profile = await profileService.getMe();

View File

@ -12,7 +12,8 @@ import AuthLogin from '../authForms/AuthLogin';
const GradientGrid = styled(Grid)(({ theme }) => ({ const GradientGrid = styled(Grid)(({ theme }) => ({
position: 'relative', position: 'relative',
backgroundColor: '#d2f1df', backgroundColor: '#d2f1df',
backgroundImage: 'linear-gradient(45deg, #d2f1df 0%, #d3d7fa 50%, #bad8f4 100%)', backgroundImage:
'linear-gradient(45deg, #d2f1df 0%, #d3d7fa 50%, #bad8f4 100%)',
backgroundSize: '400% 400%', backgroundSize: '400% 400%',
backgroundPosition: '0% 50%', backgroundPosition: '0% 50%',
animation: 'gradient 15s ease infinite', animation: 'gradient 15s ease infinite',
@ -25,7 +26,12 @@ const GradientGrid = styled(Grid)(({ theme }) => ({
export default function Login() { export default function Login() {
return ( return (
<PageContainer title="Login" description="Página de Login"> <PageContainer title="Login" description="Página de Login">
<Grid container spacing={0} justifyContent="center" sx={{ height: '100vh', width: '100%' }}> <Grid
container
spacing={0}
justifyContent="center"
sx={{ height: '100vh', width: '100%' }}
>
<GradientGrid size={{ xs: 12, lg: 7, xl: 8 }}> <GradientGrid size={{ xs: 12, lg: 7, xl: 8 }}>
<Box <Box
display="flex" display="flex"
@ -54,7 +60,12 @@ export default function Login() {
</Box> </Box>
</GradientGrid> </GradientGrid>
<Grid size={{ xs: 12, lg: 5, xl: 4 }}> <Grid size={{ xs: 12, lg: 5, xl: 4 }}>
<Box display="flex" justifyContent="center" alignItems="center" sx={{ backgroundColor: 'white' }}> <Box
display="flex"
justifyContent="center"
alignItems="center"
sx={{ backgroundColor: 'white' }}
>
<Box p={4} sx={{ width: '100%', maxWidth: '450px' }}> <Box p={4} sx={{ width: '100%', maxWidth: '450px' }}>
<AuthLogin <AuthLogin
subtitle={ subtitle={
@ -66,7 +77,14 @@ export default function Login() {
color: 'primary.main', color: 'primary.main',
}} }}
> >
<NextLink href="/auth/auth1/register" style={{ textDecoration: 'none', color: 'inherit', whiteSpace: 'nowrap' }}> <NextLink
href="/auth/auth1/register"
style={{
textDecoration: 'none',
color: 'inherit',
whiteSpace: 'nowrap',
}}
>
Criar uma conta Criar uma conta
</NextLink> </NextLink>
</Typography> </Typography>
@ -78,5 +96,5 @@ export default function Login() {
</Grid> </Grid>
</Grid> </Grid>
</PageContainer> </PageContainer>
) );
}; }

View File

@ -6,10 +6,6 @@ type Props = {
title?: string; title?: string;
}; };
const PageContainer = ({ children }: Props) => ( const PageContainer = ({ children }: Props) => <div>{children}</div>;
<div>
{children}
</div>
);
export default PageContainer; export default PageContainer;

View File

@ -1,12 +1,12 @@
import SimpleBar from "simplebar-react"; import SimpleBar from 'simplebar-react';
import "simplebar-react/dist/simplebar.min.css"; import 'simplebar-react/dist/simplebar.min.css';
import Box from '@mui/material/Box' import Box from '@mui/material/Box';
import { SxProps } from '@mui/system'; import { SxProps } from '@mui/system';
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles';
import { useMediaQuery } from "@mui/material"; import { useMediaQuery } from '@mui/material';
const SimpleBarStyle = styled(SimpleBar)(() => ({ const SimpleBarStyle = styled(SimpleBar)(() => ({
maxHeight: "100%", maxHeight: '100%',
})); }));
interface PropsType { interface PropsType {
@ -19,7 +19,7 @@ const Scrollbar = (props: PropsType) => {
const lgDown = useMediaQuery((theme: any) => theme.breakpoints.down('lg')); const lgDown = useMediaQuery((theme: any) => theme.breakpoints.down('lg'));
if (lgDown) { if (lgDown) {
return <Box sx={{ overflowX: "auto" }}>{children}</Box>; return <Box sx={{ overflowX: 'auto' }}>{children}</Box>;
} }
return ( return (

View File

@ -31,7 +31,10 @@ const DashboardCard = ({
return ( return (
<Card <Card
sx={{ padding: 0, border: !isCardShadow ? `1px solid ${borderColor}` : 'none' }} sx={{
padding: 0,
border: !isCardShadow ? `1px solid ${borderColor}` : 'none',
}}
elevation={isCardShadow ? 9 : 0} elevation={isCardShadow ? 9 : 0}
variant={!isCardShadow ? 'outlined' : undefined} variant={!isCardShadow ? 'outlined' : undefined}
> >
@ -43,7 +46,7 @@ const DashboardCard = ({
</Typography> </Typography>
</CardContent> </CardContent>
) : ( ) : (
<CardContent sx={{p: "30px"}}> <CardContent sx={{ p: '30px' }}>
{title ? ( {title ? (
<Stack <Stack
direction="row" direction="row"

View File

@ -1,7 +1,11 @@
import { useEffect, ReactElement } from 'react'; import { useEffect, ReactElement } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
export default function ScrollToTop({ children }: { children: ReactElement | null }) { export default function ScrollToTop({
children,
}: {
children: ReactElement | null;
}) {
const { pathname } = useRouter(); const { pathname } = useRouter();
useEffect(() => { useEffect(() => {

View File

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { styled } from "@mui/system"; import { styled } from '@mui/system';
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox'; import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
const BpIcon = styled('span')(({ theme }) => ({ const BpIcon = styled('span')(({ theme }) => ({
@ -21,7 +21,10 @@ const BpIcon = styled('span')(({ theme }) => ({
outlineOffset: 2, outlineOffset: 2,
}, },
'input:hover ~ &': { 'input:hover ~ &': {
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary : theme.palette.primary, backgroundColor:
theme.palette.mode === 'dark'
? theme.palette.primary
: theme.palette.primary,
}, },
'input:disabled ~ &': { 'input:disabled ~ &': {
boxShadow: 'none', boxShadow: 'none',
@ -54,7 +57,9 @@ function CustomCheckbox(props: CheckboxProps) {
checkedIcon={ checkedIcon={
<BpCheckedIcon <BpCheckedIcon
sx={{ sx={{
backgroundColor: props.color ? `${props.color}.main` : 'primary.main', backgroundColor: props.color
? `${props.color}.main`
: 'primary.main',
}} }}
/> />
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { styled } from "@mui/system"; import { styled } from '@mui/system';
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
const CustomFormLabel = styled((props: any) => ( const CustomFormLabel = styled((props: any) => (

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { styled } from "@mui/system"; import { styled } from '@mui/system';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
const CustomSocialButton = styled((props: any) => ( const CustomSocialButton = styled((props: any) => (

View File

@ -2,7 +2,8 @@ import React from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { TextField } from '@mui/material'; import { TextField } from '@mui/material';
const CustomTextField = styled((props: any) => <TextField {...props} />)(({ theme }) => ({ const CustomTextField = styled((props: any) => <TextField {...props} />)(
({ theme }) => ({
'& .MuiOutlinedInput-input::-webkit-input-placeholder': { '& .MuiOutlinedInput-input::-webkit-input-placeholder': {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
opacity: '0.8', opacity: '0.8',
@ -14,6 +15,7 @@ const CustomTextField = styled((props: any) => <TextField {...props} />)(({ them
'& .Mui-disabled .MuiOutlinedInput-notchedOutline': { '& .Mui-disabled .MuiOutlinedInput-notchedOutline': {
borderColor: theme.palette.grey[200], borderColor: theme.palette.grey[200],
}, },
})); })
);
export default CustomTextField; export default CustomTextField;

View File

@ -30,8 +30,7 @@ refreshToken
: UUID do refresh token (HttpOnly, Secure, 7 dias) : UUID do refresh token (HttpOnly, Secure, 7 dias)
Erros Comuns Erros Comuns
400 Bad Request: Campos obrigatórios ausentes. 400 Bad Request: Campos obrigatórios ausentes.
401/500: Usuário ou senha inválidos. 401/500: Usuário ou senha inválidos. 2. Renovar Token (Refresh)
2. Renovar Token (Refresh)
Gera um novo par de tokens usando um Refresh Token válido. Gera um novo par de tokens usando um Refresh Token válido.
Método: POST Método: POST
@ -57,8 +56,7 @@ Retorna novos tokens e atualiza o cookie.
"username": "usuario.sistema" "username": "usuario.sistema"
} }
Erros Comuns Erros Comuns
403 Forbidden: Token expirado ou inválido. 403 Forbidden: Token expirado ou inválido. 3. Obter Usuário Atual (Me)
3. Obter Usuário Atual (Me)
Retorna dados detalhados do usuário logado. Retorna dados detalhados do usuário logado.
Método: GET Método: GET
@ -79,8 +77,7 @@ Response (200 OK)
} }
Erros Comuns Erros Comuns
401 Unauthorized: Token não enviado ou inválido. 401 Unauthorized: Token não enviado ou inválido.
404 Not Found: Usuário não encontrado no banco. 404 Not Found: Usuário não encontrado no banco. 4. Logout
4. Logout
Invalida a sessão atual. Invalida a sessão atual.
Método: POST Método: POST

View File

@ -30,7 +30,8 @@ export function useAuth() {
}, },
}); });
const useMe = () => useQuery({ const useMe = () =>
useQuery({
queryKey: ['auth-me'], queryKey: ['auth-me'],
queryFn: async () => { queryFn: async () => {
const data = await profileService.getMe(); const data = await profileService.getMe();

View File

@ -0,0 +1,11 @@
// Login feature barrel export
export { default as LoginForm } from './components/LoginForm';
export { AuthInitializer } from './components/AuthInitializer';
export { useAuth } from './hooks/useAuth';
export { useAuthStore } from './store/useAuthStore';
export type {
LoginInput,
TokenResponse,
AuthState,
AuthLoginProps,
} from './interfaces/types';

View File

@ -2,9 +2,17 @@ import { z } from 'zod';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { UserProfile } from '../../profile/types'; import type { UserProfile } from '../../profile/types';
import { loginSchema, tokenResponseSchema, logoutResponseSchema } from '../schemas/schemas'; import {
loginSchema,
tokenResponseSchema,
logoutResponseSchema,
} from '../schemas/schemas';
export { loginSchema, tokenResponseSchema, logoutResponseSchema } from '../schemas/schemas'; export {
loginSchema,
tokenResponseSchema,
logoutResponseSchema,
} from '../schemas/schemas';
export type LoginInput = z.infer<typeof loginSchema>; export type LoginInput = z.infer<typeof loginSchema>;
export type TokenResponse = z.infer<typeof tokenResponseSchema>; export type TokenResponse = z.infer<typeof tokenResponseSchema>;

View File

@ -3,12 +3,11 @@ import {
LoginInput, LoginInput,
TokenResponse, TokenResponse,
LogoutResponse, LogoutResponse,
tokenResponseSchema tokenResponseSchema,
} from '../interfaces/types'; } from '../interfaces/types';
import { setTemporaryToken } from '../utils/tokenRefresh'; import { setTemporaryToken } from '../utils/tokenRefresh';
import { useAuthStore } from '../store/useAuthStore'; import { useAuthStore } from '../store/useAuthStore';
const handleAuthSuccess = async (data: any): Promise<TokenResponse> => { const handleAuthSuccess = async (data: any): Promise<TokenResponse> => {
// 1. Valida o schema do token // 1. Valida o schema do token
const validatedData = tokenResponseSchema.parse(data); const validatedData = tokenResponseSchema.parse(data);

View File

@ -31,8 +31,7 @@ export const useAuthStore = create<AuthState>()(
* Valida sincronização entre token em memória e estado persistido * Valida sincronização entre token em memória e estado persistido
* Chamado automaticamente ao recarregar a página (onRehydrateStorage) * Chamado automaticamente ao recarregar a página (onRehydrateStorage)
*/ */
hydrate: () => { hydrate: () => {},
},
}), }),
{ {
name: 'auth-storage', name: 'auth-storage',

View File

@ -1,6 +1,5 @@
import { UserProfile } from '../../profile/types'; import { UserProfile } from '../../profile/types';
export const mapToSafeProfile = (data: any): UserProfile => { export const mapToSafeProfile = (data: any): UserProfile => {
return { return {
matricula: data.matricula, matricula: data.matricula,

View File

@ -1,4 +1,9 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import axios, {
AxiosError,
AxiosInstance,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import { TokenResponse } from '../interfaces/types'; import { TokenResponse } from '../interfaces/types';
const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL; const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL;
@ -62,7 +67,10 @@ export function handleTokenRefresh<T = unknown>(
throw error; throw error;
} }
if (request.url?.includes('/auth/login') || request.url?.includes('/auth/refresh')) { if (
request.url?.includes('/auth/login') ||
request.url?.includes('/auth/refresh')
) {
throw error; throw error;
} }

View File

@ -1,5 +1,12 @@
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import axios, {
import { getAccessToken, handleTokenRefresh } from '../../login/utils/tokenRefresh'; AxiosError,
AxiosInstance,
InternalAxiosRequestConfig,
} from 'axios';
import {
getAccessToken,
handleTokenRefresh,
} from '../../login/utils/tokenRefresh';
import { import {
OrderFilters, OrderFilters,
orderApiParamsSchema, orderApiParamsSchema,
@ -8,9 +15,18 @@ import {
storesResponseSchema, storesResponseSchema,
customersResponseSchema, customersResponseSchema,
sellersResponseSchema, sellersResponseSchema,
unwrapApiData unwrapApiData,
} from '../schemas/order.schema'; } from '../schemas/order.schema';
import { orderItemsResponseSchema, OrderItem } from '../schemas/order.item.schema'; import {
orderItemsResponseSchema,
OrderItem,
shipmentResponseSchema,
Shipment,
cargoMovementResponseSchema,
CargoMovement,
cuttingItemResponseSchema,
CuttingItem,
} from '../schemas/order.item.schema';
import { Store } from '../schemas/store.schema'; import { Store } from '../schemas/store.schema';
import { Seller } from '../schemas/seller.schema'; import { Seller } from '../schemas/seller.schema';
import { Order } from '../types'; import { Order } from '../types';
@ -53,7 +69,9 @@ ordersApi.interceptors.request.use(addToken);
* @throws {AxiosError} Se não houver configuração de requisição original disponível * @throws {AxiosError} Se não houver configuração de requisição original disponível
*/ */
const handleResponseError = async (error: AxiosError) => { const handleResponseError = async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
if (!originalRequest) { if (!originalRequest) {
throw error; throw error;
@ -62,7 +80,10 @@ const handleResponseError = async (error: AxiosError) => {
return handleTokenRefresh(error, originalRequest, ordersApi); return handleTokenRefresh(error, originalRequest, ordersApi);
}; };
ordersApi.interceptors.response.use((response) => response, handleResponseError); ordersApi.interceptors.response.use(
(response) => response,
handleResponseError
);
export const orderService = { export const orderService = {
/** /**
@ -75,7 +96,9 @@ export const orderService = {
findOrders: async (filters: OrderFilters): Promise<Order[]> => { findOrders: async (filters: OrderFilters): Promise<Order[]> => {
try { try {
const cleanParams = orderApiParamsSchema.parse(filters); const cleanParams = orderApiParamsSchema.parse(filters);
const response = await ordersApi.get('/api/v1/orders/find', { params: cleanParams }); const response = await ordersApi.get('/api/v1/orders/find', {
params: cleanParams,
});
return unwrapApiData(response, ordersResponseSchema, []); return unwrapApiData(response, ordersResponseSchema, []);
} catch (error) { } catch (error) {
console.error('Erro ao buscar pedidos:', error); console.error('Erro ao buscar pedidos:', error);
@ -121,10 +144,14 @@ export const orderService = {
* @param {string} name - O nome do cliente a ser buscado (mínimo 2 caracteres) * @param {string} name - O nome do cliente a ser buscado (mínimo 2 caracteres)
* @returns {Promise<Array<{id: number, name: string, estcob: string}>>} Array de clientes correspondentes com os campos id, name e estcob * @returns {Promise<Array<{id: number, name: string, estcob: string}>>} Array de clientes correspondentes com os campos id, name e estcob
*/ */
findCustomers: async (name: string): Promise<Array<{ id: number; name: string; estcob: string }>> => { findCustomers: async (
name: string
): Promise<Array<{ id: number; name: string; estcob: string }>> => {
if (!name || name.trim().length < 2) return []; if (!name || name.trim().length < 2) return [];
try { try {
const response = await ordersApi.get(`/api/v1/clientes/${encodeURIComponent(name)}`); const response = await ordersApi.get(
`/api/v1/clientes/${encodeURIComponent(name)}`
);
return unwrapApiData(response, customersResponseSchema, []); return unwrapApiData(response, customersResponseSchema, []);
} catch (error) { } catch (error) {
console.error('Erro ao buscar clientes:', error); console.error('Erro ao buscar clientes:', error);
@ -151,4 +178,49 @@ export const orderService = {
return []; return [];
} }
}, },
findDelivery: async (
orderId: number,
includeCompletedDeliveries: boolean = true
): Promise<Shipment[]> => {
try {
const response = await ordersApi.get(
`/api/v1/orders/delivery/${orderId}`,
{
params: { includeCompletedDeliveries },
}
);
return unwrapApiData(response, shipmentResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar entregas do pedido ${orderId}:`, error);
return [];
}
},
findCargoMovement: async (orderId: number): Promise<CargoMovement[]> => {
try {
const response = await ordersApi.get(
`/api/v1/orders/transfer/${orderId}`
);
return unwrapApiData(response, cargoMovementResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar movimentação de carga do pedido ${orderId}:`, error);
return [];
}
},
findCuttingItems: async (orderId: number): Promise<CuttingItem[]> => {
try {
const response = await ordersApi.get(
`/api/v1/orders/cut-itens/${orderId}`
);
return unwrapApiData(response, cuttingItemResponseSchema, []);
} catch (error) {
console.error(`Erro ao buscar itens de corte do pedido ${orderId}:`, error);
return [];
}
},
}; };

View File

@ -1,7 +1,14 @@
'use client'; 'use client';
import TablePagination from '@mui/material/TablePagination'; import TablePagination from '@mui/material/TablePagination';
import { useGridApiContext, useGridSelector, gridPageCountSelector, gridPageSelector, gridPageSizeSelector, gridRowCountSelector } from '@mui/x-data-grid-premium'; import {
useGridApiContext,
useGridSelector,
gridPageCountSelector,
gridPageSelector,
gridPageSizeSelector,
gridRowCountSelector,
} from '@mui/x-data-grid-premium';
function CustomPagination() { function CustomPagination() {
const apiRef = useGridApiContext(); const apiRef = useGridApiContext();
@ -10,7 +17,15 @@ function CustomPagination() {
const pageSize = useGridSelector(apiRef, gridPageSizeSelector); const pageSize = useGridSelector(apiRef, gridPageSizeSelector);
const rowCount = useGridSelector(apiRef, gridRowCountSelector); const rowCount = useGridSelector(apiRef, gridRowCountSelector);
const labelDisplayedRows = ({ from, to, count }: { from: number; to: number; count: number }) => { const labelDisplayedRows = ({
from,
to,
count,
}: {
from: number;
to: number;
count: number;
}) => {
const currentPage = page + 1; const currentPage = page + 1;
const displayCount = count === -1 ? `mais de ${to}` : count; const displayCount = count === -1 ? `mais de ${to}` : count;
return `${from}${to} de ${displayCount} | Página ${currentPage} de ${pageCount}`; return `${from}${to} de ${displayCount} | Página ${currentPage} de ${pageCount}`;
@ -23,7 +38,9 @@ function CustomPagination() {
page={page} page={page}
onPageChange={(event, newPage) => apiRef.current.setPage(newPage)} onPageChange={(event, newPage) => apiRef.current.setPage(newPage)}
rowsPerPage={pageSize} rowsPerPage={pageSize}
onRowsPerPageChange={(event) => apiRef.current.setPageSize(Number.parseInt(event.target.value, 10))} onRowsPerPageChange={(event) =>
apiRef.current.setPageSize(Number.parseInt(event.target.value, 10))
}
labelRowsPerPage="Pedidos por página:" labelRowsPerPage="Pedidos por página:"
labelDisplayedRows={labelDisplayedRows} labelDisplayedRows={labelDisplayedRows}
/> />

View File

@ -13,7 +13,7 @@ import LocalShippingIcon from '@mui/icons-material/LocalShipping';
import MoveToInboxIcon from '@mui/icons-material/MoveToInbox'; import MoveToInboxIcon from '@mui/icons-material/MoveToInbox';
import TvIcon from '@mui/icons-material/Tv'; import TvIcon from '@mui/icons-material/Tv';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import { TabPanel } from './TabPanel'; import { TabPanel } from '@/shared/components';
import { OrderItemsTable } from './tabs/OrderItemsTable'; import { OrderItemsTable } from './tabs/OrderItemsTable';
import { PreBoxPanel } from './tabs/PreBoxPanel'; import { PreBoxPanel } from './tabs/PreBoxPanel';
import { InformationPanel } from './tabs/InformationPanel'; import { InformationPanel } from './tabs/InformationPanel';

View File

@ -11,7 +11,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
>
{params.value} {params.value}
</Typography> </Typography>
), ),
@ -23,7 +27,12 @@ export const createOrderItemsColumns = (): GridColDef[] => [
minWidth: 250, minWidth: 250,
flex: 1, flex: 1,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem' }} noWrap> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem' }}
noWrap
>
{params.value} {params.value}
</Typography> </Typography>
), ),
@ -36,7 +45,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
headerAlign: 'center', headerAlign: 'center',
align: 'center', align: 'center',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'center' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', textAlign: 'center' }}
>
{params.value || '-'} {params.value || '-'}
</Typography> </Typography>
), ),
@ -49,7 +62,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
headerAlign: 'center', headerAlign: 'center',
align: 'center', align: 'center',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'center' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', textAlign: 'center' }}
>
{params.value || '-'} {params.value || '-'}
</Typography> </Typography>
), ),
@ -62,7 +79,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value} {params.value}
</Typography> </Typography>
), ),
@ -78,7 +99,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
align: 'right', align: 'right',
valueFormatter: (value) => formatNumber(value as number), valueFormatter: (value) => formatNumber(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
>
{formatNumber(params.value)} {formatNumber(params.value)}
</Typography> </Typography>
), ),
@ -93,7 +118,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
align: 'right', align: 'right',
valueFormatter: (value) => formatCurrency(value as number), valueFormatter: (value) => formatCurrency(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{formatCurrency(params.value)} {formatCurrency(params.value)}
</Typography> </Typography>
), ),
@ -109,7 +138,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
align: 'right', align: 'right',
valueFormatter: (value) => formatCurrency(value as number), valueFormatter: (value) => formatCurrency(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', fontWeight: 600, textAlign: 'right' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', fontWeight: 600, textAlign: 'right' }}
>
{formatCurrency(params.value)} {formatCurrency(params.value)}
</Typography> </Typography>
), ),
@ -120,7 +153,12 @@ export const createOrderItemsColumns = (): GridColDef[] => [
width: 140, width: 140,
minWidth: 130, minWidth: 130,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem' }} noWrap> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem' }}
noWrap
>
{params.value} {params.value}
</Typography> </Typography>
), ),
@ -136,7 +174,11 @@ export const createOrderItemsColumns = (): GridColDef[] => [
align: 'right', align: 'right',
valueFormatter: (value) => formatNumber(value as number), valueFormatter: (value) => formatNumber(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
color="text.primary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{formatNumber(params.value)} {formatNumber(params.value)}
</Typography> </Typography>
), ),

View File

@ -0,0 +1,381 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import { DataGridPremium } from '@mui/x-data-grid-premium';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { LicenseInfo } from '@mui/x-license';
// Mock license for DataGrid Premium (for Storybook demo only)
LicenseInfo.setLicenseKey(
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'
);
// Mock data que simula os dados normalizados
const mockOrders = [
{
id: '1',
orderId: 123456,
createDate: '2026-01-15T10:00:00',
customerName: 'Maria Silva',
status: 'Faturado',
orderType: 'Venda',
amount: 1250.5,
invoiceNumber: 'NF-001234',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 15.5,
fatUserName: 'Admin',
deliveryLocal: 'São Paulo - SP',
masterDeliveryLocal: 'Região Sul',
deliveryPriority: 'Alta',
paymentName: 'Boleto',
partnerName: 'Parceiro ABC',
codusur2Name: 'Rep. Regional',
releaseUserName: 'Supervisor',
driver: 'Carlos Motorista',
carDescription: 'Sprinter',
carrier: 'Transportadora XYZ',
schedulerDelivery: '15/01/2026',
fatUserDescription: 'Faturamento',
emitenteNome: 'Empresa LTDA',
},
{
id: '2',
orderId: 123457,
createDate: '2026-01-14T14:30:00',
customerName: 'João Santos',
status: 'Pendente',
orderType: 'Venda',
amount: 3450.0,
invoiceNumber: '',
sellerName: 'Ana Vendedora',
deliveryType: 'Retirada',
totalWeight: 8.2,
fatUserName: '',
deliveryLocal: 'Rio de Janeiro - RJ',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Normal',
paymentName: 'Cartão de Crédito',
partnerName: '',
codusur2Name: '',
releaseUserName: '',
driver: '',
carDescription: '',
carrier: '',
schedulerDelivery: '',
fatUserDescription: '',
emitenteNome: 'Empresa LTDA',
},
{
id: '3',
orderId: 123458,
createDate: '2026-01-13T09:15:00',
customerName: 'Pedro Oliveira',
status: 'Entregue',
orderType: 'Venda',
amount: 890.75,
invoiceNumber: 'NF-001235',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 5.0,
fatUserName: 'Admin',
deliveryLocal: 'Belo Horizonte - MG',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Baixa',
paymentName: 'PIX',
partnerName: 'Parceiro DEF',
codusur2Name: 'Rep. Nacional',
releaseUserName: 'Gerente',
driver: 'Paulo Motorista',
carDescription: 'Fiorino',
carrier: 'Transportadora ABC',
schedulerDelivery: '13/01/2026',
fatUserDescription: 'Faturamento Automático',
emitenteNome: 'Outra Empresa LTDA',
},
];
// Colunas simplificadas para a demo
const demoColumns = [
{ field: 'orderId', headerName: 'Pedido', width: 100 },
{ field: 'createDate', headerName: 'Data Criação', width: 150 },
{ field: 'customerName', headerName: 'Cliente', width: 180 },
{ field: 'status', headerName: 'Status', width: 120 },
{ field: 'orderType', headerName: 'Tipo', width: 100 },
{
field: 'amount',
headerName: 'Valor',
width: 120,
valueFormatter: (value: number) =>
new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(value),
},
{ field: 'invoiceNumber', headerName: 'Nota Fiscal', width: 130 },
{ field: 'sellerName', headerName: 'Vendedor', width: 150 },
{ field: 'deliveryType', headerName: 'Entrega', width: 100 },
{ field: 'deliveryLocal', headerName: 'Local Entrega', width: 180 },
];
const theme = createTheme({
palette: {
mode: 'light',
},
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
// Componente wrapper para o Storybook
const OrderTableDemo = ({
rows = mockOrders,
loading = false,
emptyState = false,
}: {
rows?: typeof mockOrders;
loading?: boolean;
emptyState?: boolean;
}) => {
const displayRows = emptyState ? [] : rows;
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<QueryClientProvider client={queryClient}>
<Box sx={{ width: '100%', p: 2 }}>
<Paper
sx={{
boxShadow: 'none',
border: 'none',
backgroundColor: 'transparent',
overflow: 'hidden',
}}
>
<DataGridPremium
rows={displayRows}
columns={demoColumns}
loading={loading}
density="compact"
autoHeight={false}
initialState={{
pagination: {
paginationModel: {
pageSize: 10,
page: 0,
},
},
sorting: {
sortModel: [{ field: 'createDate', sort: 'desc' }],
},
}}
pageSizeOptions={[10, 25, 50]}
paginationMode="client"
pagination
sx={{
height: 400,
border: '1px solid',
borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
backgroundColor: 'background.paper',
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'grey.50',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',
borderColor: 'divider',
},
'& .MuiDataGrid-row': {
borderBottom: '1px solid',
borderColor: 'divider',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'action.hover',
},
},
'& .MuiDataGrid-cell': {
fontSize: '0.75rem',
},
'& .MuiDataGrid-footerContainer': {
borderTop: '2px solid',
borderColor: 'divider',
backgroundColor: 'grey.50',
},
}}
localeText={{
noRowsLabel: 'Nenhum pedido encontrado.',
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
}}
/>
</Paper>
</Box>
</QueryClientProvider>
</ThemeProvider>
);
};
const meta: Meta<typeof OrderTableDemo> = {
title: 'Features/Orders/OrderTable',
component: OrderTableDemo,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: `
## OrderTable
Tabela de pedidos com as seguintes funcionalidades:
- **Paginação**: Suporta 10, 25 ou 50 itens por página
- **Ordenação**: Por qualquer coluna clicando no header
- **Seleção**: Clique em uma linha para ver detalhes
- **Responsividade**: Adapta altura para mobile/desktop
`,
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof OrderTableDemo>;
/**
* Estado padrão da tabela com dados de pedidos.
*/
export const Default: Story = {
args: {
rows: [
{
id: '1',
orderId: 1232236,
createDate: '2026-01-15T10:00:00',
customerName: 'Maria Silva',
status: 'Faturado',
orderType: 'Venda',
amount: 1250.5,
invoiceNumber: 'NF-001234',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 15.5,
fatUserName: 'Admin',
deliveryLocal: 'São Paulo - SP',
masterDeliveryLocal: 'Região Sul',
deliveryPriority: 'Alta',
paymentName: 'Boleto',
partnerName: 'Parceiro ABC',
codusur2Name: 'Rep. Regional',
releaseUserName: 'Supervisor',
driver: 'Carlos Motorista',
carDescription: 'Sprinter',
carrier: 'Transportadora XYZ',
schedulerDelivery: '15/01/2026',
fatUserDescription: 'Faturamento',
emitenteNome: 'Empresa LTDA',
},
{
id: '2',
orderId: 123457,
createDate: '2026-01-14T14:30:00',
customerName: 'João Santos',
status: 'Pendente',
orderType: 'Venda',
amount: 3450,
invoiceNumber: '',
sellerName: 'Ana Vendedora',
deliveryType: 'Retirada',
totalWeight: 8.2,
fatUserName: '',
deliveryLocal: 'Rio de Janeiro - RJ',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Normal',
paymentName: 'Cartão de Crédito',
partnerName: '',
codusur2Name: '',
releaseUserName: '',
driver: '',
carDescription: '',
carrier: '',
schedulerDelivery: '',
fatUserDescription: '',
emitenteNome: 'Empresa LTDA',
},
{
id: '3',
orderId: 123458,
createDate: '2026-01-13T09:15:00',
customerName: 'Pedro Oliveira',
status: 'Entregue',
orderType: 'Venda',
amount: 890.75,
invoiceNumber: 'NF-001235',
sellerName: 'João Vendedor',
deliveryType: 'Entrega',
totalWeight: 5,
fatUserName: 'Admin',
deliveryLocal: 'Belo Horizonte - MG',
masterDeliveryLocal: 'Região Sudeste',
deliveryPriority: 'Baixa',
paymentName: 'PIX',
partnerName: 'Parceiro DEF',
codusur2Name: 'Rep. Nacional',
releaseUserName: 'Gerente',
driver: 'Paulo Motorista',
carDescription: 'Fiorino',
carrier: 'Transportadora ABC',
schedulerDelivery: '13/01/2026',
fatUserDescription: 'Faturamento Automático',
emitenteNome: 'Outra Empresa LTDA',
},
],
loading: false,
emptyState: false,
},
};
/**
* Estado de carregamento enquanto busca pedidos da API.
*/
export const Loading: Story = {
args: {
rows: [],
loading: true,
emptyState: false,
},
};
/**
* Estado vazio quando não pedidos para exibir.
*/
export const Empty: Story = {
args: {
rows: [],
loading: false,
emptyState: true,
},
};
/**
* Tabela com muitos pedidos para demonstrar a paginação.
*/
export const ManyOrders: Story = {
args: {
rows: Array.from({ length: 50 }, (_, i) => ({
...mockOrders[i % 3],
id: String(i + 1),
orderId: 123456 + i,
customerName: `Cliente ${i + 1}`,
amount: Math.random() * 10000,
})),
loading: false,
emptyState: false,
},
};

View File

@ -1,14 +1,18 @@
'use client'; 'use client';
import { useMemo, useState } from 'react'; import { useMemo, useState, useEffect } from 'react';
import { SearchBar } from './SearchBar'; import { SearchBar } from './SearchBar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import { DataGridPremium, GridCellSelectionModel } from '@mui/x-data-grid-premium'; import {
DataGridPremium,
GridCellSelectionModel,
} from '@mui/x-data-grid-premium';
import { useOrders } from '../hooks/useOrders'; import { useOrders } from '../hooks/useOrders';
import { useStores } from '../store/useStores';
import { createOrderColumns } from './OrderTableColumns'; import { createOrderColumns } from './OrderTableColumns';
import { calculateTableHeight } from '../utils/tableHelpers'; import { calculateTableHeight } from '../utils/tableHelpers';
import { normalizeOrder } from '../utils/orderNormalizer'; import { normalizeOrder } from '../utils/orderNormalizer';
@ -17,21 +21,41 @@ import { OrderDetailsTabs } from './OrderDetailsTabs';
export const OrderTable = () => { export const OrderTable = () => {
const { data: orders, isLoading, error } = useOrders(); const { data: orders, isLoading, error } = useOrders();
const [cellSelectionModel, setCellSelectionModel] = useState<GridCellSelectionModel>({}); const { data: stores } = useStores();
const [cellSelectionModel, setCellSelectionModel] =
useState<GridCellSelectionModel>({});
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null); const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
useEffect(() => {
if (!orders || orders.length === 0) {
setSelectedOrderId(null);
return;
}
setSelectedOrderId((current) => {
if (!current) return null;
const exists = orders.some((order: Order) => order.orderId === current);
return exists ? current : null;
});
}, [orders]);
// Cria mapa de storeId -> storeName
const storesMap = useMemo(() => {
if (!stores) return new Map<string, string>();
return new Map(
stores.map((store) => [String(store.id), store.store || store.name || String(store.id)])
);
}, [stores]);
const rows = useMemo(() => { const rows = useMemo(() => {
if (!Array.isArray(orders) || orders.length === 0) return []; if (!Array.isArray(orders) || orders.length === 0) return [];
return orders.map((order: Order, index: number) => normalizeOrder(order, index)); return orders.map((order: Order, index: number) =>
normalizeOrder(order, index)
);
}, [orders]); }, [orders]);
const columns = useMemo( const columns = useMemo(() => createOrderColumns({ storesMap }), [storesMap]);
() => createOrderColumns(),
[]
);
if (error) { if (error) {
return ( return (
@ -47,11 +71,24 @@ export const OrderTable = () => {
const tableHeight = calculateTableHeight(rows.length, 10); const tableHeight = calculateTableHeight(rows.length, 10);
const mobileTableHeight = calculateTableHeight(rows.length, 5, {
minHeight: 300,
rowHeight: 40,
});
return ( return (
<Box> <Box>
<SearchBar /> <SearchBar />
<Paper sx={{ mt: 3, boxShadow: 'none', border: 'none', backgroundColor: 'transparent', overflow: 'hidden' }}> <Paper
sx={{
mt: { xs: 2, md: 3 },
boxShadow: 'none',
border: 'none',
backgroundColor: 'transparent',
overflow: 'hidden',
}}
>
<DataGridPremium <DataGridPremium
rows={rows} rows={rows}
columns={columns} columns={columns}
@ -73,7 +110,7 @@ export const OrderTable = () => {
sortModel: [{ field: 'createDate', sort: 'desc' }], sortModel: [{ field: 'createDate', sort: 'desc' }],
}, },
pinnedColumns: { pinnedColumns: {
left: ['orderId', 'customerName'], /// left: ['orderId', 'customerName'],
}, },
}} }}
pageSizeOptions={[10, 25, 50]} pageSizeOptions={[10, 25, 50]}
@ -88,7 +125,7 @@ export const OrderTable = () => {
params.row.orderId === selectedOrderId ? 'Mui-selected' : '' params.row.orderId === selectedOrderId ? 'Mui-selected' : ''
} }
sx={{ sx={{
height: tableHeight, height: { xs: mobileTableHeight, md: tableHeight },
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
@ -253,7 +290,8 @@ export const OrderTable = () => {
}, },
}} }}
localeText={{ localeText={{
noRowsLabel: 'Nenhum pedido encontrado para os filtros selecionados.', noRowsLabel:
'Nenhum pedido encontrado para os filtros selecionados.',
noResultsOverlayLabel: 'Nenhum resultado encontrado.', noResultsOverlayLabel: 'Nenhum resultado encontrado.',
footerTotalRows: 'Total de registros:', footerTotalRows: 'Total de registros:',
footerTotalVisibleRows: (visibleCount, totalCount) => footerTotalVisibleRows: (visibleCount, totalCount) =>
@ -263,11 +301,18 @@ export const OrderTable = () => {
? `${count.toLocaleString()} linha selecionada` ? `${count.toLocaleString()} linha selecionada`
: `${count.toLocaleString()} linhas selecionadas`, : `${count.toLocaleString()} linhas selecionadas`,
}} }}
slotProps={{ slotProps={{
pagination: { pagination: {
labelRowsPerPage: 'Pedidos por página:', labelRowsPerPage: 'Pedidos por página:',
labelDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) => { labelDisplayedRows: ({
from,
to,
count,
}: {
from: number;
to: number;
count: number;
}) => {
const pageSize = to >= from ? to - from + 1 : 10; const pageSize = to >= from ? to - from + 1 : 10;
const currentPage = Math.floor((from - 1) / pageSize) + 1; const currentPage = Math.floor((from - 1) / pageSize) + 1;
const totalPages = Math.ceil(count / pageSize); const totalPages = Math.ceil(count / pageSize);

View File

@ -3,10 +3,25 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import { formatDate, formatDateTime, formatCurrency, formatNumber } from '../utils/orderFormatters'; import {
import { getStatusChipProps, getPriorityChipProps } from '../utils/tableHelpers'; formatDate,
formatDateTime,
formatCurrency,
formatNumber,
} from '../utils/orderFormatters';
import {
getStatusChipProps,
getPriorityChipProps,
} from '../utils/tableHelpers';
export const createOrderColumns = (): GridColDef[] => [ interface CreateOrderColumnsOptions {
storesMap?: Map<string, string>;
}
export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridColDef[] => {
const storesMap = options?.storesMap;
return [
{ {
field: 'orderId', field: 'orderId',
headerName: 'Pedido', headerName: 'Pedido',
@ -15,7 +30,10 @@ export const createOrderColumns = (): GridColDef[] => [
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}> <Typography
variant="body2"
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
>
{params.value} {params.value}
</Typography> </Typography>
), ),
@ -33,7 +51,11 @@ export const createOrderColumns = (): GridColDef[] => [
{formatDate(params.value)} {formatDate(params.value)}
</Typography> </Typography>
{dateTime && ( {dateTime && (
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.6875rem' }}> <Typography
variant="caption"
color="text.secondary"
sx={{ fontSize: '0.6875rem' }}
>
{dateTime} {dateTime}
</Typography> </Typography>
)} )}
@ -58,15 +80,19 @@ export const createOrderColumns = (): GridColDef[] => [
headerName: 'Filial', headerName: 'Filial',
width: 200, width: 200,
minWidth: 180, minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => {
const storeId = String(params.value);
const storeName = storesMap?.get(storeId) || storeId;
return (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value} {storeName}
</Typography> </Typography>
), );
},
}, },
{ {
field: 'store', field: 'store',
headerName: 'Filial Faturamento', headerName: 'Supervisor',
width: 200, width: 200,
minWidth: 180, minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
@ -122,7 +148,10 @@ export const createOrderColumns = (): GridColDef[] => [
align: 'right', align: 'right',
valueFormatter: (value) => formatCurrency(value as number), valueFormatter: (value) => formatCurrency(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}> <Typography
variant="body2"
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
>
{formatCurrency(params.value)} {formatCurrency(params.value)}
</Typography> </Typography>
), ),
@ -135,7 +164,10 @@ export const createOrderColumns = (): GridColDef[] => [
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value && params.value !== '-' ? params.value : '-'} {params.value && params.value !== '-' ? params.value : '-'}
</Typography> </Typography>
), ),
@ -184,7 +216,10 @@ export const createOrderColumns = (): GridColDef[] => [
align: 'right', align: 'right',
valueFormatter: (value) => formatNumber(value as number), valueFormatter: (value) => formatNumber(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{formatNumber(params.value)} {formatNumber(params.value)}
</Typography> </Typography>
), ),
@ -208,7 +243,11 @@ export const createOrderColumns = (): GridColDef[] => [
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value || '-'} {params.value || '-'}
</Typography> </Typography>
), ),
@ -290,10 +329,16 @@ export const createOrderColumns = (): GridColDef[] => [
const timeStr = params.row.invoiceTime; const timeStr = params.row.invoiceTime;
return ( return (
<Typography variant="body2" sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}> <Typography
variant="body2"
sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}
>
{dateStr} {dateStr}
{timeStr && ( {timeStr && (
<Box component="span" sx={{ color: 'text.secondary', fontSize: '0.6875rem', ml: 0.5 }}> <Box
component="span"
sx={{ color: 'text.secondary', fontSize: '0.6875rem', ml: 0.5 }}
>
{timeStr} {timeStr}
</Box> </Box>
)} )}
@ -342,7 +387,11 @@ export const createOrderColumns = (): GridColDef[] => [
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}> <Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
>
{params.value || '-'} {params.value || '-'}
</Typography> </Typography>
), ),
@ -392,4 +441,4 @@ export const createOrderColumns = (): GridColDef[] => [
), ),
}, },
]; ];
};

View File

@ -12,6 +12,9 @@ import {
Paper, Paper,
Tooltip, Tooltip,
Collapse, Collapse,
CircularProgress,
Badge,
type AutocompleteRenderInputParams,
} from '@mui/material'; } from '@mui/material';
import { useStores } from '../store/useStores'; import { useStores } from '../store/useStores';
import { useCustomers } from '../hooks/useCustomers'; import { useCustomers } from '../hooks/useCustomers';
@ -30,7 +33,6 @@ import 'moment/locale/pt-br';
moment.locale('pt-br'); moment.locale('pt-br');
// Tipo para os filtros locais (não precisam da flag searchTriggered)
interface LocalFilters { interface LocalFilters {
status: string | null; status: string | null;
orderId: number | null; orderId: number | null;
@ -44,7 +46,9 @@ interface LocalFilters {
sellerName: string | null; sellerName: string | null;
} }
const getInitialLocalFilters = (urlFilters: any): LocalFilters => ({ const getInitialLocalFilters = (
urlFilters: Partial<LocalFilters>
): LocalFilters => ({
status: urlFilters.status ?? null, status: urlFilters.status ?? null,
orderId: urlFilters.orderId ?? null, orderId: urlFilters.orderId ?? null,
customerId: urlFilters.customerId ?? null, customerId: urlFilters.customerId ?? null,
@ -60,7 +64,6 @@ const getInitialLocalFilters = (urlFilters: any): LocalFilters => ({
export const SearchBar = () => { export const SearchBar = () => {
const [urlFilters, setUrlFilters] = useOrderFilters(); const [urlFilters, setUrlFilters] = useOrderFilters();
// Estado local para inputs (não dispara busca ao mudar)
const [localFilters, setLocalFilters] = useState<LocalFilters>(() => const [localFilters, setLocalFilters] = useState<LocalFilters>(() =>
getInitialLocalFilters(urlFilters) getInitialLocalFilters(urlFilters)
); );
@ -70,22 +73,22 @@ export const SearchBar = () => {
const [customerSearchTerm, setCustomerSearchTerm] = useState(''); const [customerSearchTerm, setCustomerSearchTerm] = useState('');
const customers = useCustomers(customerSearchTerm); const customers = useCustomers(customerSearchTerm);
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const [touchedFields, setTouchedFields] = useState<{ const [touchedFields, setTouchedFields] = useState<{
createDateIni?: boolean; createDateIni?: boolean;
createDateEnd?: boolean; createDateEnd?: boolean;
}>({}); }>({});
// Sync local state com URL (para navegação Back/Forward)
useEffect(() => { useEffect(() => {
setLocalFilters(getInitialLocalFilters(urlFilters)); setLocalFilters(getInitialLocalFilters(urlFilters));
}, [urlFilters]); }, [urlFilters]);
const updateLocalFilter = useCallback(<K extends keyof LocalFilters>( const updateLocalFilter = useCallback(
key: K, <K extends keyof LocalFilters>(key: K, value: LocalFilters[K]) => {
value: LocalFilters[K] setLocalFilters((prev) => ({ ...prev, [key]: value }));
) => { },
setLocalFilters(prev => ({ ...prev, [key]: value })); []
}, []); );
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
setTouchedFields({}); setTouchedFields({});
@ -110,7 +113,6 @@ export const SearchBar = () => {
customerName: null, customerName: null,
}; };
// Reset local + URL
setLocalFilters(getInitialLocalFilters(resetState)); setLocalFilters(getInitialLocalFilters(resetState));
setUrlFilters(resetState); setUrlFilters(resetState);
}, [setUrlFilters]); }, [setUrlFilters]);
@ -129,24 +131,27 @@ export const SearchBar = () => {
const handleFilter = useCallback(() => { const handleFilter = useCallback(() => {
if (!localFilters.createDateIni) { if (!localFilters.createDateIni) {
setTouchedFields(prev => ({ ...prev, createDateIni: true })); setTouchedFields((prev) => ({ ...prev, createDateIni: true }));
return; return;
} }
const dateError = validateDates(); const dateError = validateDates();
if (dateError) { if (dateError) {
setTouchedFields(prev => ({ ...prev, createDateEnd: true })); setTouchedFields((prev) => ({ ...prev, createDateEnd: true }));
return; return;
} }
// Commit local filters to URL (triggers search) setIsSearching(true);
setUrlFilters({ setUrlFilters({
...localFilters, ...localFilters,
searchTriggered: true, searchTriggered: true,
}); });
// Reset loading state after a short delay
setTimeout(() => setIsSearching(false), 500);
}, [localFilters, setUrlFilters, validateDates]); }, [localFilters, setUrlFilters, validateDates]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => { const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const isValid = !!localFilters.createDateIni; const isValid = !!localFilters.createDateIni;
const dateErr = validateDates(); const dateErr = validateDates();
@ -154,17 +159,27 @@ export const SearchBar = () => {
handleFilter(); handleFilter();
} }
} }
}, [localFilters.createDateIni, validateDates, handleFilter]); },
[localFilters.createDateIni, validateDates, handleFilter]
);
const isDateValid = !!localFilters.createDateIni; const isDateValid = !!localFilters.createDateIni;
const dateError = validateDates(); const dateError = validateDates();
const showDateIniError = touchedFields.createDateIni && !localFilters.createDateIni; const showDateIniError =
touchedFields.createDateIni && !localFilters.createDateIni;
const showDateEndError = touchedFields.createDateEnd && dateError; const showDateEndError = touchedFields.createDateEnd && dateError;
// Contador de filtros avançados ativos
const advancedFiltersCount = [
localFilters.store?.length,
localFilters.stockId?.length,
localFilters.sellerId,
].filter(Boolean).length;
return ( return (
<Paper <Paper
sx={{ sx={{
p: 3, p: { xs: 2, md: 3 },
mb: 2, mb: 2,
bgcolor: 'background.paper', bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
@ -173,8 +188,7 @@ export const SearchBar = () => {
elevation={0} elevation={0}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<Grid container spacing={2} alignItems="flex-end"> <Grid container spacing={{ xs: 1.5, md: 2 }} alignItems="flex-end">
{/* --- Primary Filters (Always Visible) --- */} {/* --- Primary Filters (Always Visible) --- */}
{/* Campo de Texto Simples (Nº Pedido) */} {/* Campo de Texto Simples (Nº Pedido) */}
@ -222,9 +236,11 @@ export const SearchBar = () => {
options={customers.options} options={customers.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) => option.id === value.id}
value={customers.options.find(option => value={
localFilters.customerId === option.customer?.id customers.options.find(
) || null} (option) => localFilters.customerId === option.customer?.id
) || null
}
onChange={(_, newValue) => { onChange={(_, newValue) => {
if (!newValue) { if (!newValue) {
updateLocalFilter('customerName', null); updateLocalFilter('customerName', null);
@ -234,7 +250,10 @@ export const SearchBar = () => {
} }
updateLocalFilter('customerId', newValue.customer?.id || null); updateLocalFilter('customerId', newValue.customer?.id || null);
updateLocalFilter('customerName', newValue.customer?.name || null); updateLocalFilter(
'customerName',
newValue.customer?.name || null
);
}} }}
onInputChange={(_, newInputValue, reason) => { onInputChange={(_, newInputValue, reason) => {
if (reason === 'clear') { if (reason === 'clear') {
@ -254,14 +273,18 @@ export const SearchBar = () => {
} }
}} }}
loading={customers.isLoading} loading={customers.isLoading}
renderInput={(params: Readonly<any>) => ( renderInput={(params: AutocompleteRenderInputParams) => (
<TextField <TextField
{...params} {...params}
label="Cliente" label="Cliente"
placeholder="Digite para buscar..." placeholder="Digite para buscar..."
/> />
)} )}
noOptionsText={customerSearchTerm.length < 2 ? 'Digite pelo menos 2 caracteres' : 'Nenhum cliente encontrado'} noOptionsText={
customerSearchTerm.length < 2
? 'Digite pelo menos 2 caracteres'
: 'Nenhum cliente encontrado'
}
loadingText="Buscando clientes..." loadingText="Buscando clientes..."
filterOptions={(x) => x} filterOptions={(x) => x}
clearOnBlur={false} clearOnBlur={false}
@ -272,54 +295,95 @@ export const SearchBar = () => {
{/* Campos de Data */} {/* Campos de Data */}
<Grid size={{ xs: 12, sm: 12, md: 4 }}> <Grid size={{ xs: 12, sm: 12, md: 4 }}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="pt-br"> <LocalizationProvider
<Box display="flex" gap={2}> dateAdapter={AdapterMoment}
adapterLocale="pt-br"
>
<Box display="flex" gap={{ xs: 1.5, md: 2 }} flexDirection={{ xs: 'column', sm: 'row' }}>
<Box flex={1}> <Box flex={1}>
<DatePicker <DatePicker
label="Data Inicial" label="Data Inicial"
value={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : null} value={
localFilters.createDateIni
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
: null
}
onChange={(date: moment.Moment | null) => { onChange={(date: moment.Moment | null) => {
setTouchedFields(prev => ({ ...prev, createDateIni: true })); setTouchedFields((prev) => ({
updateLocalFilter('createDateIni', date ? date.format('YYYY-MM-DD') : null); ...prev,
createDateIni: true,
}));
updateLocalFilter(
'createDateIni',
date ? date.format('YYYY-MM-DD') : null
);
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
maxDate={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : undefined} maxDate={
localFilters.createDateEnd
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
: undefined
}
slotProps={{ slotProps={{
textField: { textField: {
size: 'small', size: 'small',
fullWidth: true, fullWidth: true,
required: true, required: true,
error: showDateIniError, error: showDateIniError,
helperText: showDateIniError ? 'Data inicial é obrigatória' : '', helperText: showDateIniError
onBlur: () => setTouchedFields(prev => ({ ...prev, createDateIni: true })), ? 'Data inicial é obrigatória'
: '',
onBlur: () =>
setTouchedFields((prev) => ({
...prev,
createDateIni: true,
})),
inputProps: { inputProps: {
'aria-required': true, 'aria-required': true,
} },
} },
}} }}
/> />
</Box> </Box>
<Box flex={1}> <Box flex={1}>
<DatePicker <DatePicker
label="Data Final" label="Data Final (opcional)"
value={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : null} value={
localFilters.createDateEnd
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
: null
}
onChange={(date: moment.Moment | null) => { onChange={(date: moment.Moment | null) => {
setTouchedFields(prev => ({ ...prev, createDateEnd: true })); setTouchedFields((prev) => ({
updateLocalFilter('createDateEnd', date ? date.format('YYYY-MM-DD') : null); ...prev,
createDateEnd: true,
}));
updateLocalFilter(
'createDateEnd',
date ? date.format('YYYY-MM-DD') : null
);
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
minDate={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : undefined} minDate={
localFilters.createDateIni
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
: undefined
}
slotProps={{ slotProps={{
textField: { textField: {
size: 'small', size: 'small',
fullWidth: true, fullWidth: true,
error: !!showDateEndError, error: !!showDateEndError,
helperText: showDateEndError || '', helperText: showDateEndError || '',
onBlur: () => setTouchedFields(prev => ({ ...prev, createDateEnd: true })), onBlur: () =>
setTouchedFields((prev) => ({
...prev,
createDateEnd: true,
})),
inputProps: { inputProps: {
placeholder: 'Opcional', placeholder: 'Opcional',
} },
} },
}} }}
/> />
</Box> </Box>
@ -328,28 +392,41 @@ export const SearchBar = () => {
</Grid> </Grid>
{/* Botões de Ação */} {/* Botões de Ação */}
<Grid size={{ xs: 12, sm: 12, md: 1.5 }} sx={{ display: 'flex', justifyContent: 'flex-end' }}> <Grid
<Box sx={{ display: 'flex', gap: 1 }}> size={{ xs: 12, sm: 12, md: 1.5 }}
sx={{ display: 'flex', justifyContent: { xs: 'stretch', sm: 'flex-end' } }}
>
<Box sx={{ display: 'flex', gap: 1, width: { xs: '100%', sm: 'auto' } }}>
<Tooltip title="Limpar filtros" arrow> <Tooltip title="Limpar filtros" arrow>
<span> <span>
<Button <Button
variant="outlined" variant="outlined"
color="inherit" color="inherit"
onClick={handleReset} onClick={handleReset}
aria-label="Limpar filtros"
sx={{ sx={{
minWidth: 40, minWidth: { xs: 48, md: 40 },
minHeight: 44,
px: 1.5, px: 1.5,
flex: { xs: 1, sm: 'none' },
'&:hover': { '&:hover': {
bgcolor: 'action.hover', bgcolor: 'action.hover',
} },
}} }}
> >
<ResetIcon /> <ResetIcon />
<Box component="span" sx={{ display: { xs: 'none', md: 'inline' }, ml: 0.5 }}>
Limpar
</Box>
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={isDateValid ? 'Buscar pedidos' : 'Preencha a data inicial para buscar'} title={
isDateValid
? 'Buscar pedidos'
: 'Preencha a data inicial para buscar'
}
arrow arrow
> >
<span> <span>
@ -357,16 +434,28 @@ export const SearchBar = () => {
variant="contained" variant="contained"
color="primary" color="primary"
onClick={handleFilter} onClick={handleFilter}
disabled={!isDateValid || !!dateError} disabled={!isDateValid || !!dateError || isSearching}
aria-label="Buscar pedidos"
sx={{ sx={{
minWidth: 40, minWidth: { xs: 48, md: 40 },
minHeight: 44,
px: 1.5, px: 1.5,
flex: { xs: 2, sm: 'none' },
'&:disabled': { '&:disabled': {
opacity: 0.6, opacity: 0.6,
} },
}} }}
> >
{isSearching ? (
<CircularProgress size={20} color="inherit" />
) : (
<>
<SearchIcon /> <SearchIcon />
<Box component="span" sx={{ display: { xs: 'none', md: 'inline' }, ml: 0.5 }}>
Buscar
</Box>
</>
)}
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
@ -376,14 +465,23 @@ export const SearchBar = () => {
{/* --- Advanced Filters (Collapsible) --- */} {/* --- Advanced Filters (Collapsible) --- */}
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Badge
badgeContent={advancedFiltersCount}
color="primary"
invisible={advancedFiltersCount === 0}
>
<Button <Button
size="small" size="small"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)} onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
endIcon={showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />} endIcon={
showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />
}
aria-label={showAdvancedFilters ? 'Ocultar filtros avançados' : 'Mostrar filtros avançados'}
sx={{ textTransform: 'none', color: 'text.secondary' }} sx={{ textTransform: 'none', color: 'text.secondary' }}
> >
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'} {showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
</Button> </Button>
</Badge>
</Box> </Box>
<Collapse in={showAdvancedFilters}> <Collapse in={showAdvancedFilters}>
<Grid container spacing={2} sx={{ pt: 2 }}> <Grid container spacing={2} sx={{ pt: 2 }}>
@ -394,19 +492,28 @@ export const SearchBar = () => {
size="small" size="small"
options={stores.options} options={stores.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) =>
value={stores.options.filter(option => option.id === value.id
}
value={stores.options.filter((option) =>
localFilters.store?.includes(option.value) localFilters.store?.includes(option.value)
)} )}
onChange={(_, newValue) => { onChange={(_, newValue) => {
updateLocalFilter('store', newValue.map(option => option.value)); updateLocalFilter(
'store',
newValue.map((option) => option.value)
);
}} }}
loading={stores.isLoading} loading={stores.isLoading}
renderInput={(params: Readonly<any>) => ( renderInput={(params: AutocompleteRenderInputParams) => (
<TextField <TextField
{...params} {...params}
label="Filiais" label="Filiais"
placeholder={localFilters.store?.length ? `${localFilters.store.length} selecionadas` : 'Selecione'} placeholder={
localFilters.store?.length
? `${localFilters.store.length} selecionadas`
: 'Selecione'
}
/> />
)} )}
/> />
@ -419,19 +526,28 @@ export const SearchBar = () => {
size="small" size="small"
options={stores.options} options={stores.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) =>
value={stores.options.filter(option => option.id === value.id
}
value={stores.options.filter((option) =>
localFilters.stockId?.includes(option.value) localFilters.stockId?.includes(option.value)
)} )}
onChange={(_, newValue) => { onChange={(_, newValue) => {
updateLocalFilter('stockId', newValue.map(option => option.value)); updateLocalFilter(
'stockId',
newValue.map((option) => option.value)
);
}} }}
loading={stores.isLoading} loading={stores.isLoading}
renderInput={(params: Readonly<any>) => ( renderInput={(params: AutocompleteRenderInputParams) => (
<TextField <TextField
{...params} {...params}
label="Filial de Estoque" label="Filial de Estoque"
placeholder={localFilters.stockId?.length ? `${localFilters.stockId.length} selecionadas` : 'Selecione'} placeholder={
localFilters.stockId?.length
? `${localFilters.stockId.length} selecionadas`
: 'Selecione'
}
/> />
)} )}
/> />
@ -443,13 +559,24 @@ export const SearchBar = () => {
size="small" size="small"
options={sellers.options} options={sellers.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) =>
value={sellers.options.find(option => option.id === value.id
}
value={
sellers.options.find(
(option) =>
localFilters.sellerId === option.seller.id.toString() localFilters.sellerId === option.seller.id.toString()
) || null} ) || null
}
onChange={(_, newValue) => { onChange={(_, newValue) => {
updateLocalFilter('sellerId', newValue?.seller.id.toString() || null); updateLocalFilter(
updateLocalFilter('sellerName', newValue?.seller.name || null); 'sellerId',
newValue?.seller.id.toString() || null
);
updateLocalFilter(
'sellerName',
newValue?.seller.name || null
);
}} }}
loading={sellers.isLoading} loading={sellers.isLoading}
renderInput={(params) => ( renderInput={(params) => (
@ -472,7 +599,6 @@ export const SearchBar = () => {
</Grid> </Grid>
</Collapse> </Collapse>
</Grid> </Grid>
</Grid> </Grid>
</Paper> </Paper>
); );

View File

@ -17,11 +17,7 @@ export const TabPanel = ({ children, value, index }: TabPanelProps) => {
id={`order-tabpanel-${index}`} id={`order-tabpanel-${index}`}
aria-labelledby={`order-tab-${index}`} aria-labelledby={`order-tab-${index}`}
> >
{value === index && ( {value === index && <Box sx={{ py: 3 }}>{children}</Box>}
<Box sx={{ py: 3 }}>
{children}
</Box>
)}
</div> </div>
); );
}; };

View File

@ -1,21 +1,63 @@
'use client'; 'use client';
import { useMemo } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useCargoMovement } from '../../hooks/useCargoMovement';
import { createCargoMovementColumns } from './CargoMovementPanelColumns';
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
interface CargoMovementPanelProps { interface CargoMovementPanelProps {
orderId: number; orderId: number;
} }
export const CargoMovementPanel = ({ orderId }: CargoMovementPanelProps) => { export const CargoMovementPanel = ({ orderId }: CargoMovementPanelProps) => {
const { data: movements, isLoading, error } = useCargoMovement(orderId);
const columns = useMemo(() => createCargoMovementColumns(), []);
const rows = useMemo(() => {
if (!movements || movements.length === 0) return [];
return movements.map((movement, index) => ({
id: `${movement.transactionId}-${index}`,
...movement,
}));
}, [movements]);
if (isLoading) {
return ( return (
<Box> <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<Alert severity="info" sx={{ mb: 2 }}> <CircularProgress size={30} />
<Typography variant="body2">
Funcionalidade em desenvolvimento para o pedido {orderId}
</Typography>
</Alert>
</Box> </Box>
); );
}
if (error) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="error">Erro ao carregar movimentação de carga.</Alert>
</Box>
);
}
if (!movements || movements.length === 0) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="info">Nenhuma movimentação de carga encontrada para este pedido.</Alert>
</Box>
);
}
return (
<DataGridPremium
rows={rows}
columns={columns}
density="compact"
autoHeight
hideFooter
sx={dataGridStylesSimple}
/>
);
}; };

View File

@ -0,0 +1,75 @@
import { GridColDef } from '@mui/x-data-grid-premium';
import { formatDate } from '../../utils/orderFormatters';
export const createCargoMovementColumns = (): GridColDef[] => [
{
field: 'transactionId',
headerName: ' Transação',
width: 110,
align: 'center',
headerAlign: 'center',
description: 'Identificador da transação',
},
{
field: 'transferDate',
headerName: 'Data Transf.',
width: 100,
align: 'center',
headerAlign: 'center',
description: 'Data da transferência',
valueFormatter: (value) => formatDate(value as string),
},
{
field: 'invoiceId',
headerName: 'Nota Fiscal',
width: 100,
align: 'center',
headerAlign: 'center',
description: 'Número da nota fiscal',
},
{
field: 'oldShipment',
headerName: 'Carreg anterior',
width: 110,
align: 'center',
headerAlign: 'center',
description: 'Código do carregamento anterior',
},
{
field: 'newShipment',
headerName: 'Carreg atual',
width: 110,
align: 'center',
headerAlign: 'center',
description: 'Código do carregamento atual',
},
{
field: 'transferText',
headerName: 'Motivo',
minWidth: 200,
flex: 1.5,
description: 'Descrição da transferência',
},
{
field: 'cause',
headerName: 'Causa',
minWidth: 180,
flex: 1.2,
description: 'Causa da movimentação',
},
{
field: 'userName',
headerName: 'Usuário',
minWidth: 180,
flex: 1.2,
description: 'Nome do usuário responsável',
},
{
field: 'program',
headerName: 'Programa',
width: 100,
align: 'center',
headerAlign: 'center',
description: 'Programa utilizado',
},
];

View File

@ -1,21 +1,63 @@
'use client'; 'use client';
import { useMemo } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useCuttingItems } from '../../hooks/useCuttingItems';
import { createCuttingPanelColumns } from './CuttingPanelColumns';
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
interface CuttingPanelProps { interface CuttingPanelProps {
orderId: number; orderId: number;
} }
export const CuttingPanel = ({ orderId }: CuttingPanelProps) => { export const CuttingPanel = ({ orderId }: CuttingPanelProps) => {
const { data: items, isLoading, error } = useCuttingItems(orderId);
const columns = useMemo(() => createCuttingPanelColumns(), []);
const rows = useMemo(() => {
if (!items || items.length === 0) return [];
return items.map((item) => ({
id: item.productId,
...item,
}));
}, [items]);
if (isLoading) {
return ( return (
<Box> <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<Alert severity="info" sx={{ mb: 2 }}> <CircularProgress size={30} />
<Typography variant="body2">
Funcionalidade em desenvolvimento para o pedido {orderId}
</Typography>
</Alert>
</Box> </Box>
); );
}
if (error) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="error">Erro ao carregar itens de corte.</Alert>
</Box>
);
}
if (!items || items.length === 0) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="info">Nenhum item de corte encontrado para este pedido.</Alert>
</Box>
);
}
return (
<DataGridPremium
rows={rows}
columns={columns}
density="compact"
autoHeight
hideFooter
sx={dataGridStylesSimple}
/>
);
}; };

View File

@ -0,0 +1,59 @@
import { GridColDef } from '@mui/x-data-grid-premium';
export const createCuttingPanelColumns = (): GridColDef[] => [
{
field: 'productId',
headerName: 'Código',
width: 90,
align: 'center',
headerAlign: 'center',
description: 'Código do produto',
},
{
field: 'description',
headerName: 'Descrição',
minWidth: 250,
flex: 2,
description: 'Descrição do produto',
},
{
field: 'pacth',
headerName: 'Unidade',
width: 80,
align: 'center',
headerAlign: 'center',
description: 'Unidade de medida',
},
{
field: 'stockId',
headerName: 'Estoque',
width: 80,
align: 'center',
headerAlign: 'center',
description: 'Código do estoque',
},
{
field: 'saleQuantity',
headerName: 'Qtd Vendida',
width: 100,
align: 'right',
headerAlign: 'right',
description: 'Quantidade vendida',
},
{
field: 'cutQuantity',
headerName: 'Qtd Cortada',
width: 100,
align: 'right',
headerAlign: 'right',
description: 'Quantidade cortada',
},
{
field: 'separedQuantity',
headerName: 'Qtd Separada',
width: 110,
align: 'right',
headerAlign: 'right',
description: 'Quantidade separada',
},
];

View File

@ -1,21 +1,63 @@
'use client'; 'use client';
import { useMemo } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography'; import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useDelivery } from '../../hooks/useDelivery';
import { createDeliveryPanelColumns } from './DeliveryPanelColumns';
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
interface DeliveryPanelProps { interface DeliveryPanelProps {
orderId: number; orderId: number;
} }
export const DeliveryPanel = ({ orderId }: DeliveryPanelProps) => { export const DeliveryPanel = ({ orderId }: DeliveryPanelProps) => {
const { data: deliveries, isLoading, error } = useDelivery(orderId);
const columns = useMemo(() => createDeliveryPanelColumns(), []);
const rows = useMemo(() => {
if (!deliveries || deliveries.length === 0) return [];
return deliveries.map((delivery) => ({
id: delivery.shippimentId,
...delivery,
}));
}, [deliveries]);
if (isLoading) {
return ( return (
<Box> <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<Alert severity="info" sx={{ mb: 2 }}> <CircularProgress size={30} />
<Typography variant="body2">
Funcionalidade em desenvolvimento para o pedido {orderId}
</Typography>
</Alert>
</Box> </Box>
); );
}
if (error) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="error">Erro ao carregar entregas do pedido.</Alert>
</Box>
);
}
if (!deliveries || deliveries.length === 0) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="info">Nenhuma entrega encontrada para este pedido.</Alert>
</Box>
);
}
return (
<DataGridPremium
rows={rows}
columns={columns}
density="compact"
autoHeight
hideFooter
sx={dataGridStylesSimple}
/>
);
}; };

View File

@ -0,0 +1,146 @@
import { GridColDef } from '@mui/x-data-grid-premium';
import { formatDate } from '../../utils/orderFormatters';
export const createDeliveryPanelColumns = (): GridColDef[] => [
{
field: 'shippimentId',
headerName: 'Código da entrega',
width: 90,
align: 'center',
headerAlign: 'center',
description: 'Identificador da entrega',
},
{
field: 'placeName',
headerName: 'Praça',
minWidth: 150,
flex: 1,
description: 'Nome do local de entrega',
},
{
field: 'street',
headerName: 'Rua',
minWidth: 180,
flex: 1.5,
description: 'Rua do endereço de entrega',
},
{
field: 'addressNumber',
headerName: 'Nº',
width: 60,
align: 'center',
headerAlign: 'center',
description: 'Número do endereço',
},
{
field: 'bairro',
headerName: 'Bairro',
minWidth: 120,
flex: 1,
description: 'Bairro do endereço de entrega',
},
{
field: 'city',
headerName: 'Cidade',
minWidth: 120,
flex: 1,
description: 'Cidade de entrega',
},
{
field: 'state',
headerName: 'UF',
width: 50,
align: 'center',
headerAlign: 'center',
description: 'Estado',
},
{
field: 'cep',
headerName: 'CEP',
width: 90,
align: 'center',
headerAlign: 'center',
description: 'CEP do endereço',
},
{
field: 'shippimentDate',
headerName: 'Data Entrega',
width: 100,
align: 'center',
headerAlign: 'center',
description: 'Data programada para entrega',
valueFormatter: (value) => formatDate(value as string),
},
{
field: 'driver',
headerName: 'Motorista',
minWidth: 120,
flex: 1,
description: 'Nome do motorista',
},
{
field: 'car',
headerName: 'Veículo',
minWidth: 100,
flex: 0.8,
description: 'Veículo utilizado na entrega',
},
{
field: 'separatorName',
headerName: 'Separador',
minWidth: 100,
flex: 0.8,
description: 'Nome do separador',
},
{
field: 'closeDate',
headerName: 'Data Fechamento',
width: 120,
align: 'center',
headerAlign: 'center',
description: 'Data de fechamento da entrega',
valueFormatter: (value) => (value ? formatDate(value as string) : '-'),
},
{
field: 'commentOrder1',
headerName: 'Obs. Pedido 1',
minWidth: 200,
flex: 1.5,
description: 'Observação do pedido 1',
},
{
field: 'commentOrder2',
headerName: 'Obs. Pedido 2',
minWidth: 200,
flex: 1.5,
description: 'Observação do pedido 2',
},
{
field: 'commentDelivery1',
headerName: 'Obs. Entrega 1',
minWidth: 200,
flex: 1.5,
description: 'Observação da entrega 1',
},
{
field: 'commentDelivery2',
headerName: 'Obs. Entrega 2',
minWidth: 200,
flex: 1.5,
description: 'Observação da entrega 2',
},
{
field: 'commentDelivery3',
headerName: 'Obs. Entrega 3',
minWidth: 200,
flex: 1.5,
description: 'Observação da entrega 3',
},
{
field: 'commentDelivery4',
headerName: 'Obs. Entrega 4',
minWidth: 200,
flex: 1.5,
description: 'Observação da entrega 4',
},
];

View File

@ -20,10 +20,12 @@ export const InformationPanel = ({ orderId }: InformationPanelProps) => {
const rows = useMemo(() => { const rows = useMemo(() => {
if (!order) return []; if (!order) return [];
return [{ return [
{
id: order.orderId || orderId, id: order.orderId || orderId,
...order ...order,
}]; },
];
}, [order, orderId]); }, [order, orderId]);
if (isLoading) { if (isLoading) {

View File

@ -1,29 +1,24 @@
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import { formatCurrency, formatDate, getStatusColor } from '../../utils/orderFormatters'; import {
formatCurrency,
/** formatDate,
* Mapeia códigos de status para texto legível. getStatusColor,
*/ getStatusLabel,
const STATUS_LABELS: Record<string, string> = { } from '../../utils/orderFormatters';
'F': 'Faturado',
'C': 'Cancelado',
'P': 'Pendente',
};
const getStatusLabel = (status: string): string => STATUS_LABELS[status] || status;
export const createInformationPanelColumns = (): GridColDef[] => [ export const createInformationPanelColumns = (): GridColDef[] => [
{ {
field: 'customerName', field: 'customerName',
headerName: 'Cliente', headerName: 'Cliente',
width: 250, minWidth: 180,
flex: 1.5,
description: 'Nome do cliente do pedido', description: 'Nome do cliente do pedido',
}, },
{ {
field: 'storeId', field: 'storeId',
headerName: 'Filial', headerName: 'Filial',
width: 80, width: 60,
align: 'center', align: 'center',
headerAlign: 'center', headerAlign: 'center',
description: 'Código da filial', description: 'Código da filial',
@ -31,7 +26,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
{ {
field: 'createDate', field: 'createDate',
headerName: 'Data Criação', headerName: 'Data Criação',
width: 110, width: 95,
align: 'center', align: 'center',
headerAlign: 'center', headerAlign: 'center',
description: 'Data de criação do pedido', description: 'Data de criação do pedido',
@ -40,7 +35,7 @@ export const createInformationPanelColumns = (): GridColDef[] => [
{ {
field: 'status', field: 'status',
headerName: 'Situação', headerName: 'Situação',
width: 120, width: 90,
align: 'center', align: 'center',
headerAlign: 'center', headerAlign: 'center',
description: 'Situação atual do pedido', description: 'Situação atual do pedido',
@ -50,26 +45,28 @@ export const createInformationPanelColumns = (): GridColDef[] => [
size="small" size="small"
color={getStatusColor(params.value as string)} color={getStatusColor(params.value as string)}
variant="outlined" variant="outlined"
sx={{ height: 24 }} sx={{ height: 22, fontSize: '0.7rem' }}
/> />
), ),
}, },
{ {
field: 'paymentName', field: 'paymentName',
headerName: 'Forma Pagamento', headerName: 'Forma Pgto',
width: 150, minWidth: 100,
flex: 1,
description: 'Forma de pagamento utilizada', description: 'Forma de pagamento utilizada',
}, },
{ {
field: 'billingName', field: 'billingName',
headerName: 'Cond. Pagamento', headerName: 'Cond. Pgto',
width: 150, minWidth: 100,
flex: 1,
description: 'Condição de pagamento', description: 'Condição de pagamento',
}, },
{ {
field: 'amount', field: 'amount',
headerName: 'Valor Total', headerName: 'Valor',
width: 120, width: 100,
align: 'right', align: 'right',
headerAlign: 'right', headerAlign: 'right',
description: 'Valor total do pedido', description: 'Valor total do pedido',
@ -78,14 +75,15 @@ export const createInformationPanelColumns = (): GridColDef[] => [
{ {
field: 'deliveryType', field: 'deliveryType',
headerName: 'Tipo Entrega', headerName: 'Tipo Entrega',
width: 150, minWidth: 100,
flex: 1,
description: 'Tipo de entrega selecionado', description: 'Tipo de entrega selecionado',
}, },
{ {
field: 'deliveryLocal', field: 'deliveryLocal',
headerName: 'Local Entrega', headerName: 'Local Entrega',
width: 200, minWidth: 120,
flex: 1, flex: 1.2,
description: 'Local de entrega do pedido', description: 'Local de entrega do pedido',
}, },
]; ];

View File

@ -30,7 +30,14 @@ export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
if (isLoading) { if (isLoading) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 4 }}> <Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
py: 4,
}}
>
<CircularProgress size={40} /> <CircularProgress size={40} />
<Typography variant="body2" color="text.secondary" sx={{ ml: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ ml: 2 }}>
Carregando itens do pedido... Carregando itens do pedido...
@ -86,7 +93,15 @@ export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
slotProps={{ slotProps={{
pagination: { pagination: {
labelRowsPerPage: 'Itens por página:', labelRowsPerPage: 'Itens por página:',
labelDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) => { labelDisplayedRows: ({
from,
to,
count,
}: {
from: number;
to: number;
count: number;
}) => {
const pageSize = to >= from ? to - from + 1 : 10; const pageSize = to >= from ? to - from + 1 : 10;
const currentPage = Math.floor((from - 1) / pageSize) + 1; const currentPage = Math.floor((from - 1) / pageSize) + 1;
const totalPages = Math.ceil(count / pageSize); const totalPages = Math.ceil(count / pageSize);

View File

@ -1,26 +1,13 @@
{ {
"status": { "status": {
"success": [ "success": ["FATURADO", "F"],
"FATURADO", "error": ["CANCELADO", "C"],
"F"
],
"error": [
"CANCELADO",
"C"
],
"warning": [] "warning": []
}, },
"priority": { "priority": {
"error": [ "error": ["ALTA"],
"ALTA" "warning": ["MÉDIA", "MEDIA"],
], "success": ["BAIXA"],
"warning": [
"MÉDIA",
"MEDIA"
],
"success": [
"BAIXA"
],
"default": [] "default": []
} }
} }

View File

@ -5,9 +5,11 @@ Esta pasta contém a documentação técnica da feature de pedidos do Portal Jur
## Documentos Disponíveis ## Documentos Disponíveis
### [order-items-implementation.md](./order-items-implementation.md) ### [order-items-implementation.md](./order-items-implementation.md)
Documentação completa da implementação da funcionalidade de visualização de itens do pedido. Documentação completa da implementação da funcionalidade de visualização de itens do pedido.
**Conteúdo:** **Conteúdo:**
- Arquitetura da solução - Arquitetura da solução
- Arquivos criados e modificados - Arquivos criados e modificados
- Fluxo de execução - Fluxo de execução
@ -20,9 +22,11 @@ Documentação completa da implementação da funcionalidade de visualização d
## Funcionalidades Documentadas ## Funcionalidades Documentadas
### Tabela de Itens do Pedido ### Tabela de Itens do Pedido
Permite visualizar os produtos/itens de um pedido ao clicar na linha da tabela principal. Permite visualizar os produtos/itens de um pedido ao clicar na linha da tabela principal.
**Arquivos principais:** **Arquivos principais:**
- `schemas/order.item.schema.ts` - Schema de validação - `schemas/order.item.schema.ts` - Schema de validação
- `api/order.service.ts` - Serviço de API - `api/order.service.ts` - Serviço de API
- `hooks/useOrderItems.ts` - Hook React Query - `hooks/useOrderItems.ts` - Hook React Query

View File

@ -25,6 +25,7 @@ graph TD
## Arquivos Criados ## Arquivos Criados
### 1. Schema de Validação ### 1. Schema de Validação
**Arquivo:** `src/features/orders/schemas/order.item.schema.ts` **Arquivo:** `src/features/orders/schemas/order.item.schema.ts`
Define a estrutura de dados dos itens do pedido usando Zod: Define a estrutura de dados dos itens do pedido usando Zod:
@ -51,6 +52,7 @@ export const orderItemSchema = z.object({
--- ---
### 2. Serviço de API ### 2. Serviço de API
**Arquivo:** `src/features/orders/api/order.service.ts` (linha 145) **Arquivo:** `src/features/orders/api/order.service.ts` (linha 145)
Adiciona método para buscar itens do pedido: Adiciona método para buscar itens do pedido:
@ -64,12 +66,13 @@ findOrderItems: async (orderId: number): Promise<OrderItem[]> => {
console.error(`Erro ao buscar itens do pedido ${orderId}:`, error); console.error(`Erro ao buscar itens do pedido ${orderId}:`, error);
return []; return [];
} }
} };
``` ```
**Endpoint:** `GET /api/v1/orders/itens/{orderId}` **Endpoint:** `GET /api/v1/orders/itens/{orderId}`
**Resposta esperada:** **Resposta esperada:**
```json ```json
{ {
"success": true, "success": true,
@ -95,6 +98,7 @@ findOrderItems: async (orderId: number): Promise<OrderItem[]> => {
--- ---
### 3. Hook React Query ### 3. Hook React Query
**Arquivo:** `src/features/orders/hooks/useOrderItems.ts` **Arquivo:** `src/features/orders/hooks/useOrderItems.ts`
Gerencia o estado e cache dos itens: Gerencia o estado e cache dos itens:
@ -114,6 +118,7 @@ export function useOrderItems(orderId: number | null | undefined) {
``` ```
**Características:** **Características:**
- Cache de 5 minutos - Cache de 5 minutos
- Só executa quando há `orderId` válido - Só executa quando há `orderId` válido
- Retry automático (1 tentativa) - Retry automático (1 tentativa)
@ -122,12 +127,13 @@ export function useOrderItems(orderId: number | null | undefined) {
--- ---
### 4. Definição de Colunas ### 4. Definição de Colunas
**Arquivo:** `src/features/orders/components/OrderItemsTableColumns.tsx` **Arquivo:** `src/features/orders/components/OrderItemsTableColumns.tsx`
Define 12 colunas para a tabela: Define 12 colunas para a tabela:
| Campo | Cabeçalho | Tipo | Agregável | Formatação | | Campo | Cabeçalho | Tipo | Agregável | Formatação |
|-------|-----------|------|-----------|------------| | -------------- | -------------- | ------ | --------- | ------------------ |
| `productId` | Cód. Produto | number | Não | - | | `productId` | Cód. Produto | number | Não | - |
| `description` | Descrição | string | Não | - | | `description` | Descrição | string | Não | - |
| `pacth` | Unidade | string | Não | Centralizado | | `pacth` | Unidade | string | Não | Centralizado |
@ -146,6 +152,7 @@ Define 12 colunas para a tabela:
--- ---
### 5. Componente da Tabela ### 5. Componente da Tabela
**Arquivo:** `src/features/orders/components/OrderItemsTable.tsx` **Arquivo:** `src/features/orders/components/OrderItemsTable.tsx`
Componente principal que renderiza a tabela de itens: Componente principal que renderiza a tabela de itens:
@ -158,10 +165,11 @@ interface OrderItemsTableProps {
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => { export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
const { data: items, isLoading, error } = useOrderItems(orderId); const { data: items, isLoading, error } = useOrderItems(orderId);
// ... renderização // ... renderização
} };
``` ```
**Estados:** **Estados:**
- **Loading:** Exibe spinner + mensagem "Carregando itens..." - **Loading:** Exibe spinner + mensagem "Carregando itens..."
- **Erro:** Exibe Alert vermelho com mensagem de erro - **Erro:** Exibe Alert vermelho com mensagem de erro
- **Vazio:** Exibe Alert azul "Nenhum item encontrado" - **Vazio:** Exibe Alert azul "Nenhum item encontrado"
@ -170,16 +178,19 @@ export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
--- ---
### 6. Integração na Tabela Principal ### 6. Integração na Tabela Principal
**Arquivo:** `src/features/orders/components/OrderTable.tsx` **Arquivo:** `src/features/orders/components/OrderTable.tsx`
**Mudanças realizadas:** **Mudanças realizadas:**
#### a) Estado para pedido selecionado #### a) Estado para pedido selecionado
```typescript ```typescript
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null); const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
``` ```
#### b) Handler de clique na linha #### b) Handler de clique na linha
```typescript ```typescript
onRowClick={(params) => { onRowClick={(params) => {
const orderId = params.row.orderId; const orderId = params.row.orderId;
@ -188,6 +199,7 @@ onRowClick={(params) => {
``` ```
#### c) Classe CSS para linha selecionada #### c) Classe CSS para linha selecionada
```typescript ```typescript
getRowClassName={(params) => getRowClassName={(params) =>
params.row.orderId === selectedOrderId ? 'Mui-selected' : '' params.row.orderId === selectedOrderId ? 'Mui-selected' : ''
@ -195,6 +207,7 @@ getRowClassName={(params) =>
``` ```
#### d) Estilo visual da linha selecionada #### d) Estilo visual da linha selecionada
```typescript ```typescript
'& .MuiDataGrid-row.Mui-selected': { '& .MuiDataGrid-row.Mui-selected': {
backgroundColor: 'primary.light', backgroundColor: 'primary.light',
@ -205,6 +218,7 @@ getRowClassName={(params) =>
``` ```
#### e) Renderização condicional da tabela de itens #### e) Renderização condicional da tabela de itens
```typescript ```typescript
{selectedOrderId && <OrderItemsTable orderId={selectedOrderId} />} {selectedOrderId && <OrderItemsTable orderId={selectedOrderId} />}
``` ```
@ -226,11 +240,13 @@ getRowClassName={(params) =>
## Problemas Encontrados e Soluções ## Problemas Encontrados e Soluções
### Problema 1: "Nenhum item encontrado" ### Problema 1: "Nenhum item encontrado"
**Sintoma:** Mesmo com dados na API, a mensagem "Nenhum item encontrado" aparecia. **Sintoma:** Mesmo com dados na API, a mensagem "Nenhum item encontrado" aparecia.
**Causa:** A API retorna campos numéricos como strings (`"123"` ao invés de `123`), mas o schema Zod esperava `number`. **Causa:** A API retorna campos numéricos como strings (`"123"` ao invés de `123`), mas o schema Zod esperava `number`.
**Solução:** Usar `z.coerce.number()` em todos os campos numéricos: **Solução:** Usar `z.coerce.number()` em todos os campos numéricos:
```diff ```diff
- productId: z.number(), - productId: z.number(),
+ productId: z.coerce.number(), + productId: z.coerce.number(),
@ -241,9 +257,11 @@ getRowClassName={(params) =>
--- ---
### Problema 2: Nomes de colunas genéricos ### Problema 2: Nomes de colunas genéricos
**Sintoma:** Colunas com nomes pouco descritivos ("Código", "Lote"). **Sintoma:** Colunas com nomes pouco descritivos ("Código", "Lote").
**Solução:** Renomear baseado nos dados reais: **Solução:** Renomear baseado nos dados reais:
- `Código``Cód. Produto` - `Código``Cód. Produto`
- `Lote``Unidade` (campo contém unidade de medida) - `Lote``Unidade` (campo contém unidade de medida)
- `Estoque``Cód. Estoque` - `Estoque``Cód. Estoque`
@ -254,6 +272,7 @@ getRowClassName={(params) =>
## Estilização ## Estilização
### Cabeçalho da Tabela ### Cabeçalho da Tabela
```typescript ```typescript
'& .MuiDataGrid-columnHeaders': { '& .MuiDataGrid-columnHeaders': {
backgroundColor: 'grey.50', backgroundColor: 'grey.50',
@ -265,6 +284,7 @@ getRowClassName={(params) =>
``` ```
### Linhas ### Linhas
```typescript ```typescript
'& .MuiDataGrid-row': { '& .MuiDataGrid-row': {
minHeight: '36px !important', minHeight: '36px !important',
@ -278,6 +298,7 @@ getRowClassName={(params) =>
``` ```
### Células ### Células
```typescript ```typescript
'& .MuiDataGrid-cell': { '& .MuiDataGrid-cell': {
fontSize: '0.75rem', fontSize: '0.75rem',
@ -311,7 +332,7 @@ getRowClassName={(params) =>
**Resultado na tabela:** **Resultado na tabela:**
| Cód. Produto | Descrição | Unidade | Qtd. | Preço Unitário | Valor Total | Departamento | Marca | | Cód. Produto | Descrição | Unidade | Qtd. | Preço Unitário | Valor Total | Departamento | Marca |
|--------------|-----------|---------|------|----------------|-------------|--------------|-------| | ------------ | -------------------------- | ------- | ---- | -------------- | ----------- | ----------------------- | -------- |
| 2813 | TELHA ONDINA 2,44X50MM 4MM | UN | 1 | R$ 25,99 | R$ 25,99 | MATERIAIS DE CONSTRUCAO | BRASILIT | | 2813 | TELHA ONDINA 2,44X50MM 4MM | UN | 1 | R$ 25,99 | R$ 25,99 | MATERIAIS DE CONSTRUCAO | BRASILIT |
## Checklist de Implementação ## Checklist de Implementação

View File

@ -0,0 +1,16 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { orderService } from '../api/order.service';
/**
* Hook para buscar movimentação de carga de um pedido específico.
*/
export function useCargoMovement(orderId: number) {
return useQuery({
queryKey: ['orderCargoMovement', orderId],
enabled: !!orderId,
queryFn: () => orderService.findCargoMovement(orderId),
staleTime: 1000 * 60 * 5, // 5 minutes
});
}

View File

@ -33,7 +33,8 @@ export function useCustomers(searchTerm: string) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const options = query.data?.map((customer, index) => ({ const options =
query.data?.map((customer, index) => ({
value: customer.id.toString(), value: customer.id.toString(),
label: customer.name, label: customer.name,
id: `customer-${customer.id}-${index}`, id: `customer-${customer.id}-${index}`,
@ -45,4 +46,3 @@ export function useCustomers(searchTerm: string) {
options, options,
}; };
} }

View File

@ -0,0 +1,16 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { orderService } from '../api/order.service';
/**
* Hook para buscar itens de corte de um pedido específico.
*/
export function useCuttingItems(orderId: number) {
return useQuery({
queryKey: ['orderCuttingItems', orderId],
enabled: !!orderId,
queryFn: () => orderService.findCuttingItems(orderId),
staleTime: 1000 * 60 * 5, // 5 minutes
});
}

View File

@ -0,0 +1,17 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { orderService } from '../api/order.service';
/**
* Hook para buscar entregas de um pedido específico.
*/
export function useDelivery(orderId: number) {
return useQuery({
queryKey: ['orderDelivery', orderId],
enabled: !!orderId,
queryFn: () => orderService.findDelivery(orderId, true),
staleTime: 1000 * 60 * 5, // 5 minutes
});
}

View File

@ -5,11 +5,12 @@ import {
parseAsString, parseAsString,
parseAsInteger, parseAsInteger,
parseAsBoolean, parseAsBoolean,
parseAsArrayOf parseAsArrayOf,
} from 'nuqs'; } from 'nuqs';
export const useOrderFilters = () => { export const useOrderFilters = () => {
return useQueryStates({ return useQueryStates(
{
status: parseAsString, status: parseAsString,
sellerName: parseAsString, sellerName: parseAsString,
sellerId: parseAsString, sellerId: parseAsString,
@ -30,8 +31,10 @@ export const useOrderFilters = () => {
createDateEnd: parseAsString, createDateEnd: parseAsString,
searchTriggered: parseAsBoolean.withDefault(false), searchTriggered: parseAsBoolean.withDefault(false),
}, { },
{
shallow: true, shallow: true,
history: 'replace', history: 'replace',
}); }
);
}; };

View File

@ -4,7 +4,6 @@ import { orderService } from '../api/order.service';
import { OrderFilters } from '../schemas/order.schema'; import { OrderFilters } from '../schemas/order.schema';
import { omitBy, isNil } from 'lodash'; import { omitBy, isNil } from 'lodash';
const normalizeFilters = (filters: Record<string, any>): OrderFilters => { const normalizeFilters = (filters: Record<string, any>): OrderFilters => {
const { searchTriggered, ...rest } = filters; const { searchTriggered, ...rest } = filters;
const normalized = omitBy(rest, isNil); const normalized = omitBy(rest, isNil);

View File

@ -11,7 +11,8 @@ export function useSellers() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const options = query.data?.map((seller, index) => { const options =
query.data?.map((seller, index) => {
return { return {
value: seller.id.toString(), value: seller.id.toString(),
label: seller.name, label: seller.name,
@ -22,6 +23,6 @@ export function useSellers() {
return { return {
...query, ...query,
options options,
}; };
} }

View File

@ -6,4 +6,3 @@ export * from './hooks/useOrders';
export * from './types'; export * from './types';
export * from './api/order.service'; export * from './api/order.service';
export { default as OrdersPage } from './pages/OrdersPage'; export { default as OrdersPage } from './pages/OrdersPage';

View File

@ -6,11 +6,8 @@ import { Box, Typography } from '@mui/material';
export default function OrdersPage() { export default function OrdersPage() {
return ( return (
<Box> <Box>
<Typography variant="h4" fontWeight="400" mb={3}> <Typography variant="h4" fontWeight="400" mb={0}></Typography>
Consultas de Pedidos
</Typography>
<OrderTable /> <OrderTable />
</Box> </Box>
); );
} }

View File

@ -30,5 +30,5 @@ export const unwrapApiData = <T>(
fallback: T fallback: T
): T => { ): T => {
const result = schema.safeParse(response?.data); const result = schema.safeParse(response?.data);
return (result.success && result.data.success) ? result.data.data : fallback; return result.success && result.data.success ? result.data.data : fallback;
}; };

View File

@ -72,12 +72,14 @@ export type OrderFilters = z.infer<typeof findOrdersSchema>;
* @returns {boolean} true se o valor é considerado vazio * @returns {boolean} true se o valor é considerado vazio
*/ */
const isEmptyValue = (val: any): boolean => { const isEmptyValue = (val: any): boolean => {
return val === null || return (
val === null ||
val === undefined || val === undefined ||
val === '' || val === '' ||
(typeof val === 'boolean' && !val) || (typeof val === 'boolean' && !val) ||
(typeof val === 'number' && val === 0) || (typeof val === 'number' && val === 0) ||
(Array.isArray(val) && val.length === 0); (Array.isArray(val) && val.length === 0)
);
}; };
/** /**
@ -103,15 +105,18 @@ export const orderApiParamsSchema = findOrdersSchema
.transform((filters) => { .transform((filters) => {
// Mapeamento de chaves que precisam ser renomeadas // Mapeamento de chaves que precisam ser renomeadas
const keyMap: Record<string, string> = { const keyMap: Record<string, string> = {
store: 'codfilial' store: 'codfilial',
}; };
return Object.entries(filters).reduce((acc, [key, value]) => { return Object.entries(filters).reduce(
(acc, [key, value]) => {
// Early return: ignora valores vazios // Early return: ignora valores vazios
if (isEmptyValue(value)) return acc; if (isEmptyValue(value)) return acc;
const apiKey = keyMap[key] ?? key; const apiKey = keyMap[key] ?? key;
acc[apiKey] = formatValueToString(value); acc[apiKey] = formatValueToString(value);
return acc; return acc;
}, {} as Record<string, string>); },
{} as Record<string, string>
);
}); });

View File

@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from 'zod';
/** /**
* Schema for a single order item * Schema for a single order item
@ -18,6 +18,49 @@ export const orderItemSchema = z.object({
brand: z.string(), brand: z.string(),
}); });
export const shipmentSchema = z.object({
placeId: z.number(),
placeName: z.string(),
street: z.string(),
addressNumber: z.string(),
bairro: z.string(),
city: z.string(),
state: z.string(),
addressComplement: z.string().nullable(),
cep: z.string(),
commentOrder1: z.string().nullable(),
commentOrder2: z.string().nullable(),
commentDelivery1: z.string().nullable(),
commentDelivery2: z.string().nullable(),
commentDelivery3: z.string().nullable(),
commentDelivery4: z.string().nullable(),
shippimentId: z.number(),
shippimentDate: z.string(), // Ou z.coerce.date() se quiser converter para Objeto Date
shippimentComment: z.string().nullable(),
place: z.any().nullable(),
driver: z.string(),
car: z.string(),
closeDate: z.string().nullable(),
separatorName: z.string().nullable(),
confName: z.string().nullable(),
releaseDate: z.string().nullable(),
});
export type Shipment = z.infer<typeof shipmentSchema>;
/**
* Schema for the delivery API response
* A API pode retornar data como objeto único ou array
*/
export const shipmentResponseSchema = z.object({
success: z.boolean(),
data: z.union([shipmentSchema, z.array(shipmentSchema)]).transform((data) =>
Array.isArray(data) ? data : [data]
),
});
/** /**
* Schema for the order items API response * Schema for the order items API response
*/ */
@ -26,6 +69,7 @@ export const orderItemsResponseSchema = z.object({
data: z.array(orderItemSchema), data: z.array(orderItemSchema),
}); });
/** /**
* TypeScript type inferred from the Zod schema * TypeScript type inferred from the Zod schema
*/ */
@ -35,3 +79,46 @@ export type OrderItem = z.infer<typeof orderItemSchema>;
* TypeScript type for the API response * TypeScript type for the API response
*/ */
export type OrderItemsResponse = z.infer<typeof orderItemsResponseSchema>; export type OrderItemsResponse = z.infer<typeof orderItemsResponseSchema>;
/**
* Schema for cargo movement/transfer
*/
export const cargoMovementSchema = z.object({
orderId: z.number().nullable(),
transferDate: z.string(),
invoiceId: z.number(),
transactionId: z.number(),
oldShipment: z.number(),
newShipment: z.number(),
transferText: z.string(),
cause: z.string(),
userName: z.string(),
program: z.string(),
});
export type CargoMovement = z.infer<typeof cargoMovementSchema>;
export const cargoMovementResponseSchema = z.object({
success: z.boolean(),
data: z.array(cargoMovementSchema),
});
/**
* Schema for cutting items
*/
export const cuttingItemSchema = z.object({
productId: z.number(),
description: z.string(),
pacth: z.string(),
stockId: z.number(),
saleQuantity: z.number(),
cutQuantity: z.number(),
separedQuantity: z.number(),
});
export type CuttingItem = z.infer<typeof cuttingItemSchema>;
export const cuttingItemResponseSchema = z.object({
success: z.boolean(),
data: z.array(cuttingItemSchema),
});

View File

@ -7,7 +7,12 @@ export { findOrdersSchema, orderApiParamsSchema } from './order-filters.schema';
export type { OrderFilters } from './order-filters.schema'; export type { OrderFilters } from './order-filters.schema';
// Schemas de resposta da API // Schemas de resposta da API
export { orderResponseSchema, ordersResponseSchema, storesResponseSchema, customersResponseSchema } from './api-responses.schema'; export {
orderResponseSchema,
ordersResponseSchema,
storesResponseSchema,
customersResponseSchema,
} from './api-responses.schema';
// Helpers de validação de resposta // Helpers de validação de resposta
export { createApiSchema, unwrapApiData } from './api-response.schema'; export { createApiSchema, unwrapApiData } from './api-response.schema';

View File

@ -11,4 +11,3 @@ export const storeSchema = z.object({
}); });
export type Store = z.infer<typeof storeSchema>; export type Store = z.infer<typeof storeSchema>;

View File

@ -11,9 +11,12 @@ export function useStores() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const options = query.data?.map((store, index) => { const options =
const uniqueId = store.id || store.storeId || store.code || `store-${index}`; query.data?.map((store, index) => {
const label = store.store || store.label || store.name || `Loja ${index + 1}`; const uniqueId =
store.id || store.storeId || store.code || `store-${index}`;
const label =
store.store || store.label || store.name || `Loja ${index + 1}`;
return { return {
value: uniqueId, value: uniqueId,
@ -24,6 +27,6 @@ export function useStores() {
return { return {
...query, ...query,
options options,
}; };
} }

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