Compare commits

..

No commits in common. "2e940404818d14a6d28acb4c5e1ecd972ae0b9ba" and "cf7e56e2b732945ea27de0d0717df38febb08e71" have entirely different histories.

157 changed files with 17464 additions and 22253 deletions

3
.gitignore vendored
View File

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

View File

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

View File

@ -1,20 +0,0 @@
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;

View File

@ -1,14 +0,0 @@
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,13 +23,11 @@ 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
``` ```
@ -44,7 +42,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,10 +3,7 @@
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 { import type { EmotionCache, Options as OptionsOfCreateCache } from '@emotion/cache';
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';
@ -23,9 +20,7 @@ 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( export default function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) {
props: NextAppDirEmotionCacheProviderProps
) {
const { options, CacheProvider = DefaultCacheProvider, children } = props; const { options, CacheProvider = DefaultCacheProvider, children } = props;
const [registry] = useState(() => { const [registry] = useState(() => {

View File

@ -8,17 +8,15 @@ 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 = const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
'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({
children, export default function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
}: 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);
@ -69,24 +67,15 @@ export default function Providers({
}, },
typography: { typography: {
fontFamily: '"Plus Jakarta Sans", "Helvetica", "Arial", sans-serif', fontFamily: '"Plus Jakarta Sans", "Helvetica", "Arial", sans-serif',
h1: { fontWeight: 600, fontSize: '2.25rem', lineHeight: 1.2 }, h1: { fontWeight: 600 },
h2: { fontWeight: 600, fontSize: '1.875rem', lineHeight: 1.2 }, h6: { fontWeight: 600 },
h3: { fontWeight: 600, fontSize: '1.5rem', lineHeight: 1.2 }, body1: { fontSize: '0.875rem', fontWeight: 400 },
h4: { fontWeight: 600, fontSize: '1.3125rem', lineHeight: 1.2 },
h5: { fontWeight: 600, fontSize: '1.125rem', lineHeight: 1.2 },
h6: { fontWeight: 600, fontSize: '1rem', lineHeight: 1.2 },
button: { textTransform: 'none', fontWeight: 500 },
body1: { fontSize: '0.8125rem', fontWeight: 400, lineHeight: 1.5 }, // 13px
body2: { fontSize: '0.75rem', fontWeight: 400, lineHeight: 1.5 }, // 12px
subtitle1: { fontSize: '0.875rem', fontWeight: 400 },
subtitle2: { fontSize: '0.75rem', fontWeight: 400 },
}, },
components: { components: {
MuiCssBaseline: { MuiCssBaseline: {
styleOverrides: { styleOverrides: {
body: { body: {
fontFamily: fontFamily: "var(--font-plus-jakarta), 'Plus Jakarta Sans', sans-serif",
"var(--font-plus-jakarta), 'Plus Jakarta Sans', sans-serif",
}, },
}, },
}, },

View File

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

View File

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

View File

@ -20,21 +20,13 @@ function ProfileContent() {
export default function ProfilePageRoute() { export default function ProfilePageRoute() {
return ( return (
<Suspense <Suspense fallback={
fallback={ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
<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,6 +36,7 @@ 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,5 +1,6 @@
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,9 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format import { defineConfig, globalIgnores } from "eslint/config";
import storybook from "eslint-plugin-storybook"; import nextVitals from "eslint-config-next/core-web-vitals";
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,
@ -11,10 +8,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,9 +1,8 @@
import { LicenseInfo } from '@mui/x-license'; import { LicenseInfo } from '@mui/x-license';
const PERPETUAL_LICENSE_KEY = const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'; const ALTERNATIVE_LICENSE_KEY = '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=';
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,9 +9,7 @@
"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",
@ -44,7 +42,6 @@
"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",
@ -67,19 +64,6 @@
"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,6 +28,7 @@ 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'));
@ -36,15 +37,10 @@ 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 <Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
sx={{ <StyledMain isMobile={!lgUp}>
display: 'flex', {children}
flexDirection: 'column', </StyledMain>
flexGrow: 1,
overflow: 'hidden',
}}
>
<StyledMain isMobile={!lgUp}>{children}</StyledMain>
</Box> </Box>
</Box> </Box>
); );
@ -53,17 +49,13 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
return ( return (
<Box sx={{ display: 'flex', height: '100vh' }}> <Box sx={{ display: 'flex', height: '100vh' }}>
<Sidebar /> <Sidebar />
<Box <Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
}}
>
<Header /> <Header />
<StyledMain isMobile={!lgUp}>{children}</StyledMain> <StyledMain isMobile={!lgUp}>
{children}
</StyledMain>
</Box> </Box>
</Box> </Box>
); );
} }

View File

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

View File

@ -13,3 +13,4 @@ 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,3 +30,4 @@ 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,18 +70,21 @@ 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={() => onClick={() => setDarkMode(activeMode === "light" ? "dark" : "light")}
setDarkMode(activeMode === 'light' ? 'dark' : 'light')
}
aria-label="alternar tema" aria-label="alternar tema"
> >
{activeMode === 'light' ? <DarkModeIcon /> : <LightModeIcon />} {activeMode === "light" ? (
<DarkModeIcon />
) : (
<LightModeIcon />
)}
</IconButton> </IconButton>
<Notifications /> <Notifications />

View File

@ -85,7 +85,8 @@ const MobileRightSidebar = () => {
</List> </List>
</Box> </Box>
<Box px={3} mt={3}></Box> <Box px={3} mt={3}>
</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,17 +99,15 @@ const AppDD = () => {
<Divider orientation="vertical" /> <Divider orientation="vertical" />
</Grid> </Grid>
<Grid size={{ sm: 4 }}> <Grid size={{ sm: 4 }}>
<Box p={4}></Box> <Box p={4}>
</Box>
</Grid> </Grid>
</Grid> </Grid>
</Menu> </Menu>
</Box> </Box>
<Button <Button
color="inherit" color="inherit"
sx={{ sx={{ color: (theme) => theme.palette.text.secondary, fontSize: "13px" }}
color: (theme) => theme.palette.text.secondary,
fontSize: '13px',
}}
variant="text" variant="text"
href="/apps/chats" href="/apps/chats"
component={Link} component={Link}
@ -118,10 +116,7 @@ const AppDD = () => {
</Button> </Button>
<Button <Button
color="inherit" color="inherit"
sx={{ sx={{ color: (theme) => theme.palette.text.secondary, fontSize: "13px" }}
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 '@/shared/components'; import Scrollbar from '../components/Scrollbar';
import { IconBellRinging } from '@tabler/icons-react'; import { IconBellRinging } from '@tabler/icons-react';
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
@ -62,13 +62,7 @@ const Notifications = () => {
}, },
}} }}
> >
<Stack <Stack direction="row" py={2} px={4} justifyContent="space-between" alignItems="center">
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>
@ -114,13 +108,7 @@ const Notifications = () => {
))} ))}
</Scrollbar> </Scrollbar>
<Box p={3} pb={1}> <Box p={3} pb={1}>
<Button <Button href="/apps/email" variant="outlined" component={Link} color="primary" fullWidth>
href="/apps/email"
variant="outlined"
component={Link}
color="primary"
fullWidth
>
See all Notifications See all Notifications
</Button> </Button>
</Box> </Box>

View File

@ -9,17 +9,15 @@ import {
Button, Button,
IconButton, IconButton,
} from '@mui/material'; } from '@mui/material';
import * as dropdownData from './data';
import { IconMail } from '@tabler/icons-react'; import { IconMail } from '@tabler/icons-react';
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
import Image from 'next/image';
import { useAuthStore } from '../../login/store/useAuthStore';
import { useAuth } from '../../login/hooks/useAuth';
const Profile = () => { const Profile = () => {
const [anchorEl2, setAnchorEl2] = useState(null); const [anchorEl2, setAnchorEl2] = useState(null);
const user = useAuthStore((s) => s.user);
const { logout } = useAuth();
const handleClick2 = (event: any) => { const handleClick2 = (event: any) => {
setAnchorEl2(event.currentTarget); setAnchorEl2(event.currentTarget);
}; };
@ -42,14 +40,13 @@ const Profile = () => {
onClick={handleClick2} onClick={handleClick2}
> >
<Avatar <Avatar
src={"/images/profile/user-1.jpg"}
alt={'ProfileImg'}
sx={{ sx={{
width: 35, width: 35,
height: 35, height: 35,
bgcolor: 'primary.main',
}} }}
> />
{user?.nome?.[0] || user?.userName?.[0] || 'U'}
</Avatar>
</IconButton> </IconButton>
{/* ------------------------------------------- */} {/* ------------------------------------------- */}
{/* Message Dropdown */} {/* Message Dropdown */}
@ -71,21 +68,13 @@ 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 <Avatar src={"/images/profile/user-1.jpg"} alt={"ProfileImg"} sx={{ width: 95, height: 95 }} />
sx={{ width: 95, height: 95, bgcolor: 'primary.main' }}
>
{user?.nome?.[0] || user?.userName?.[0] || 'U'}
</Avatar>
<Box> <Box>
<Typography <Typography variant="subtitle2" color="textPrimary" fontWeight={600}>
variant="subtitle2" Mathew Anderson
color="textPrimary"
fontWeight={600}
>
{user?.nome || user?.userName || 'Usuário'}
</Typography> </Typography>
<Typography variant="subtitle2" color="textSecondary"> <Typography variant="subtitle2" color="textSecondary">
{user?.nomeFilial || 'Sem filial'} Designer
</Typography> </Typography>
<Typography <Typography
variant="subtitle2" variant="subtitle2"
@ -95,18 +84,80 @@ const Profile = () => {
gap={1} gap={1}
> >
<IconMail width={15} height={15} /> <IconMail width={15} height={15} />
{user?.rca ? `RCA: ${user.rca}` : 'Sem e-mail'} info@modernize.com
</Typography> </Typography>
</Box> </Box>
</Stack> </Stack>
<Box mt={2}> <Divider />
<Button {dropdownData.profile.map((profile) => (
variant="outlined" <Box key={profile.title}>
color="primary" <Box sx={{ py: 2, px: 0 }} className="hover-text-primary">
onClick={logout} <Link href={profile.href}>
fullWidth <Stack direction="row" spacing={2}>
<Box
width="45px"
height="45px"
bgcolor="primary.light"
display="flex"
alignItems="center"
justifyContent="center" flexShrink="0"
> >
Sair <Avatar
src={profile.icon}
alt={profile.icon}
sx={{
width: 24,
height: 24,
borderRadius: 0,
}}
/>
</Box>
<Box>
<Typography
variant="subtitle2"
fontWeight={600}
color="textPrimary"
className="text-hover"
noWrap
sx={{
width: '240px',
}}
>
{profile.title}
</Typography>
<Typography
color="textSecondary"
variant="subtitle2"
sx={{
width: '240px',
}}
noWrap
>
{profile.subtitle}
</Typography>
</Box>
</Stack>
</Link>
</Box>
</Box>
))}
<Box mt={2}>
<Box bgcolor="primary.light" p={3} mb={3} overflow="hidden" position="relative">
<Box display="flex" justifyContent="space-between">
<Box>
<Typography variant="h5" mb={2}>
Unlimited <br />
Access
</Typography>
<Button variant="contained" color="primary">
Upgrade
</Button>
</Box>
<Image src={"/images/backgrounds/unlimited-bg.png"} width={150} height={183} style={{ height: 'auto', width: 'auto' }} alt="unlimited" className="signup-bg" />
</Box>
</Box>
<Button href="/auth/auth1/login" variant="outlined" color="primary" component={Link} fullWidth>
Logout
</Button> </Button>
</Box> </Box>
</Menu> </Menu>

View File

@ -36,9 +36,7 @@ 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.title ? t.href.toLocaleLowerCase().includes(cSearch.toLocaleLowerCase()) : '',
? t.href.toLocaleLowerCase().includes(cSearch.toLocaleLowerCase())
: ''
); );
return rotr; return rotr;
@ -91,11 +89,7 @@ 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 <ListItemButton sx={{ py: 0.5, px: 1 }} href={menu?.href} component={Link}>
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,2 +1,3 @@
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,3 +8,4 @@ export default function DashboardPage() {
</DashboardLayout> </DashboardLayout>
); );
} }

View File

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

View File

@ -43,9 +43,7 @@ const SidebarItems = ({ open, onItemClick }: SidebarItemsProps) => {
{Menuitems.map((item) => { {Menuitems.map((item) => {
// SubHeader // SubHeader
if (item.subheader) { if (item.subheader) {
return ( return <NavGroup item={item} hideMenu={hideMenu} key={item.subheader} />;
<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,13 +31,8 @@ interface StyledListItemButtonProps {
} }
const StyledListItemButton = styled(ListItemButton, { const StyledListItemButton = styled(ListItemButton, {
shouldForwardProp: (prop) => shouldForwardProp: (prop) => prop !== 'open' && prop !== 'active' && prop !== 'level' && prop !== 'hideMenu',
prop !== 'open' && })<StyledListItemButtonProps>(({ theme, open, active, level = 1, hideMenu }) => ({
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',
@ -45,8 +40,7 @@ const StyledListItemButton = styled(ListItemButton, {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
borderRadius: '7px', borderRadius: '7px',
'&:hover': { '&:hover': {
backgroundColor: backgroundColor: active || open
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,
@ -57,8 +51,7 @@ 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',
@ -135,7 +128,9 @@ export default function NavCollapse({
hideMenu={hideMenu} hideMenu={hideMenu}
key={menu?.id} key={menu?.id}
> >
<StyledListItemIcon>{menuIcon}</StyledListItemIcon> <StyledListItemIcon>
{menuIcon}
</StyledListItemIcon>
<ListItemText color="inherit"> <ListItemText color="inherit">
{hideMenu ? '' : <>{menu.title}</>} {hideMenu ? '' : <>{menu.title}</>}
</ListItemText> </ListItemText>

View File

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

View File

@ -28,18 +28,14 @@ interface StyledListItemButtonProps {
} }
const StyledListItemButton = styled(ListItemButton, { const StyledListItemButton = styled(ListItemButton, {
shouldForwardProp: (prop) => shouldForwardProp: (prop) => prop !== 'active' && prop !== 'level' && prop !== 'hideMenu',
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: color: active && level > 1 ? `${theme.palette.primary.main}!important` : theme.palette.text.secondary,
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,
@ -65,8 +61,7 @@ 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: color: active && level > 1 ? `${theme.palette.primary.main}!important` : 'inherit',
active && level > 1 ? `${theme.palette.primary.main}!important` : 'inherit',
})); }));
export default function NavItem({ export default function NavItem({

View File

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

View File

@ -27,14 +27,12 @@ export const useCustomizerStore = create<CustomizerState>()(
setDarkMode: (mode) => set({ activeMode: mode }), setDarkMode: (mode) => set({ activeMode: mode }),
toggleSidebar: () => toggleSidebar: () => set((state) => ({
set((state) => ({ isCollapse: !state.isCollapse
isCollapse: !state.isCollapse,
})), })),
toggleMobileSidebar: () => toggleMobileSidebar: () => set((state) => ({
set((state) => ({ isMobileSidebar: !state.isMobileSidebar
isMobileSidebar: !state.isMobileSidebar,
})), })),
setHydrated: () => set({ isHydrated: true }), setHydrated: () => set({ isHydrated: true }),
@ -44,7 +42,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,8 +1,4 @@
import axios, { import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from '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;
@ -28,9 +24,7 @@ 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 & { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
_retry?: boolean;
};
if (!originalRequest) { if (!originalRequest) {
throw error; throw error;
@ -40,3 +34,4 @@ const handleResponseError = async (error: AxiosError) => {
}; };
authApi.interceptors.response.use((response) => response, handleResponseError); authApi.interceptors.response.use((response) => response, handleResponseError);

View File

@ -0,0 +1,204 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AuthLogin from './AuthLogin';
import { useAuth } from '../hooks/useAuth';
// Mock do hook useAuth
jest.mock('../hooks/useAuth');
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe('AuthLogin Component', () => {
const mockLoginMutation = {
mutate: jest.fn(),
isPending: false,
isError: false,
isSuccess: false,
error: null,
};
beforeEach(() => {
jest.clearAllMocks();
(useAuth as jest.Mock).mockReturnValue({
loginMutation: mockLoginMutation,
});
});
describe('Renderização', () => {
it('deve renderizar formulário de login', () => {
render(<AuthLogin title="Login" />, { wrapper: createWrapper() });
expect(screen.getByLabelText(/usuário/i)).toBeInTheDocument();
expect(screen.getByLabelText(/senha/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
it('deve renderizar título quando fornecido', () => {
render(<AuthLogin title="Bem-vindo" />, { wrapper: createWrapper() });
expect(screen.getByText('Bem-vindo')).toBeInTheDocument();
});
it('deve renderizar checkbox "Manter-me conectado"', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
expect(screen.getByText(/manter-me conectado/i)).toBeInTheDocument();
});
it('deve renderizar link "Esqueceu sua senha"', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const link = screen.getByText(/esqueceu sua senha/i);
expect(link).toBeInTheDocument();
});
});
describe('Validação', () => {
it('deve validar campos obrigatórios', async () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/usuário é obrigatório/i)).toBeInTheDocument();
});
});
it('deve validar senha mínima', async () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const usernameInput = screen.getByLabelText(/usuário/i);
const passwordInput = screen.getByLabelText(/senha/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
fireEvent.change(passwordInput, { target: { value: '123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/senha deve ter no mínimo 4 caracteres/i)).toBeInTheDocument();
});
});
});
describe('Submissão', () => {
it('deve submeter formulário com credenciais válidas', async () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const usernameInput = screen.getByLabelText(/usuário/i);
const passwordInput = screen.getByLabelText(/senha/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockLoginMutation.mutate).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123',
});
});
});
});
describe('Estados de Loading e Erro', () => {
it('deve desabilitar botão durante loading', () => {
const loadingMutation = {
...mockLoginMutation,
isPending: true,
};
(useAuth as jest.Mock).mockReturnValue({
loginMutation: loadingMutation,
});
render(<AuthLogin />, { wrapper: createWrapper() });
const submitButton = screen.getByRole('button', { name: /logging in/i });
expect(submitButton).toBeDisabled();
});
it('deve mostrar mensagem de erro quando login falha', () => {
const errorMutation = {
...mockLoginMutation,
isError: true,
error: {
response: {
data: { message: 'Credenciais inválidas' },
},
},
};
(useAuth as jest.Mock).mockReturnValue({
loginMutation: errorMutation,
});
render(<AuthLogin />, { wrapper: createWrapper() });
expect(screen.getByText(/credenciais inválidas/i)).toBeInTheDocument();
});
// 🐛 TESTE QUE REVELA BUG: Erro não limpa durante nova tentativa
it('🐛 BUG: deve esconder erro durante nova tentativa de login', () => {
const errorAndLoadingMutation = {
...mockLoginMutation,
isError: true,
isPending: true, // Está tentando novamente
error: {
response: {
data: { message: 'Credenciais inválidas' },
},
},
};
(useAuth as jest.Mock).mockReturnValue({
loginMutation: errorAndLoadingMutation,
});
render(<AuthLogin />, { wrapper: createWrapper() });
// ❌ ESTE TESTE VAI FALHAR - erro ainda aparece durante loading!
expect(screen.queryByText(/credenciais inválidas/i)).not.toBeInTheDocument();
});
});
describe('🐛 Bugs Identificados', () => {
// 🐛 BUG: Link "Esqueceu senha" vai para home
it('🐛 BUG: link "Esqueceu senha" deve ir para /forgot-password', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const link = screen.getByText(/esqueceu sua senha/i).closest('a');
// ❌ ESTE TESTE VAI FALHAR - href é "/"
expect(link).toHaveAttribute('href', '/forgot-password');
});
// 🐛 BUG: Checkbox não funciona
it('🐛 BUG: checkbox "Manter-me conectado" deve ser controlado', () => {
render(<AuthLogin />, { wrapper: createWrapper() });
const checkbox = screen.getByRole('checkbox', { name: /manter-me conectado/i });
// Checkbox está sempre marcado
expect(checkbox).toBeChecked();
// Tenta desmarcar
fireEvent.click(checkbox);
// ❌ ESTE TESTE VAI FALHAR - checkbox não muda de estado!
expect(checkbox).not.toBeChecked();
});
});
});

View File

@ -1,17 +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 Stack from '@mui/material/Stack'; import FormControlLabel from "@mui/material/FormControlLabel";
import { TextField, Alert } from '@mui/material'; import FormGroup from "@mui/material/FormGroup";
import Typography from '@mui/material/Typography'; import Stack from "@mui/material/Stack";
import NextLink from 'next/link'; import { TextField, Alert } from "@mui/material";
import { useForm } from 'react-hook-form'; import Typography from "@mui/material/Typography";
import { zodResolver } from '@hookform/resolvers/zod'; import NextLink from "next/link";
import { loginSchema, LoginInput, AuthLoginProps } from '../interfaces/types'; import { useForm } from "react-hook-form";
import { useAuth } from '../hooks/useAuth'; import { zodResolver } from "@hookform/resolvers/zod";
import CustomFormLabel from '../components/forms/theme-elements/CustomFormLabel'; import { loginSchema, LoginInput, AuthLoginProps } from "../interfaces/types";
import AuthSocialButtons from './AuthSocialButtons'; import { useAuth } from "../hooks/useAuth";
import CustomCheckbox from "../components/forms/theme-elements/CustomCheckbox";
import CustomFormLabel from "../components/forms/theme-elements/CustomFormLabel";
import AuthSocialButtons from "./AuthSocialButtons";
const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => { const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
const { loginMutation } = useAuth(); const { loginMutation } = useAuth();
@ -23,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: "",
}, },
}); });
@ -42,7 +45,7 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
{subtext} {subtext}
<AuthSocialButtons title="Entrar com" /> <AuthSocialButtons title="Sign in with" />
<Box mt={4} mb={2}> <Box mt={4} mb={2}>
<Divider> <Divider>
<Typography <Typography
@ -64,12 +67,8 @@ 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 { const axiosError = error as { response?: { data?: { message?: string } } };
response?: { data?: { message?: string } }; return axiosError.response?.data?.message || 'Erro ao realizar login';
};
return (
axiosError.response?.data?.message || 'Erro ao realizar login'
);
} }
return 'Erro ao realizar login'; return 'Erro ao realizar login';
})()} })()}
@ -103,23 +102,28 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
/> />
</Box> </Box>
<Stack <Stack
justifyContent="flex-end" justifyContent="space-between"
direction="row" direction="row"
alignItems="center" alignItems="center"
my={2} my={2}
spacing={1} spacing={1}
flexWrap="wrap"
> >
<FormGroup>
<FormControlLabel
control={<CustomCheckbox defaultChecked />}
label="Manter-me conectado"
sx={{ whiteSpace: 'nowrap' }}
/>
</FormGroup>
<Typography <Typography
fontWeight="500" fontWeight="500"
sx={{ sx={{
textDecoration: 'none', textDecoration: "none",
color: 'primary.main', color: "primary.main",
}} }}
> >
<NextLink <NextLink href="/" style={{ textDecoration: 'none', color: 'inherit' }}>
href="/"
style={{ textDecoration: 'none', color: 'inherit' }}
>
Esqueceu sua senha ? Esqueceu sua senha ?
</NextLink> </NextLink>
</Typography> </Typography>
@ -134,7 +138,7 @@ const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
type="submit" type="submit"
disabled={loginMutation.isPending} disabled={loginMutation.isPending}
> >
{loginMutation.isPending ? 'Entrando...' : 'Entrar'} {loginMutation.isPending ? 'Logging in...' : 'Sign In'}
</Button> </Button>
</Box> </Box>
</form> </form>

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,20 +18,11 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
}; };
return ( return (
<> <>
<Stack <Stack direction="row" justifyContent="center" spacing={2} mt={3} flexWrap="wrap">
direction="row" <CustomSocialButton onClick={handleGoogleSignIn} sx={{ flex: 1, minWidth: '140px' }}>
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,
@ -41,21 +32,18 @@ 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 <CustomSocialButton onClick={handleGithubSignIn} sx={{ flex: 1, minWidth: '140px' }}>
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,
@ -65,9 +53,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
@ -76,6 +64,6 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
</Stack> </Stack>
</> </>
); );
}; }
export default AuthSocialButtons; export default AuthSocialButtons;

View File

@ -1,5 +1,4 @@
'use client'; 'use client';
import { Box, CircularProgress, Typography } from '@mui/material';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useAuthStore } from '../store/useAuthStore'; import { useAuthStore } from '../store/useAuthStore';
@ -18,6 +17,7 @@ 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();
@ -35,56 +35,12 @@ export function AuthInitializer({ children }: { children: React.ReactNode }) {
if (isChecking) { if (isChecking) {
return ( return (
<Box <div className="flex h-screen w-screen items-center justify-center bg-background">
sx={{ <div className="animate-pulse flex flex-col items-center gap-4">
display: 'flex', <div className="h-12 w-12 rounded-full bg-primary/20" />
height: '100vh', <p className="text-sm text-muted-foreground">Validando acesso...</p>
width: '100vw', </div>
alignItems: 'center', </div>
justifyContent: 'center',
bgcolor: 'background.default',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 2,
position: 'relative',
}}
>
<Box sx={{ position: 'relative', display: 'flex' }}>
<CircularProgress
variant="determinate"
sx={{
color: (theme) => theme.palette.grey[200],
}}
size={48}
thickness={4}
value={100}
/>
<CircularProgress
variant="indeterminate"
disableShrink
sx={{
color: (theme) => theme.palette.primary.main,
animationDuration: '550ms',
position: 'absolute',
left: 0,
[`& .MuiCircularProgress-circle`]: {
strokeLinecap: 'round',
},
}}
size={48}
thickness={4}
/>
</Box>
<Typography variant="body2" color="textSecondary">
Validando acesso...
</Typography>
</Box>
</Box>
); );
} }

View File

@ -12,8 +12,7 @@ import AuthLogin from '../authForms/AuthLogin';
const GradientGrid = styled(Grid)(({ theme }) => ({ const GradientGrid = styled(Grid)(({ theme }) => ({
position: 'relative', position: 'relative',
backgroundColor: '#d2f1df', backgroundColor: '#d2f1df',
backgroundImage: backgroundImage: 'linear-gradient(45deg, #d2f1df 0%, #d3d7fa 50%, #bad8f4 100%)',
'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',
@ -26,12 +25,7 @@ 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 <Grid container spacing={0} justifyContent="center" sx={{ height: '100vh', width: '100%' }}>
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"
@ -60,12 +54,7 @@ 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 <Box display="flex" justifyContent="center" alignItems="center" sx={{ backgroundColor: 'white' }}>
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={
@ -77,14 +66,7 @@ export default function Login() {
color: 'primary.main', color: 'primary.main',
}} }}
> >
<NextLink <NextLink href="/auth/auth1/register" style={{ textDecoration: 'none', color: 'inherit', whiteSpace: 'nowrap' }}>
href="/auth/auth1/register"
style={{
textDecoration: 'none',
color: 'inherit',
whiteSpace: 'nowrap',
}}
>
Criar uma conta Criar uma conta
</NextLink> </NextLink>
</Typography> </Typography>
@ -96,5 +78,5 @@ export default function Login() {
</Grid> </Grid>
</Grid> </Grid>
</PageContainer> </PageContainer>
); )
} };

View File

@ -6,6 +6,10 @@ type Props = {
title?: string; title?: string;
}; };
const PageContainer = ({ children }: Props) => <div>{children}</div>; const PageContainer = ({ children }: Props) => (
<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,10 +31,7 @@ const DashboardCard = ({
return ( return (
<Card <Card
sx={{ sx={{ padding: 0, border: !isCardShadow ? `1px solid ${borderColor}` : 'none' }}
padding: 0,
border: !isCardShadow ? `1px solid ${borderColor}` : 'none',
}}
elevation={isCardShadow ? 9 : 0} elevation={isCardShadow ? 9 : 0}
variant={!isCardShadow ? 'outlined' : undefined} variant={!isCardShadow ? 'outlined' : undefined}
> >
@ -46,7 +43,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,11 +1,7 @@
import { useEffect, ReactElement } from 'react'; import { useEffect, ReactElement } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
export default function ScrollToTop({ export default function ScrollToTop({ children }: { children: ReactElement | null }) {
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,10 +21,7 @@ const BpIcon = styled('span')(({ theme }) => ({
outlineOffset: 2, outlineOffset: 2,
}, },
'input:hover ~ &': { 'input:hover ~ &': {
backgroundColor: backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary : theme.palette.primary,
theme.palette.mode === 'dark'
? theme.palette.primary
: theme.palette.primary,
}, },
'input:disabled ~ &': { 'input:disabled ~ &': {
boxShadow: 'none', boxShadow: 'none',
@ -57,9 +54,7 @@ function CustomCheckbox(props: CheckboxProps) {
checkedIcon={ checkedIcon={
<BpCheckedIcon <BpCheckedIcon
sx={{ sx={{
backgroundColor: props.color backgroundColor: props.color ? `${props.color}.main` : 'primary.main',
? `${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,8 +2,7 @@ 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} />)( const CustomTextField = styled((props: any) => <TextField {...props} />)(({ theme }) => ({
({ 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',
@ -15,7 +14,6 @@ const CustomTextField = styled((props: any) => <TextField {...props} />)(
'& .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

@ -4,25 +4,25 @@ Este documento descreve os contratos de interface para a API de Autenticação.
Base URL: /api/v1/auth (ou /api/auth) Base URL: /api/v1/auth (ou /api/auth)
1. Realizar Login 1. Realizar Login
Autentica o usuário e retorna os tokens de acesso. Autentica o usuário e retorna os tokens de acesso.
Método: POST Método: POST
Endpoint: /login Endpoint: /login
Content-Type: application/json Content-Type: application/json
Request Body Request Body
{ {
"username": "usuario.sistema", "username": "usuario.sistema",
"password": "senha_secreta" "password": "senha_secreta"
} }
Response (200 OK) Response (200 OK)
Retorna o Access Token no corpo e configura o Refresh Token como um Cookie HttpOnly. Retorna o Access Token no corpo e configura o Refresh Token como um Cookie HttpOnly.
{ {
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"type": "Bearer", "type": "Bearer",
"expiresIn": 900, "expiresIn": 900,
"refreshToken": "550e8400-e29b-41d4-a716-446655440000", "refreshToken": "550e8400-e29b-41d4-a716-446655440000",
"username": "usuario.sistema" "username": "usuario.sistema"
} }
Cookies Set: Cookies Set:
@ -30,7 +30,8 @@ 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. 2. Renovar Token (Refresh) 401/500: Usuário ou senha inválidos.
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
@ -40,7 +41,7 @@ O Refresh Token pode ser enviado de duas formas (nesta ordem de prioridade):
Body JSON: Body JSON:
{ {
"refreshToken": "550e8400-e29b-41d4-a716-446655440000" "refreshToken": "550e8400-e29b-41d4-a716-446655440000"
} }
Cookie Cookie
refreshToken refreshToken
@ -49,14 +50,15 @@ Response (200 OK)
Retorna novos tokens e atualiza o cookie. Retorna novos tokens e atualiza o cookie.
{ {
"token": "novatoken...", "token": "novatoken...",
"type": "Bearer", "type": "Bearer",
"expiresIn": 900, "expiresIn": 900,
"refreshToken": "novorefreshtoken...", "refreshToken": "novorefreshtoken...",
"username": "usuario.sistema" "username": "usuario.sistema"
} }
Erros Comuns Erros Comuns
403 Forbidden: Token expirado ou inválido. 3. Obter Usuário Atual (Me) 403 Forbidden: Token expirado ou inválido.
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
@ -64,20 +66,21 @@ Endpoint: /me
Headers: Authorization: Bearer <access_token> Headers: Authorization: Bearer <access_token>
Response (200 OK) Response (200 OK)
{ {
"matricula": 12345, "matricula": 12345,
"userName": "usuario.sistema", "userName": "usuario.sistema",
"nome": "João da Silva", "nome": "João da Silva",
"codigoFilial": "1", "codigoFilial": "1",
"nomeFilial": "Matriz", "nomeFilial": "Matriz",
"rca": 100, "rca": 100,
"discountPercent": 0, "discountPercent": 0,
"sectorId": 10, "sectorId": 10,
"sectorManagerId": 50, "sectorManagerId": 50,
"supervisorId": 55 "supervisorId": 55
} }
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. 4. Logout 404 Not Found: Usuário não encontrado no banco.
4. Logout
Invalida a sessão atual. Invalida a sessão atual.
Método: POST Método: POST
@ -87,11 +90,11 @@ Request (Opcional)
Pode enviar o refresh token no corpo para forçar sua invalidação, caso não esteja no cookie. Pode enviar o refresh token no corpo para forçar sua invalidação, caso não esteja no cookie.
{ {
"refreshToken": "..." "refreshToken": "..."
} }
Response (200 OK) Response (200 OK)
{ {
"message": "Logout realizado com sucesso" "message": "Logout realizado com sucesso"
} }
Effect: Effect:

View File

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

View File

@ -1,11 +0,0 @@
// 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,17 +2,9 @@ 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 { import { loginSchema, tokenResponseSchema, logoutResponseSchema } from '../schemas/schemas';
loginSchema,
tokenResponseSchema,
logoutResponseSchema,
} from '../schemas/schemas';
export { export { loginSchema, tokenResponseSchema, logoutResponseSchema } from '../schemas/schemas';
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,11 +3,12 @@ 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,7 +31,8 @@ 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,5 +1,6 @@
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,9 +1,4 @@
import axios, { import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from '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;
@ -67,10 +62,7 @@ export function handleTokenRefresh<T = unknown>(
throw error; throw error;
} }
if ( if (request.url?.includes('/auth/login') || request.url?.includes('/auth/refresh')) {
request.url?.includes('/auth/login') ||
request.url?.includes('/auth/refresh')
) {
throw error; throw error;
} }

View File

@ -1,12 +1,5 @@
import axios, { import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';
AxiosError, import { getAccessToken, handleTokenRefresh } from '../../login/utils/tokenRefresh';
AxiosInstance,
InternalAxiosRequestConfig,
} from 'axios';
import {
getAccessToken,
handleTokenRefresh,
} from '../../login/utils/tokenRefresh';
import { import {
OrderFilters, OrderFilters,
orderApiParamsSchema, orderApiParamsSchema,
@ -15,18 +8,9 @@ import {
storesResponseSchema, storesResponseSchema,
customersResponseSchema, customersResponseSchema,
sellersResponseSchema, sellersResponseSchema,
unwrapApiData, unwrapApiData
} from '../schemas/order.schema'; } from '../schemas/order.schema';
import { import { orderItemsResponseSchema, OrderItem } from '../schemas/order.item.schema';
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';
@ -69,9 +53,7 @@ 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 & { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
_retry?: boolean;
};
if (!originalRequest) { if (!originalRequest) {
throw error; throw error;
@ -80,10 +62,7 @@ const handleResponseError = async (error: AxiosError) => {
return handleTokenRefresh(error, originalRequest, ordersApi); return handleTokenRefresh(error, originalRequest, ordersApi);
}; };
ordersApi.interceptors.response.use( ordersApi.interceptors.response.use((response) => response, handleResponseError);
(response) => response,
handleResponseError
);
export const orderService = { export const orderService = {
/** /**
@ -94,11 +73,14 @@ export const orderService = {
* @returns {Promise<Order[]>} Array de pedidos que correspondem aos filtros * @returns {Promise<Order[]>} Array de pedidos que correspondem aos filtros
*/ */
findOrders: async (filters: OrderFilters): Promise<Order[]> => { findOrders: async (filters: OrderFilters): Promise<Order[]> => {
try {
const cleanParams = orderApiParamsSchema.parse(filters); const cleanParams = orderApiParamsSchema.parse(filters);
const response = await ordersApi.get('/api/v1/orders/find', { const response = await ordersApi.get('/api/v1/orders/find', { params: cleanParams });
params: cleanParams,
});
return unwrapApiData(response, ordersResponseSchema, []); return unwrapApiData(response, ordersResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar pedidos:', error);
return [];
}
}, },
/** /**
@ -108,8 +90,13 @@ export const orderService = {
* @returns {Promise<Order | null>} O pedido com o ID especificado, ou null se não encontrado * @returns {Promise<Order | null>} O pedido com o ID especificado, ou null se não encontrado
*/ */
findById: async (id: number): Promise<Order | null> => { findById: async (id: number): Promise<Order | null> => {
try {
const response = await ordersApi.get(`/orders/${id}`); const response = await ordersApi.get(`/orders/${id}`);
return unwrapApiData(response, orderResponseSchema, null); return unwrapApiData(response, orderResponseSchema, null);
} catch (error) {
console.error(`Erro ao buscar pedido ${id}:`, error);
return null;
}
}, },
/** /**
@ -118,8 +105,13 @@ export const orderService = {
* @returns {Promise<Store[]>} Array de todas as lojas, ou array vazio se nenhuma for encontrada * @returns {Promise<Store[]>} Array de todas as lojas, ou array vazio se nenhuma for encontrada
*/ */
findStores: async (): Promise<Store[]> => { findStores: async (): Promise<Store[]> => {
try {
const response = await ordersApi.get('/api/v1/data-consult/stores'); const response = await ordersApi.get('/api/v1/data-consult/stores');
return unwrapApiData(response, storesResponseSchema, []); return unwrapApiData(response, storesResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar lojas:', error);
return [];
}
}, },
/** /**
@ -129,54 +121,34 @@ 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 ( findCustomers: async (name: string): Promise<Array<{ id: number; name: string; estcob: string }>> => {
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 {
const response = await ordersApi.get( const response = await ordersApi.get(`/api/v1/clientes/${encodeURIComponent(name)}`);
`/api/v1/clientes/${encodeURIComponent(name)}`
);
return unwrapApiData(response, customersResponseSchema, []); return unwrapApiData(response, customersResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar clientes:', error);
return [];
}
}, },
findSellers: async (): Promise<Seller[]> => { findsellers: async (): Promise<Seller[]> => {
try {
const response = await ordersApi.get('/api/v1/data-consult/sellers'); const response = await ordersApi.get('/api/v1/data-consult/sellers');
return unwrapApiData(response, sellersResponseSchema, []); return unwrapApiData(response, sellersResponseSchema, []);
} catch (error) {
console.error('Erro ao buscar vendedores:', error);
return [];
}
}, },
findOrderItems: async (orderId: number): Promise<OrderItem[]> => { findOrderItems: async (orderId: number): Promise<OrderItem[]> => {
try {
const response = await ordersApi.get(`/api/v1/orders/itens/${orderId}`); const response = await ordersApi.get(`/api/v1/orders/itens/${orderId}`);
return unwrapApiData(response, orderItemsResponseSchema, []); return unwrapApiData(response, orderItemsResponseSchema, []);
}, } catch (error) {
console.error(`Erro ao buscar itens do pedido ${orderId}:`, error);
return [];
findDelivery: async (
orderId: number,
includeCompletedDeliveries: boolean = true
): Promise<Shipment[]> => {
const response = await ordersApi.get(
`/api/v1/orders/delivery/${orderId}`,
{
params: { includeCompletedDeliveries },
} }
);
return unwrapApiData(response, shipmentResponseSchema, []);
}, },
findCargoMovement: async (orderId: number): Promise<CargoMovement[]> => {
const response = await ordersApi.get(
`/api/v1/orders/transfer/${orderId}`
);
return unwrapApiData(response, cargoMovementResponseSchema, []);
},
findCuttingItems: async (orderId: number): Promise<CuttingItem[]> => {
const response = await ordersApi.get(
`/api/v1/orders/cut-itens/${orderId}`
);
return unwrapApiData(response, cuttingItemResponseSchema, []);
},
}; };

View File

@ -1,14 +1,7 @@
'use client'; 'use client';
import TablePagination from '@mui/material/TablePagination'; import TablePagination from '@mui/material/TablePagination';
import { import { useGridApiContext, useGridSelector, gridPageCountSelector, gridPageSelector, gridPageSizeSelector, gridRowCountSelector } from '@mui/x-data-grid-premium';
useGridApiContext,
useGridSelector,
gridPageCountSelector,
gridPageSelector,
gridPageSizeSelector,
gridRowCountSelector,
} from '@mui/x-data-grid-premium';
function CustomPagination() { function CustomPagination() {
const apiRef = useGridApiContext(); const apiRef = useGridApiContext();
@ -17,15 +10,7 @@ function CustomPagination() {
const pageSize = useGridSelector(apiRef, gridPageSizeSelector); const pageSize = useGridSelector(apiRef, gridPageSizeSelector);
const rowCount = useGridSelector(apiRef, gridRowCountSelector); const rowCount = useGridSelector(apiRef, gridRowCountSelector);
const labelDisplayedRows = ({ const labelDisplayedRows = ({ from, to, count }: { from: number; to: number; count: number }) => {
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}`;
@ -38,9 +23,7 @@ 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) => onRowsPerPageChange={(event) => apiRef.current.setPageSize(Number.parseInt(event.target.value, 10))}
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,8 +13,8 @@ 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 '@/shared/components'; import { TabPanel } from './TabPanel';
import { OrderItemsTable } from './tabs/OrderItemsTable'; import { OrderItemsTable } from './OrderItemsTable';
import { PreBoxPanel } from './tabs/PreBoxPanel'; import { PreBoxPanel } from './tabs/PreBoxPanel';
import { InformationPanel } from './tabs/InformationPanel'; import { InformationPanel } from './tabs/InformationPanel';
import { TimelinePanel } from './tabs/TimelinePanel'; import { TimelinePanel } from './tabs/TimelinePanel';

View File

@ -0,0 +1,209 @@
'use client';
import { useMemo } from 'react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert';
import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useOrderItems } from '../hooks/useOrderItems';
import { createOrderItemsColumns } from './OrderItemsTableColumns';
import { OrderItem } from '../schemas/order.item.schema';
interface OrderItemsTableProps {
orderId: number;
}
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
const { data: items, isLoading, error } = useOrderItems(orderId);
const columns = useMemo(() => createOrderItemsColumns(), []);
const rows = useMemo(() => {
if (!Array.isArray(items) || items.length === 0) return [];
return items.map((item: OrderItem, index: number) => ({
id: `${orderId}-${item.productId}-${index}`,
...item,
}));
}, [items, orderId]);
if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 4 }}>
<CircularProgress size={40} />
<Typography variant="body2" color="text.secondary" sx={{ ml: 2 }}>
Carregando itens do pedido...
</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{ mt: 2 }}>
<Alert severity="error">
{error instanceof Error
? `Erro ao carregar itens: ${error.message}`
: 'Erro ao carregar itens do pedido.'}
</Alert>
</Box>
);
}
if (!items || items.length === 0) {
return (
<Box sx={{ mt: 2 }}>
<Alert severity="info">Nenhum item encontrado para este pedido.</Alert>
</Box>
);
}
return (
<Box>
<Paper sx={{ boxShadow: 'none', border: 'none', backgroundColor: 'transparent', overflow: 'hidden' }}>
<DataGridPremium
rows={rows}
columns={columns}
density="compact"
autoHeight
hideFooter={rows.length <= 10}
initialState={{
pagination: {
paginationModel: {
pageSize: 10,
page: 0,
},
},
}}
pageSizeOptions={[10, 25, 50]}
sx={{
border: '1px solid',
borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
backgroundColor: 'background.paper',
'& .MuiDataGrid-root': {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
border: 'none',
},
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'grey.50',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',
borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
minHeight: '40px !important',
maxHeight: '40px !important',
},
'& .MuiDataGrid-columnHeader': {
borderRight: '1px solid',
borderColor: 'divider',
paddingLeft: '12px',
paddingRight: '12px',
'&:focus': {
outline: 'none',
},
'&:focus-within': {
outline: 'none',
},
'&:last-of-type': {
borderRight: 'none',
},
},
'& .MuiDataGrid-row': {
borderBottom: '1px solid',
borderColor: 'divider',
backgroundColor: 'background.paper',
minHeight: '36px !important',
maxHeight: '36px !important',
'&:hover': {
backgroundColor: 'action.hover',
},
'&:last-child': {
borderBottom: 'none',
},
},
'& .MuiDataGrid-row:nth-of-type(even)': {
backgroundColor: 'grey.50',
'&:hover': {
backgroundColor: 'action.hover',
},
},
'& .MuiDataGrid-cell': {
borderRight: '1px solid',
borderColor: 'divider',
borderBottom: 'none',
paddingLeft: '12px',
paddingRight: '12px',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
fontSize: '0.75rem',
lineHeight: 1.2,
'&:focus': {
outline: 'none',
},
'&:focus-within': {
outline: 'none',
},
'&:last-of-type': {
borderRight: 'none',
},
},
'& .MuiDataGrid-footerContainer': {
borderTop: '2px solid',
borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
minHeight: '48px !important',
fontSize: '0.75rem',
backgroundColor: 'grey.50',
},
'& .MuiDataGrid-aggregationColumnHeader': {
backgroundColor: 'grey.100',
fontWeight: 600,
fontSize: '0.75rem',
borderBottom: '2px solid',
borderColor: 'divider',
},
'& .MuiDataGrid-aggregationRow': {
backgroundColor: 'grey.100',
borderTop: '2px solid',
borderColor: 'divider',
minHeight: '40px !important',
maxHeight: '40px !important',
},
'& .MuiDataGrid-aggregationCell': {
borderRight: '1px solid',
borderColor: 'divider',
paddingLeft: '12px',
paddingRight: '12px',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
fontSize: '0.75rem',
fontWeight: 600,
'&:last-of-type': {
borderRight: 'none',
},
},
}}
localeText={{
noRowsLabel: 'Nenhum item encontrado.',
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
footerTotalRows: 'Total de itens:',
footerTotalVisibleRows: (visibleCount, totalCount) =>
`${visibleCount.toLocaleString()} de ${totalCount.toLocaleString()}`,
}}
slotProps={{
pagination: {
labelRowsPerPage: 'Itens por página:',
labelDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) => {
const pageSize = to >= from ? to - from + 1 : 10;
const currentPage = Math.floor((from - 1) / pageSize) + 1;
const totalPages = Math.ceil(count / pageSize);
return `${from}${to} de ${count} | Página ${currentPage} de ${totalPages}`;
},
},
}}
/>
</Paper>
</Box>
);
};

View File

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

View File

@ -1,18 +1,14 @@
'use client'; 'use client';
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState } from 'react';
import { SearchBar } from './SearchBar'; import { SearchBar } from './SearchBar';
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 Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import { import { DataGridPremium, GridCellSelectionModel } from '@mui/x-data-grid-premium';
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';
@ -21,41 +17,28 @@ import { OrderDetailsTabs } from './OrderDetailsTabs';
export const OrderTable = () => { export const OrderTable = () => {
const { data: orders, isLoading, error } = useOrders(); const { data: orders, isLoading, error } = useOrders();
const { data: stores } = useStores(); const [cellSelectionModel, setCellSelectionModel] = useState<GridCellSelectionModel>({});
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) => return orders.map((order: Order, index: number) => normalizeOrder(order, index));
normalizeOrder(order, index)
);
}, [orders]); }, [orders]);
const columns = useMemo(() => createOrderColumns({ storesMap }), [storesMap]); const columns = useMemo(
() => createOrderColumns(),
[]
);
if (isLoading) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 10, gap: 2 }}>
<CircularProgress size={80} />
<Typography variant="body2" color="text.secondary">Buscando pedidos...</Typography>
</Box>
);
}
if (error) { if (error) {
return ( return (
@ -71,24 +54,11 @@ 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 <Paper sx={{ mt: 3, boxShadow: 'none', border: 'none', backgroundColor: 'transparent', overflow: 'hidden' }}>
sx={{
mt: { xs: 2, md: 3 },
boxShadow: 'none',
border: 'none',
backgroundColor: 'transparent',
overflow: 'hidden',
}}
>
<DataGridPremium <DataGridPremium
rows={rows} rows={rows}
columns={columns} columns={columns}
@ -110,7 +80,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]}
@ -125,7 +95,7 @@ export const OrderTable = () => {
params.row.orderId === selectedOrderId ? 'Mui-selected' : '' params.row.orderId === selectedOrderId ? 'Mui-selected' : ''
} }
sx={{ sx={{
height: { xs: mobileTableHeight, md: tableHeight }, height: tableHeight,
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
@ -135,7 +105,7 @@ export const OrderTable = () => {
border: 'none', border: 'none',
}, },
'& .MuiDataGrid-columnHeaders': { '& .MuiDataGrid-columnHeaders': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50', backgroundColor: 'grey.50',
fontWeight: 600, fontWeight: 600,
fontSize: '0.75rem', fontSize: '0.75rem',
borderBottom: '2px solid', borderBottom: '2px solid',
@ -180,7 +150,7 @@ export const OrderTable = () => {
}, },
}, },
'& .MuiDataGrid-row:nth-of-type(even)': { '& .MuiDataGrid-row:nth-of-type(even)': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.50', backgroundColor: 'grey.50',
'&:hover': { '&:hover': {
backgroundColor: 'action.hover', backgroundColor: 'action.hover',
}, },
@ -226,10 +196,10 @@ export const OrderTable = () => {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
minHeight: '48px !important', minHeight: '48px !important',
fontSize: '0.75rem', fontSize: '0.75rem',
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50', backgroundColor: 'grey.50',
}, },
'& .MuiDataGrid-aggregationColumnHeader': { '& .MuiDataGrid-aggregationColumnHeader': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100', backgroundColor: 'grey.100',
fontWeight: 600, fontWeight: 600,
fontSize: '0.75rem', fontSize: '0.75rem',
borderBottom: '2px solid', borderBottom: '2px solid',
@ -239,7 +209,7 @@ export const OrderTable = () => {
fontWeight: 600, fontWeight: 600,
}, },
'& .MuiDataGrid-aggregationRow': { '& .MuiDataGrid-aggregationRow': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100', backgroundColor: 'grey.100',
borderTop: '2px solid', borderTop: '2px solid',
borderColor: 'divider', borderColor: 'divider',
minHeight: '40px !important', minHeight: '40px !important',
@ -267,13 +237,13 @@ export const OrderTable = () => {
opacity: 1, opacity: 1,
}, },
'& .MuiDataGrid-pinnedColumnHeaders': { '& .MuiDataGrid-pinnedColumnHeaders': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50', backgroundColor: 'grey.50',
}, },
'& .MuiDataGrid-pinnedColumns': { '& .MuiDataGrid-pinnedColumns': {
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
}, },
'& .MuiDataGrid-pinnedColumnHeader': { '& .MuiDataGrid-pinnedColumnHeader': {
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50', backgroundColor: 'grey.50',
fontWeight: 600, fontWeight: 600,
fontSize: '0.75rem', fontSize: '0.75rem',
borderBottom: '2px solid', borderBottom: '2px solid',
@ -290,8 +260,7 @@ export const OrderTable = () => {
}, },
}} }}
localeText={{ localeText={{
noRowsLabel: noRowsLabel: 'Nenhum pedido encontrado para os filtros selecionados.',
'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) =>
@ -301,18 +270,11 @@ 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: ({ labelDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) => {
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,133 +3,10 @@ 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 { import { formatDate, formatDateTime, formatCurrency, formatNumber } from '../utils/orderFormatters';
formatDate, import { getStatusChipProps, getPriorityChipProps } from '../utils/tableHelpers';
formatDateTime,
formatCurrency,
formatNumber,
} from '../utils/orderFormatters';
import {
getStatusChipProps,
getPriorityChipProps,
} from '../utils/tableHelpers';
const CELL_FONT_SIZE = '0.75rem';
const CAPTION_FONT_SIZE = '0.6875rem';
const CHIP_STYLES = {
fontSize: CAPTION_FONT_SIZE,
height: 22,
fontWeight: 300,
} as const;
const CHIP_PRIORITY_STYLES = {
...CHIP_STYLES,
maxWidth: '100%',
'& .MuiChip-label': {
px: 1,
py: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
} as const;
interface CellTextProps {
value: unknown;
secondary?: boolean;
fontWeight?: number;
}
const CellText = ({ value, secondary = false, fontWeight }: CellTextProps) => (
<Typography
variant="body2"
color={secondary ? 'text.secondary' : 'text.primary'}
sx={{ fontSize: CELL_FONT_SIZE, fontWeight }}
noWrap
>
{String(value ?? '-')}
</Typography>
);
interface CellNumericProps {
value: unknown;
formatter?: (val: number) => string;
secondary?: boolean;
fontWeight?: number;
}
const CellNumeric = ({
value,
formatter,
secondary = false,
fontWeight,
}: CellNumericProps) => (
<Typography
variant="body2"
color={secondary ? 'text.secondary' : 'text.primary'}
sx={{ fontSize: CELL_FONT_SIZE, fontWeight, textAlign: 'right' }}
>
{formatter ? formatter(value as number) : String(value ?? '-')}
</Typography>
);
interface CellDateProps {
value: unknown;
showTime?: boolean;
time?: string;
}
const CellDate = ({ value, showTime = false, time }: CellDateProps) => {
const dateStr = formatDate(value as string | undefined);
if (!showTime && !time) {
return (
<Typography variant="body2" sx={{ fontSize: CELL_FONT_SIZE }}>
{dateStr}
</Typography>
);
}
const timeStr = time || formatDateTime(value as string | undefined);
return (
<Box>
<Typography variant="body2" sx={{ fontSize: CELL_FONT_SIZE }}>
{dateStr}
</Typography>
{timeStr && (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontSize: CAPTION_FONT_SIZE }}
>
{timeStr}
</Typography>
)}
</Box>
);
};
interface CreateOrderColumnsOptions {
storesMap?: Map<string, string>;
}
export const createOrderColumns = (
options?: CreateOrderColumnsOptions
): GridColDef[] => {
const storesMap = options?.storesMap;
return [
export const createOrderColumns = (): GridColDef[] => [
{ {
field: 'orderId', field: 'orderId',
headerName: 'Pedido', headerName: 'Pedido',
@ -138,7 +15,9 @@ export const createOrderColumns = (
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellNumeric value={params.value} fontWeight={500} /> <Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}>
{params.value}
</Typography>
), ),
}, },
{ {
@ -146,12 +25,22 @@ export const createOrderColumns = (
headerName: 'Data', headerName: 'Data',
width: 160, width: 160,
minWidth: 140, minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => {
<CellDate value={params.value} showTime /> const dateTime = formatDateTime(params.value);
), return (
<Box>
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{formatDate(params.value)}
</Typography>
{dateTime && (
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.6875rem' }}>
{dateTime}
</Typography>
)}
</Box>
);
},
}, },
{ {
field: 'customerName', field: 'customerName',
headerName: 'Cliente', headerName: 'Cliente',
@ -159,44 +48,33 @@ export const createOrderColumns = (
minWidth: 250, minWidth: 250,
flex: 1, flex: 1,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{
field: 'customerId',
headerName: 'Código Cliente',
width: 120,
minWidth: 110,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellNumeric value={params.value} secondary />
),
},
{ {
field: 'storeId', field: 'storeId',
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); <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
const storeName = storesMap?.get(storeId) || storeId; {params.value}
return <CellText value={storeName} />; </Typography>
}, ),
}, },
{ {
field: 'store', field: 'store',
headerName: 'Supervisor', headerName: 'Filial Faturamento',
width: 200, width: 200,
minWidth: 180, minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{ {
field: 'status', field: 'status',
headerName: 'Situação', headerName: 'Situação',
@ -212,7 +90,11 @@ export const createOrderColumns = (
label={chipProps.label} label={chipProps.label}
color={chipProps.color} color={chipProps.color}
size="small" size="small"
sx={CHIP_STYLES} sx={{
fontSize: '0.6875rem',
height: 22,
fontWeight: 300,
}}
/> />
</Tooltip> </Tooltip>
); );
@ -224,11 +106,11 @@ export const createOrderColumns = (
width: 140, width: 140,
minWidth: 120, minWidth: 120,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{ {
field: 'amount', field: 'amount',
headerName: 'Valor Total', headerName: 'Valor Total',
@ -240,7 +122,55 @@ export const createOrderColumns = (
align: 'right', align: 'right',
valueFormatter: (value) => formatCurrency(value as number), valueFormatter: (value) => formatCurrency(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellNumeric value={params.value} formatter={formatCurrency} fontWeight={500} /> <Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}>
{formatCurrency(params.value)}
</Typography>
),
},
{
field: 'invoiceNumber',
headerName: 'Nota Fiscal',
width: 120,
minWidth: 110,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
{params.value && params.value !== '-' ? params.value : '-'}
</Typography>
),
},
{
field: 'billingId',
headerName: 'Cobrança',
width: 120,
minWidth: 110,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value || '-'}
</Typography>
),
},
{
field: 'sellerName',
headerName: 'Vendedor',
width: 200,
minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
),
},
{
field: 'deliveryType',
headerName: 'Tipo de Entrega',
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{ {
@ -254,30 +184,9 @@ export const createOrderColumns = (
align: 'right', align: 'right',
valueFormatter: (value) => formatNumber(value as number), valueFormatter: (value) => formatNumber(value as number),
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellNumeric value={params.value} formatter={formatNumber} /> <Typography variant="body2" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
), {formatNumber(params.value)}
}, </Typography>
{
field: 'invoiceNumber',
headerName: 'Nota Fiscal',
width: 120,
minWidth: 110,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => {
const value = params.value && params.value !== '-' ? params.value : '-';
return <CellNumeric value={value} />;
},
},
{
field: 'invoiceDate',
headerName: 'Data Faturamento',
width: 150,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellDate value={params.value} time={params.row.invoiceTime} />
), ),
}, },
{ {
@ -286,69 +195,22 @@ export const createOrderColumns = (
width: 160, width: 160,
minWidth: 140, minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{ {
field: 'billingId', field: 'customerId',
headerName: 'Cobrança', headerName: 'Código Cliente',
width: 120, width: 120,
minWidth: 110, minWidth: 110,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'paymentName',
headerName: 'Pagamento',
width: 140,
minWidth: 130,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'sellerName',
headerName: 'Vendedor',
width: 200,
minWidth: 180,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'sellerId',
headerName: 'RCA',
width: 100,
minWidth: 90,
headerAlign: 'right', headerAlign: 'right',
align: 'right', align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellNumeric value={params.value} secondary /> <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
), {params.value || '-'}
}, </Typography>
{
field: 'codusur2Name',
headerName: 'RCA 2',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
),
},
{
field: 'deliveryType',
headerName: 'Tipo de Entrega',
width: 160,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
), ),
}, },
{ {
@ -357,7 +219,9 @@ export const createOrderColumns = (
width: 130, width: 130,
minWidth: 120, minWidth: 120,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellDate value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{params.value ? formatDate(params.value) : '-'}
</Typography>
), ),
}, },
{ {
@ -366,7 +230,9 @@ export const createOrderColumns = (
width: 180, width: 180,
minWidth: 160, minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{ {
@ -379,7 +245,15 @@ export const createOrderColumns = (
const chipProps = getPriorityChipProps(priority); const chipProps = getPriorityChipProps(priority);
if (!chipProps) { if (!chipProps) {
return <CellText value="-" secondary />; return (
<Typography
variant="body2"
sx={{ fontSize: '0.75rem', color: 'text.secondary' }}
noWrap
>
-
</Typography>
);
} }
return ( return (
@ -388,19 +262,111 @@ export const createOrderColumns = (
label={chipProps.label} label={chipProps.label}
color={chipProps.color} color={chipProps.color}
size="small" size="small"
sx={CHIP_PRIORITY_STYLES} sx={{
fontSize: '0.6875rem',
height: 22,
fontWeight: 300,
maxWidth: '100%',
'& .MuiChip-label': {
px: 1,
py: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
}}
/> />
</Tooltip> </Tooltip>
); );
}, },
}, },
{
field: 'invoiceDate',
headerName: 'Data Faturamento',
width: 150,
minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => {
const dateStr = formatDate(params.value);
const timeStr = params.row.invoiceTime;
return (
<Typography variant="body2" sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
{dateStr}
{timeStr && (
<Box component="span" sx={{ color: 'text.secondary', fontSize: '0.6875rem', ml: 0.5 }}>
{timeStr}
</Box>
)}
</Typography>
);
},
},
{
field: 'invoiceTime',
headerName: 'Hora Faturamento',
width: 120,
minWidth: 110,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{params.value || '-'}
</Typography>
),
},
{ {
field: 'confirmDeliveryDate', field: 'confirmDeliveryDate',
headerName: 'Data Confirmação Entrega', headerName: 'Data Confirmação Entrega',
width: 150, width: 150,
minWidth: 140, minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellDate value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
{params.value ? formatDate(params.value) : '-'}
</Typography>
),
},
{
field: 'paymentName',
headerName: 'Pagamento',
width: 140,
minWidth: 130,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
),
},
{
field: 'sellerId',
headerName: 'RCA',
width: 100,
minWidth: 90,
headerAlign: 'right',
align: 'right',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
{params.value || '-'}
</Typography>
),
},
{
field: 'partnerName',
headerName: 'Parceiro',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
),
},
{
field: 'codusur2Name',
headerName: 'RCA 2',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
{ {
@ -409,18 +375,9 @@ export const createOrderColumns = (
width: 160, width: 160,
minWidth: 140, minWidth: 140,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
), {params.value}
}, </Typography>
{
field: 'partnerName',
headerName: 'Parceiro',
width: 180,
minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} />
), ),
}, },
{ {
@ -429,8 +386,10 @@ export const createOrderColumns = (
width: 180, width: 180,
minWidth: 160, minWidth: 160,
renderCell: (params: Readonly<GridRenderCellParams>) => ( renderCell: (params: Readonly<GridRenderCellParams>) => (
<CellText value={params.value} /> <Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
{params.value}
</Typography>
), ),
}, },
]; ];
};

View File

@ -1,8 +1,7 @@
'use client'; 'use client';
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback } from 'react';
import { useOrderFilters } from '../hooks/useOrderFilters'; import { useOrderFilters } from '../hooks/useOrderFilters';
import { useOrders } from '../hooks/useOrders';
import { import {
Box, Box,
TextField, TextField,
@ -13,9 +12,6 @@ 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';
@ -34,63 +30,19 @@ import 'moment/locale/pt-br';
moment.locale('pt-br'); moment.locale('pt-br');
interface LocalFilters {
status: string | null;
orderId: number | null;
customerId: number | null;
customerName: string | null;
createDateIni: string | null;
createDateEnd: string | null;
store: string[] | null;
stockId: string[] | null;
sellerId: string | null;
sellerName: string | null;
}
const getInitialLocalFilters = (
urlFilters: Partial<LocalFilters>
): LocalFilters => ({
status: urlFilters.status ?? null,
orderId: urlFilters.orderId ?? null,
customerId: urlFilters.customerId ?? null,
customerName: urlFilters.customerName ?? null,
createDateIni: urlFilters.createDateIni ?? null,
createDateEnd: urlFilters.createDateEnd ?? null,
store: urlFilters.store ?? null,
stockId: urlFilters.stockId ?? null,
sellerId: urlFilters.sellerId ?? null,
sellerName: urlFilters.sellerName ?? null,
});
export const SearchBar = () => { export const SearchBar = () => {
const [urlFilters, setUrlFilters] = useOrderFilters(); const [filters, setFilters] = useOrderFilters();
const [localFilters, setLocalFilters] = useState<LocalFilters>(() =>
getInitialLocalFilters(urlFilters)
);
const stores = useStores(); const stores = useStores();
const sellers = useSellers(); const sellers = useSellers();
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 { isFetching } = useOrders();
const [touchedFields, setTouchedFields] = useState<{ const [touchedFields, setTouchedFields] = useState<{
createDateIni?: boolean; createDateIni?: boolean;
createDateEnd?: boolean; createDateEnd?: boolean;
}>({}); }>({});
useEffect(() => {
setLocalFilters(getInitialLocalFilters(urlFilters));
}, [urlFilters]);
const updateLocalFilter = useCallback(
<K extends keyof LocalFilters>(key: K, value: LocalFilters[K]) => {
setLocalFilters((prev) => ({ ...prev, [key]: value }));
},
[]
);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
setTouchedFields({}); setTouchedFields({});
setCustomerSearchTerm(''); setCustomerSearchTerm('');
@ -114,70 +66,58 @@ export const SearchBar = () => {
customerName: null, customerName: null,
}; };
setLocalFilters(getInitialLocalFilters(resetState)); setFilters(resetState);
setUrlFilters(resetState); }, [setFilters]);
}, [setUrlFilters]);
const validateDates = useCallback(() => { const validateDates = useCallback(() => {
if (!localFilters.createDateIni || !localFilters.createDateEnd) { if (!filters.createDateIni || !filters.createDateEnd) {
return null; return null;
} }
const dateIni = moment(localFilters.createDateIni, 'YYYY-MM-DD'); const dateIni = moment(filters.createDateIni, 'YYYY-MM-DD');
const dateEnd = moment(localFilters.createDateEnd, 'YYYY-MM-DD'); const dateEnd = moment(filters.createDateEnd, 'YYYY-MM-DD');
if (dateEnd.isBefore(dateIni)) { if (dateEnd.isBefore(dateIni)) {
return 'Data final não pode ser anterior à data inicial'; return 'Data final não pode ser anterior à data inicial';
} }
return null; return null;
}, [localFilters.createDateIni, localFilters.createDateEnd]); }, [filters.createDateIni, filters.createDateEnd]);
const handleFilter = useCallback(() => { const handleFilter = useCallback(() => {
if (!localFilters.createDateIni) { if (!filters.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;
} }
setUrlFilters({ setFilters({
...localFilters, ...filters,
searchTriggered: true, searchTriggered: true,
}); });
}, [localFilters, setUrlFilters, validateDates]); }, [filters, setFilters, validateDates]);
const handleKeyDown = useCallback( const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const isValid = !!localFilters.createDateIni; const isValid = !!filters.createDateIni;
const dateErr = validateDates(); const dateErr = validateDates();
if (isValid && !dateErr) { if (isValid && !dateErr) {
handleFilter(); handleFilter();
} }
} }
}, }, [filters.createDateIni, validateDates, handleFilter]);
[localFilters.createDateIni, validateDates, handleFilter]
);
const isDateValid = !!localFilters.createDateIni; const isDateValid = !!filters.createDateIni;
const dateError = validateDates(); const dateError = validateDates();
const showDateIniError = const showDateIniError = touchedFields.createDateIni && !filters.createDateIni;
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: { xs: 2, md: 3 }, p: 3,
mb: 2, mb: 2,
bgcolor: 'background.paper', bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
@ -186,7 +126,8 @@ export const SearchBar = () => {
elevation={0} elevation={0}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<Grid container spacing={{ xs: 1.5, md: 2 }} alignItems="flex-end"> <Grid container spacing={2} alignItems="flex-end">
{/* --- Primary Filters (Always Visible) --- */} {/* --- Primary Filters (Always Visible) --- */}
{/* Campo de Texto Simples (Nº Pedido) */} {/* Campo de Texto Simples (Nº Pedido) */}
@ -197,10 +138,10 @@ export const SearchBar = () => {
variant="outlined" variant="outlined"
size="small" size="small"
type="number" type="number"
value={localFilters.orderId ?? ''} value={filters.orderId ?? ''}
onChange={(e) => { onChange={(e) => {
const value = e.target.value ? Number(e.target.value) : null; const value = e.target.value ? Number(e.target.value) : null;
updateLocalFilter('orderId', value); setFilters({ orderId: value });
}} }}
slotProps={{ htmlInput: { min: 0 } }} slotProps={{ htmlInput: { min: 0 } }}
placeholder="Ex: 12345" placeholder="Ex: 12345"
@ -214,10 +155,10 @@ export const SearchBar = () => {
fullWidth fullWidth
label="Situação" label="Situação"
size="small" size="small"
value={localFilters.status ?? ''} value={filters.status ?? ''}
onChange={(e) => { onChange={(e) => {
const value = e.target.value || null; const value = e.target.value || null;
updateLocalFilter('status', value); setFilters({ status: value });
}} }}
> >
<MenuItem value="">Todos</MenuItem> <MenuItem value="">Todos</MenuItem>
@ -228,35 +169,36 @@ export const SearchBar = () => {
</Grid> </Grid>
{/* Autocomplete do MUI para Cliente */} {/* Autocomplete do MUI para Cliente */}
<Grid size={{ xs: 12, sm: 6, md: 2 }}> <Grid size={{ xs: 12, sm: 6, md: 2.5 }}>
<Autocomplete <Autocomplete
size="small" size="small"
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={ value={customers.options.find(option =>
customers.options.find( filters.customerId === option.customer?.id
(option) => localFilters.customerId === option.customer?.id ) || null}
) || null
}
onChange={(_, newValue) => { onChange={(_, newValue) => {
if (!newValue) { if (!newValue) {
updateLocalFilter('customerName', null); setFilters({
updateLocalFilter('customerId', null); customerName: null,
customerId: null,
});
setCustomerSearchTerm(''); setCustomerSearchTerm('');
return; return;
} }
updateLocalFilter('customerId', newValue.customer?.id || null); setFilters({
updateLocalFilter( customerId: newValue.customer?.id || null,
'customerName', customerName: newValue.customer?.name || null,
newValue.customer?.name || null });
);
}} }}
onInputChange={(_, newInputValue, reason) => { onInputChange={(_, newInputValue, reason) => {
if (reason === 'clear') { if (reason === 'clear') {
updateLocalFilter('customerName', null); setFilters({
updateLocalFilter('customerId', null); customerName: null,
customerId: null,
});
setCustomerSearchTerm(''); setCustomerSearchTerm('');
return; return;
} }
@ -264,25 +206,23 @@ export const SearchBar = () => {
if (reason === 'input') { if (reason === 'input') {
setCustomerSearchTerm(newInputValue); setCustomerSearchTerm(newInputValue);
if (!newInputValue) { if (!newInputValue) {
updateLocalFilter('customerName', null); setFilters({
updateLocalFilter('customerId', null); customerName: null,
customerId: null,
});
setCustomerSearchTerm(''); setCustomerSearchTerm('');
} }
} }
}} }}
loading={customers.isLoading} loading={customers.isLoading}
renderInput={(params: AutocompleteRenderInputParams) => ( renderInput={(params: Readonly<any>) => (
<TextField <TextField
{...params} {...params}
label="Cliente" label="Cliente"
placeholder="Digite para buscar..." placeholder="Digite para buscar..."
/> />
)} )}
noOptionsText={ noOptionsText={customerSearchTerm.length < 2 ? 'Digite pelo menos 2 caracteres' : 'Nenhum cliente encontrado'}
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}
@ -292,96 +232,59 @@ export const SearchBar = () => {
</Grid> </Grid>
{/* Campos de Data */} {/* Campos de Data */}
<Grid size={{ xs: 12, sm: 12, md: 3.5 }}> <Grid size={{ xs: 12, sm: 12, md: 4 }}>
<LocalizationProvider <LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="pt-br">
dateAdapter={AdapterMoment} <Box display="flex" gap={2}>
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={ value={filters.createDateIni ? moment(filters.createDateIni, 'YYYY-MM-DD') : null}
localFilters.createDateIni
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
: null
}
onChange={(date: moment.Moment | null) => { onChange={(date: moment.Moment | null) => {
setTouchedFields((prev) => ({ setTouchedFields(prev => ({ ...prev, createDateIni: true }));
...prev, setFilters({
createDateIni: true, createDateIni: date ? date.format('YYYY-MM-DD') : null,
})); });
updateLocalFilter(
'createDateIni',
date ? date.format('YYYY-MM-DD') : null
);
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
maxDate={ maxDate={filters.createDateEnd ? moment(filters.createDateEnd, 'YYYY-MM-DD') : undefined}
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 helperText: showDateIniError ? 'Data inicial é obrigatória' : '',
? 'Data inicial é obrigatória' onBlur: () => setTouchedFields(prev => ({ ...prev, createDateIni: true })),
: '',
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 (opcional)" label="Data Final"
value={ value={filters.createDateEnd ? moment(filters.createDateEnd, 'YYYY-MM-DD') : null}
localFilters.createDateEnd
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
: null
}
onChange={(date: moment.Moment | null) => { onChange={(date: moment.Moment | null) => {
setTouchedFields((prev) => ({ setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
...prev, setFilters({
createDateEnd: true, createDateEnd: date ? date.format('YYYY-MM-DD') : null,
})); });
updateLocalFilter(
'createDateEnd',
date ? date.format('YYYY-MM-DD') : null
);
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
minDate={ minDate={filters.createDateIni ? moment(filters.createDateIni, 'YYYY-MM-DD') : undefined}
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: () => onBlur: () => setTouchedFields(prev => ({ ...prev, createDateEnd: true })),
setTouchedFields((prev) => ({
...prev,
createDateEnd: true,
})),
inputProps: { inputProps: {
placeholder: 'Opcional', placeholder: 'Opcional',
}, }
}, }
}} }}
/> />
</Box> </Box>
@ -389,97 +292,46 @@ export const SearchBar = () => {
</LocalizationProvider> </LocalizationProvider>
</Grid> </Grid>
{/* Botão Mais Filtros - inline com filtros primários */} {/* Botões de Ação */}
<Grid <Grid size={{ xs: 12, sm: 12, md: 1.5 }} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
size={{ xs: 12, sm: 12, md: 2.5 }} <Box sx={{ display: 'flex', gap: 1 }}>
sx={{ display: 'flex', alignItems: 'flex-end', justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
>
<Badge
badgeContent={advancedFiltersCount}
color="primary"
invisible={advancedFiltersCount === 0}
>
<Button
size="small"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
endIcon={
showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />
}
aria-label={showAdvancedFilters ? 'Ocultar filtros avançados' : 'Mostrar filtros avançados'}
sx={{ textTransform: 'none', color: 'text.secondary', minHeight: 40 }}
>
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
</Button>
</Badge>
</Grid>
{/* Botões de Ação - nova linha abaixo */}
<Grid
size={{ xs: 12 }}
sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}
>
<Box sx={{ display: 'flex', gap: 1, width: { xs: '100%', sm: 'auto' }, flexWrap: 'nowrap' }}>
<Tooltip title="Limpar filtros" arrow> <Tooltip title="Limpar filtros" arrow>
<span> <span>
<Button <Button
variant="outlined" variant="outlined"
color="inherit" color="inherit"
size="small"
onClick={handleReset} onClick={handleReset}
aria-label="Limpar filtros"
sx={{ sx={{
minWidth: { xs: 'auto', sm: 90 }, minWidth: 40,
minHeight: 32,
px: 1.5, px: 1.5,
flexShrink: 0,
flex: { xs: 1, sm: 'none' },
fontSize: '0.8125rem',
'&:hover': { '&:hover': {
bgcolor: 'action.hover', bgcolor: 'action.hover',
}, }
}} }}
> >
<ResetIcon sx={{ mr: 0.5, fontSize: 18 }} /> <ResetIcon />
Limpar
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={ title={isDateValid ? 'Buscar pedidos' : 'Preencha a data inicial para buscar'}
isDateValid
? 'Buscar pedidos'
: 'Preencha a data inicial para buscar'
}
arrow arrow
> >
<span> <span>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
size="small"
onClick={handleFilter} onClick={handleFilter}
disabled={!isDateValid || !!dateError || isFetching} disabled={!isDateValid || !!dateError}
aria-label="Buscar pedidos"
sx={{ sx={{
minWidth: { xs: 'auto', sm: 90 }, minWidth: 40,
minHeight: 32,
px: 1.5, px: 1.5,
flexShrink: 0,
flex: { xs: 1, sm: 'none' },
fontSize: '0.8125rem',
'&:disabled': { '&:disabled': {
opacity: 0.6, opacity: 0.6,
}, }
}} }}
> >
{isFetching ? ( <SearchIcon />
<CircularProgress size={16} color="inherit" />
) : (
<>
<SearchIcon sx={{ mr: 0.5, fontSize: 18 }} />
Buscar
</>
)}
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
@ -488,6 +340,16 @@ 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 }}>
<Button
size="small"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
endIcon={showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />}
sx={{ textTransform: 'none', color: 'text.secondary' }}
>
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
</Button>
</Box>
<Collapse in={showAdvancedFilters}> <Collapse in={showAdvancedFilters}>
<Grid container spacing={2} sx={{ pt: 2 }}> <Grid container spacing={2} sx={{ pt: 2 }}>
{/* Autocomplete do MUI para Múltiplas Filiais (codfilial) */} {/* Autocomplete do MUI para Múltiplas Filiais (codfilial) */}
@ -497,28 +359,21 @@ export const SearchBar = () => {
size="small" size="small"
options={stores.options} options={stores.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) => option.id === value.id}
option.id === value.id value={stores.options.filter(option =>
} filters.store?.includes(option.value)
value={stores.options.filter((option) =>
localFilters.store?.includes(option.value)
)} )}
onChange={(_, newValue) => { onChange={(_, newValue) => {
updateLocalFilter( setFilters({
'store', store: newValue.map(option => option.value),
newValue.map((option) => option.value) });
);
}} }}
loading={stores.isLoading} loading={stores.isLoading}
renderInput={(params: AutocompleteRenderInputParams) => ( renderInput={(params: Readonly<any>) => (
<TextField <TextField
{...params} {...params}
label="Filiais" label="Filiais"
placeholder={ placeholder={filters.store?.length ? `${filters.store.length} selecionadas` : 'Selecione'}
localFilters.store?.length
? `${localFilters.store.length} selecionadas`
: 'Selecione'
}
/> />
)} )}
/> />
@ -531,28 +386,21 @@ export const SearchBar = () => {
size="small" size="small"
options={stores.options} options={stores.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) => option.id === value.id}
option.id === value.id value={stores.options.filter(option =>
} filters.stockId?.includes(option.value)
value={stores.options.filter((option) =>
localFilters.stockId?.includes(option.value)
)} )}
onChange={(_, newValue) => { onChange={(_, newValue) => {
updateLocalFilter( setFilters({
'stockId', stockId: newValue.map(option => option.value),
newValue.map((option) => option.value) });
);
}} }}
loading={stores.isLoading} loading={stores.isLoading}
renderInput={(params: AutocompleteRenderInputParams) => ( renderInput={(params: Readonly<any>) => (
<TextField <TextField
{...params} {...params}
label="Filial de Estoque" label="Filial de Estoque"
placeholder={ placeholder={filters.stockId?.length ? `${filters.stockId.length} selecionadas` : 'Selecione'}
localFilters.stockId?.length
? `${localFilters.stockId.length} selecionadas`
: 'Selecione'
}
/> />
)} )}
/> />
@ -564,24 +412,15 @@ export const SearchBar = () => {
size="small" size="small"
options={sellers.options} options={sellers.options}
getOptionLabel={(option) => option.label} getOptionLabel={(option) => option.label}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) => option.id === value.id}
option.id === value.id value={sellers.options.find(option =>
} filters.sellerId === option.seller.id.toString()
value={ ) || null}
sellers.options.find(
(option) =>
localFilters.sellerId === option.seller.id.toString()
) || null
}
onChange={(_, newValue) => { onChange={(_, newValue) => {
updateLocalFilter( setFilters({
'sellerId', sellerId: newValue?.seller.id.toString() || null,
newValue?.seller.id.toString() || null sellerName: newValue?.seller.name || null,
); });
updateLocalFilter(
'sellerName',
newValue?.seller.name || null
);
}} }}
loading={sellers.isLoading} loading={sellers.isLoading}
renderInput={(params) => ( renderInput={(params) => (
@ -604,6 +443,7 @@ export const SearchBar = () => {
</Grid> </Grid>
</Collapse> </Collapse>
</Grid> </Grid>
</Grid> </Grid>
</Paper> </Paper>
); );

View File

@ -17,7 +17,11 @@ 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 && <Box sx={{ py: 3 }}>{children}</Box>} {value === index && (
<Box sx={{ py: 3 }}>
{children}
</Box>
)}
</div> </div>
); );
}; };

View File

@ -1,63 +1,21 @@
'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 { DataGridPremium } from '@mui/x-data-grid-premium'; import Typography from '@mui/material/Typography';
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 sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <Box>
<CircularProgress size={30} /> <Alert severity="info" sx={{ mb: 2 }}>
<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

@ -1,75 +0,0 @@
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,63 +1,21 @@
'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 { DataGridPremium } from '@mui/x-data-grid-premium'; import Typography from '@mui/material/Typography';
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 sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <Box>
<CircularProgress size={30} /> <Alert severity="info" sx={{ mb: 2 }}>
<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

@ -1,59 +0,0 @@
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,63 +1,21 @@
'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 { DataGridPremium } from '@mui/x-data-grid-premium'; import Typography from '@mui/material/Typography';
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 sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <Box>
<CircularProgress size={30} /> <Alert severity="info" sx={{ mb: 2 }}>
<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

@ -1,146 +0,0 @@
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

@ -1,65 +1,21 @@
'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 { DataGridPremium } from '@mui/x-data-grid-premium'; import Typography from '@mui/material/Typography';
import { useOrderDetails } from '../../hooks/useOrderDetails';
import { createInformationPanelColumns } from './InformationPanelColumns';
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
interface InformationPanelProps { interface InformationPanelProps {
orderId: number; orderId: number;
} }
export const InformationPanel = ({ orderId }: InformationPanelProps) => { export const InformationPanel = ({ orderId }: InformationPanelProps) => {
const { data: order, isLoading, error } = useOrderDetails(orderId);
const columns = useMemo(() => createInformationPanelColumns(), []);
const rows = useMemo(() => {
if (!order) return [];
return [
{
id: order.orderId || orderId,
...order,
},
];
}, [order, orderId]);
if (isLoading) {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}> <Box>
<CircularProgress size={30} /> <Alert severity="info" sx={{ mb: 2 }}>
<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 detalhes do pedido.</Alert>
</Box>
);
}
if (!order) {
return (
<Box sx={{ p: 2 }}>
<Alert severity="info">Informações do pedido não encontradas.</Alert>
</Box>
);
}
return (
<DataGridPremium
rows={rows}
columns={columns}
density="compact"
autoHeight
hideFooter
sx={dataGridStylesSimple}
/>
);
}; };

View File

@ -1,109 +0,0 @@
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
import {
formatCurrency,
formatDate,
getStatusColor,
getStatusLabel,
} from '../../utils/orderFormatters';
const TextCell = ({ value }: { value: string | null | undefined }) => (
<Box
sx={{
whiteSpace: 'normal',
wordBreak: 'break-word',
lineHeight: 1.3,
py: 0.5,
}}
>
{value ?? ''}
</Box>
);
export const createInformationPanelColumns = (): GridColDef[] => [
{
field: 'customerName',
headerName: 'Cliente',
minWidth: 180,
flex: 1.5,
description: 'Nome do cliente do pedido',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'storeId',
headerName: 'Filial',
width: 60,
align: 'center',
headerAlign: 'center',
description: 'Código da filial',
},
{
field: 'createDate',
headerName: 'Data Criação',
width: 95,
align: 'center',
headerAlign: 'center',
description: 'Data de criação do pedido',
valueFormatter: (value) => formatDate(value as string),
},
{
field: 'status',
headerName: 'Situação',
width: 90,
align: 'center',
headerAlign: 'center',
description: 'Situação atual do pedido',
renderCell: (params: Readonly<GridRenderCellParams>) => (
<Chip
label={getStatusLabel(params.value as string)}
size="small"
color={getStatusColor(params.value as string)}
variant="outlined"
sx={{ height: 22, fontSize: '0.7rem' }}
/>
),
},
{
field: 'paymentName',
headerName: 'Forma Pgto',
minWidth: 100,
flex: 1,
description: 'Forma de pagamento utilizada',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'billingName',
headerName: 'Cond. Pgto',
minWidth: 100,
flex: 1,
description: 'Condição de pagamento',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'amount',
headerName: 'Valor',
width: 100,
align: 'right',
headerAlign: 'right',
description: 'Valor total do pedido',
valueFormatter: (value) => formatCurrency(value as number),
},
{
field: 'deliveryType',
headerName: 'Tipo Entrega',
minWidth: 100,
flex: 1,
description: 'Tipo de entrega selecionado',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
{
field: 'deliveryLocal',
headerName: 'Local Entrega',
minWidth: 120,
flex: 1.2,
description: 'Local de entrega do pedido',
renderCell: (params: GridRenderCellParams) => <TextCell value={params.value} />,
},
];

View File

@ -1,114 +0,0 @@
'use client';
import { useMemo } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import CircularProgress from '@mui/material/CircularProgress';
import Alert from '@mui/material/Alert';
import { DataGridPremium } from '@mui/x-data-grid-premium';
import { useOrderItems } from '../../hooks/useOrderItems';
import { createOrderItemsColumns } from '../OrderItemsTableColumns';
import { OrderItem } from '../../schemas/order.item.schema';
import { dataGridStyles } from '../../utils/dataGridStyles';
interface OrderItemsTableProps {
orderId: number;
}
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
const { data: items, isLoading, error } = useOrderItems(orderId);
const columns = useMemo(() => createOrderItemsColumns(), []);
const rows = useMemo(() => {
if (!Array.isArray(items) || items.length === 0) return [];
return items.map((item: OrderItem, index: number) => ({
id: `${orderId}-${item.productId}-${index}`,
...item,
}));
}, [items, orderId]);
if (isLoading) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
py: 4,
}}
>
<CircularProgress size={40} />
<Typography variant="body2" color="text.secondary" sx={{ ml: 2 }}>
Carregando itens do pedido...
</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{ mt: 2 }}>
<Alert severity="error">
{error instanceof Error
? `Erro ao carregar itens: ${error.message}`
: 'Erro ao carregar itens do pedido.'}
</Alert>
</Box>
);
}
if (!items || items.length === 0) {
return (
<Box sx={{ mt: 2 }}>
<Alert severity="info">Nenhum item encontrado para este pedido.</Alert>
</Box>
);
}
return (
<DataGridPremium
rows={rows}
columns={columns}
density="compact"
autoHeight
hideFooter={rows.length <= 10}
initialState={{
pagination: {
paginationModel: {
pageSize: 10,
page: 0,
},
},
}}
pageSizeOptions={[10, 25, 50]}
sx={dataGridStyles}
localeText={{
noRowsLabel: 'Nenhum item encontrado.',
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
footerTotalRows: 'Total de itens:',
footerTotalVisibleRows: (visibleCount, totalCount) =>
`${visibleCount.toLocaleString()} de ${totalCount.toLocaleString()}`,
}}
slotProps={{
pagination: {
labelRowsPerPage: 'Itens por página:',
labelDisplayedRows: ({
from,
to,
count,
}: {
from: number;
to: number;
count: number;
}) => {
const pageSize = to >= from ? to - from + 1 : 10;
const currentPage = Math.floor((from - 1) / pageSize) + 1;
const totalPages = Math.ceil(count / pageSize);
return `${from}${to} de ${count} | Página ${currentPage} de ${totalPages}`;
},
},
}}
/>
);
};

View File

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

View File

@ -5,11 +5,9 @@ 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
@ -22,11 +20,9 @@ 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,7 +25,6 @@ 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:
@ -52,7 +51,6 @@ 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:
@ -66,13 +64,12 @@ 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,
@ -98,7 +95,6 @@ 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:
@ -118,7 +114,6 @@ 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)
@ -127,13 +122,12 @@ 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 |
@ -152,7 +146,6 @@ 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:
@ -165,11 +158,10 @@ 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"
@ -178,19 +170,16 @@ 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;
@ -199,7 +188,6 @@ 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' : ''
@ -207,7 +195,6 @@ 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',
@ -218,7 +205,6 @@ 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} />}
``` ```
@ -240,13 +226,11 @@ 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(),
@ -257,11 +241,9 @@ 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`
@ -272,7 +254,6 @@ 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',
@ -284,7 +265,6 @@ getRowClassName={(params) =>
``` ```
### Linhas ### Linhas
```typescript ```typescript
'& .MuiDataGrid-row': { '& .MuiDataGrid-row': {
minHeight: '36px !important', minHeight: '36px !important',
@ -298,7 +278,6 @@ getRowClassName={(params) =>
``` ```
### Células ### Células
```typescript ```typescript
'& .MuiDataGrid-cell': { '& .MuiDataGrid-cell': {
fontSize: '0.75rem', fontSize: '0.75rem',
@ -332,7 +311,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

@ -1,16 +0,0 @@
'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,8 +33,7 @@ export function useCustomers(searchTerm: string) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const options = const options = query.data?.map((customer, index) => ({
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}`,
@ -46,3 +45,4 @@ export function useCustomers(searchTerm: string) {
options, options,
}; };
} }

View File

@ -1,16 +0,0 @@
'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

@ -1,17 +0,0 @@
'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

@ -1,19 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { orderService } from '../api/order.service';
/**
* Hook to fetch details for a specific order.
* Uses the general search endpoint filtering by ID as requested.
*/
export function useOrderDetails(orderId: number) {
return useQuery({
queryKey: ['orderDetails', orderId],
enabled: !!orderId,
queryFn: async () => {
// The findOrders method returns an array. We search by orderId and take the first result.
const orders = await orderService.findOrders({ orderId: orderId });
return orders.length > 0 ? orders[0] : null;
},
staleTime: 1000 * 60 * 5, // 5 minutes
});
}

View File

@ -5,12 +5,11 @@ 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,
@ -31,10 +30,8 @@ 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,6 +4,7 @@ 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

@ -4,15 +4,14 @@ import { orderService } from '../api/order.service';
export function useSellers() { export function useSellers() {
const query = useQuery({ const query = useQuery({
queryKey: ['sellers'], queryKey: ['sellers'],
queryFn: () => orderService.findSellers(), queryFn: () => orderService.findsellers(),
staleTime: 1000 * 60 * 30, // 30 minutes staleTime: 1000 * 60 * 30, // 30 minutes
retry: 1, retry: 1,
retryOnMount: false, retryOnMount: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const options = const options = query.data?.map((seller, index) => {
query.data?.map((seller, index) => {
return { return {
value: seller.id.toString(), value: seller.id.toString(),
label: seller.name, label: seller.name,
@ -23,6 +22,6 @@ export function useSellers() {
return { return {
...query, ...query,
options, options
}; };
} }

View File

@ -6,3 +6,4 @@ 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,8 +6,11 @@ import { Box, Typography } from '@mui/material';
export default function OrdersPage() { export default function OrdersPage() {
return ( return (
<Box> <Box>
<Typography variant="h4" fontWeight="400" mb={0}></Typography> <Typography variant="h4" fontWeight="700" mb={3}>
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,14 +72,12 @@ 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 ( return val === null ||
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);
);
}; };
/** /**
@ -105,18 +103,15 @@ 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( return Object.entries(filters).reduce((acc, [key, value]) => {
(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,49 +18,6 @@ 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
*/ */
@ -69,7 +26,6 @@ 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
*/ */
@ -79,46 +35,3 @@ 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,12 +7,7 @@ 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 { export { orderResponseSchema, ordersResponseSchema, storesResponseSchema, customersResponseSchema } from './api-responses.schema';
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';

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