feat: Implement core application structure including Dashboard, Orders, Login, and Profile modules.
This commit is contained in:
parent
5d469f08a7
commit
02aaae0cd3
|
|
@ -39,3 +39,6 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { StorybookConfig } from '@storybook/nextjs-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@chromatic-com/storybook",
|
||||||
|
"@storybook/addon-vitest",
|
||||||
|
"@storybook/addon-a11y",
|
||||||
|
"@storybook/addon-docs",
|
||||||
|
"@storybook/addon-onboarding"
|
||||||
|
],
|
||||||
|
"framework": "@storybook/nextjs-vite",
|
||||||
|
"staticDirs": [
|
||||||
|
"..\\public"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Preview } from '@storybook/nextjs-vite'
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"sonarlint.connectedMode.project": {
|
"sonarlint.connectedMode.project": {
|
||||||
"connectionId": "http-localhost-9000",
|
"connectionId": "http-localhost-9000",
|
||||||
"projectKey": "Portal-web"
|
"projectKey": "Portal-web"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -23,11 +23,13 @@ The application is designed to provide a robust and scalable frontend interface
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Navigate to the project directory:
|
2. Navigate to the project directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd portal-web-v2
|
cd portal-web-v2
|
||||||
```
|
```
|
||||||
|
|
@ -41,14 +43,14 @@ 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. |
|
||||||
| `npm run lint` | Runs ESLint to analyze code quality and fix issues. |
|
| `npm run lint` | Runs ESLint to analyze code quality and fix issues. |
|
||||||
| `npm test` | Executes the test suite using Jest. |
|
| `npm test` | Executes the test suite using Jest. |
|
||||||
| `npm run test:coverage` | Runs tests and generates a code coverage report. |
|
| `npm run test:coverage` | Runs tests and generates a code coverage report. |
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,93 +3,98 @@
|
||||||
import createCache from '@emotion/cache';
|
import createCache from '@emotion/cache';
|
||||||
import { useServerInsertedHTML } from 'next/navigation';
|
import { useServerInsertedHTML } from 'next/navigation';
|
||||||
import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
|
import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
|
||||||
import type { EmotionCache, Options as OptionsOfCreateCache } from '@emotion/cache';
|
import type {
|
||||||
|
EmotionCache,
|
||||||
|
Options as OptionsOfCreateCache,
|
||||||
|
} from '@emotion/cache';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export type NextAppDirEmotionCacheProviderProps = {
|
export type NextAppDirEmotionCacheProviderProps = {
|
||||||
/** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */
|
/** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */
|
||||||
options: Omit<OptionsOfCreateCache, 'insertionPoint'>;
|
options: Omit<OptionsOfCreateCache, 'insertionPoint'>;
|
||||||
/** By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"' */
|
/** By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"' */
|
||||||
CacheProvider?: (props: {
|
CacheProvider?: (props: {
|
||||||
value: EmotionCache;
|
value: EmotionCache;
|
||||||
children: ReactNode;
|
|
||||||
}) => React.JSX.Element | null;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
}) => React.JSX.Element | null;
|
||||||
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx
|
// Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx
|
||||||
export default function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) {
|
export default function NextAppDirEmotionCacheProvider(
|
||||||
const { options, CacheProvider = DefaultCacheProvider, children } = props;
|
props: NextAppDirEmotionCacheProviderProps
|
||||||
|
) {
|
||||||
|
const { options, CacheProvider = DefaultCacheProvider, children } = props;
|
||||||
|
|
||||||
const [registry] = useState(() => {
|
const [registry] = useState(() => {
|
||||||
const cache = createCache(options);
|
const cache = createCache(options);
|
||||||
cache.compat = true;
|
cache.compat = true;
|
||||||
const prevInsert = cache.insert;
|
const prevInsert = cache.insert;
|
||||||
let inserted: { name: string; isGlobal: boolean }[] = [];
|
let inserted: { name: string; isGlobal: boolean }[] = [];
|
||||||
cache.insert = (...args) => {
|
cache.insert = (...args) => {
|
||||||
const [selector, serialized] = args;
|
const [selector, serialized] = args;
|
||||||
if (cache.inserted[serialized.name] === undefined) {
|
if (cache.inserted[serialized.name] === undefined) {
|
||||||
inserted.push({
|
inserted.push({
|
||||||
name: serialized.name,
|
name: serialized.name,
|
||||||
isGlobal: !selector,
|
isGlobal: !selector,
|
||||||
});
|
|
||||||
}
|
|
||||||
return prevInsert(...args);
|
|
||||||
};
|
|
||||||
const flush = () => {
|
|
||||||
const prevInserted = inserted;
|
|
||||||
inserted = [];
|
|
||||||
return prevInserted;
|
|
||||||
};
|
|
||||||
return { cache, flush };
|
|
||||||
});
|
|
||||||
|
|
||||||
useServerInsertedHTML(() => {
|
|
||||||
const inserted = registry.flush();
|
|
||||||
if (inserted.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let styles = '';
|
|
||||||
let dataEmotionAttribute = registry.cache.key;
|
|
||||||
|
|
||||||
const globals: {
|
|
||||||
name: string;
|
|
||||||
style: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
inserted.forEach(({ name, isGlobal }) => {
|
|
||||||
const style = registry.cache.inserted[name];
|
|
||||||
|
|
||||||
if (style && typeof style !== 'boolean') {
|
|
||||||
if (isGlobal) {
|
|
||||||
globals.push({ name, style });
|
|
||||||
} else {
|
|
||||||
styles += style;
|
|
||||||
dataEmotionAttribute += ` ${name}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
return prevInsert(...args);
|
||||||
|
};
|
||||||
|
const flush = () => {
|
||||||
|
const prevInserted = inserted;
|
||||||
|
inserted = [];
|
||||||
|
return prevInserted;
|
||||||
|
};
|
||||||
|
return { cache, flush };
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
useServerInsertedHTML(() => {
|
||||||
<>
|
const inserted = registry.flush();
|
||||||
{globals.map(({ name, style }) => (
|
if (inserted.length === 0) {
|
||||||
<style
|
return null;
|
||||||
key={name}
|
}
|
||||||
data-emotion={`${registry.cache.key}-global ${name}`}
|
let styles = '';
|
||||||
dangerouslySetInnerHTML={{ __html: style }}
|
let dataEmotionAttribute = registry.cache.key;
|
||||||
/>
|
|
||||||
))}
|
const globals: {
|
||||||
{styles && (
|
name: string;
|
||||||
<style
|
style: string;
|
||||||
data-emotion={dataEmotionAttribute}
|
}[] = [];
|
||||||
dangerouslySetInnerHTML={{ __html: styles }}
|
|
||||||
/>
|
inserted.forEach(({ name, isGlobal }) => {
|
||||||
)}
|
const style = registry.cache.inserted[name];
|
||||||
</>
|
|
||||||
);
|
if (style && typeof style !== 'boolean') {
|
||||||
|
if (isGlobal) {
|
||||||
|
globals.push({ name, style });
|
||||||
|
} else {
|
||||||
|
styles += style;
|
||||||
|
dataEmotionAttribute += ` ${name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return <CacheProvider value={registry.cache}>{children}</CacheProvider>;
|
return (
|
||||||
|
<>
|
||||||
|
{globals.map(({ name, style }) => (
|
||||||
|
<style
|
||||||
|
key={name}
|
||||||
|
data-emotion={`${registry.cache.key}-global ${name}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: style }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{styles && (
|
||||||
|
<style
|
||||||
|
data-emotion={dataEmotionAttribute}
|
||||||
|
dangerouslySetInnerHTML={{ __html: styles }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <CacheProvider value={registry.cache}>{children}</CacheProvider>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,99 +8,102 @@ import { useCustomizerStore } from '@/features/dashboard/store/useCustomizerStor
|
||||||
import { LicenseInfo } from '@mui/x-license';
|
import { LicenseInfo } from '@mui/x-license';
|
||||||
import { AuthInitializer } from '@/features/login/components/AuthInitializer';
|
import { AuthInitializer } from '@/features/login/components/AuthInitializer';
|
||||||
|
|
||||||
const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
|
const PERPETUAL_LICENSE_KEY =
|
||||||
|
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
|
||||||
try {
|
try {
|
||||||
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
|
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set MUI license key:', error);
|
console.error('Failed to set MUI license key:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Providers({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
const [queryClient] = useState(() => new QueryClient());
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const activeMode = useCustomizerStore((state) => state.activeMode);
|
||||||
|
const isHydrated = useCustomizerStore((state) => state.isHydrated);
|
||||||
|
|
||||||
export default function Providers({ children }: Readonly<{ children: React.ReactNode }>) {
|
useEffect(() => {
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
setMounted(true);
|
||||||
const [mounted, setMounted] = useState(false);
|
}, []);
|
||||||
const activeMode = useCustomizerStore((state) => state.activeMode);
|
|
||||||
const isHydrated = useCustomizerStore((state) => state.isHydrated);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const safeMode = mounted && isHydrated ? activeMode : 'light';
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const safeMode = mounted && isHydrated ? activeMode : 'light';
|
const theme = useMemo(
|
||||||
|
() =>
|
||||||
const theme = useMemo(
|
createTheme({
|
||||||
() =>
|
palette: {
|
||||||
createTheme({
|
mode: safeMode,
|
||||||
palette: {
|
primary: {
|
||||||
mode: safeMode,
|
main: '#5d87ff',
|
||||||
primary: {
|
light: '#ecf2ff',
|
||||||
main: '#5d87ff',
|
dark: '#4570ea',
|
||||||
light: '#ecf2ff',
|
},
|
||||||
dark: '#4570ea',
|
secondary: {
|
||||||
},
|
main: '#49beff',
|
||||||
secondary: {
|
light: '#e8f7ff',
|
||||||
main: '#49beff',
|
dark: '#23afdb',
|
||||||
light: '#e8f7ff',
|
},
|
||||||
dark: '#23afdb',
|
...(safeMode === 'dark'
|
||||||
},
|
? {
|
||||||
...(safeMode === 'dark'
|
text: {
|
||||||
? {
|
primary: '#ffffff',
|
||||||
text: {
|
secondary: '#b0bcc8',
|
||||||
primary: '#ffffff',
|
|
||||||
secondary: '#b0bcc8',
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
default: '#0b1426',
|
|
||||||
paper: '#111c2e',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
text: {
|
|
||||||
primary: '#1a1a1a',
|
|
||||||
secondary: '#4a5568',
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
default: '#ffffff',
|
|
||||||
paper: '#ffffff',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
typography: {
|
background: {
|
||||||
fontFamily: '"Plus Jakarta Sans", "Helvetica", "Arial", sans-serif',
|
default: '#0b1426',
|
||||||
h1: { fontWeight: 600 },
|
paper: '#111c2e',
|
||||||
h6: { fontWeight: 600 },
|
|
||||||
body1: { fontSize: '0.875rem', fontWeight: 400 },
|
|
||||||
},
|
},
|
||||||
components: {
|
}
|
||||||
MuiCssBaseline: {
|
: {
|
||||||
styleOverrides: {
|
text: {
|
||||||
body: {
|
primary: '#1a1a1a',
|
||||||
fontFamily: "var(--font-plus-jakarta), 'Plus Jakarta Sans', sans-serif",
|
secondary: '#4a5568',
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTypography: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: ({ theme }) => ({
|
|
||||||
...(theme.palette.mode === 'light' && {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
background: {
|
||||||
[safeMode]
|
default: '#ffffff',
|
||||||
);
|
paper: '#ffffff',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: '"Plus Jakarta Sans", "Helvetica", "Arial", sans-serif',
|
||||||
|
h1: { fontWeight: 600 },
|
||||||
|
h6: { fontWeight: 600 },
|
||||||
|
body1: { fontSize: '0.875rem', fontWeight: 400 },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
body: {
|
||||||
|
fontFamily:
|
||||||
|
"var(--font-plus-jakarta), 'Plus Jakarta Sans', sans-serif",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTypography: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
...(theme.palette.mode === 'light' && {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[safeMode]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthInitializer>
|
<AuthInitializer>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AuthInitializer>
|
</AuthInitializer>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,21 @@ function OrdersContent() {
|
||||||
|
|
||||||
export default function OrdersPageRoute() {
|
export default function OrdersPageRoute() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={
|
<Suspense
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
fallback={
|
||||||
<CircularProgress />
|
<Box
|
||||||
</Box>
|
sx={{
|
||||||
}>
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
<OrdersContent />
|
<OrdersContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export { default } from '../../src/features/dashboard/pages/DashboardPage';
|
export { default } from '../../src/features/dashboard/pages/DashboardPage';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,21 @@ function ProfileContent() {
|
||||||
|
|
||||||
export default function ProfilePageRoute() {
|
export default function ProfilePageRoute() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={
|
<Suspense
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
|
fallback={
|
||||||
<CircularProgress />
|
<Box
|
||||||
</Box>
|
sx={{
|
||||||
}>
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ProfileContent />
|
<ProfileContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
|
|
@ -28,4 +28,4 @@ body {
|
||||||
100% {
|
100% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono, Plus_Jakarta_Sans } from "next/font/google";
|
import { Geist, Geist_Mono, Plus_Jakarta_Sans } from 'next/font/google';
|
||||||
import "./globals.css";
|
import './globals.css';
|
||||||
import Providers from "./components/Providers";
|
import Providers from './components/Providers';
|
||||||
import EmotionRegistry from "./components/EmotionRegistry";
|
import EmotionRegistry from './components/EmotionRegistry';
|
||||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
import { NuqsAdapter } from 'nuqs/adapters/next/app';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: '--font-geist-sans',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: '--font-geist-mono',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const plusJakartaSans = Plus_Jakarta_Sans({
|
const plusJakartaSans = Plus_Jakarta_Sans({
|
||||||
variable: "--font-plus-jakarta",
|
variable: '--font-plus-jakarta',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
weight: ['300', '400', '500', '600', '700', '800'],
|
weight: ['300', '400', '500', '600', '700', '800'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Portal Jurunense - Login Page",
|
title: 'Portal Jurunense - Login Page',
|
||||||
description: "Login page for Modernize dashboard",
|
description: 'Login page for Modernize dashboard',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -36,7 +36,6 @@ export default function RootLayout({
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} ${plusJakartaSans.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} ${plusJakartaSans.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|
||||||
<EmotionRegistry options={{ key: 'mui' }}>
|
<EmotionRegistry options={{ key: 'mui' }}>
|
||||||
<NuqsAdapter>
|
<NuqsAdapter>
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import Login from "../../src/features/login/components/LoginForm";
|
import Login from '../../src/features/login/components/LoginForm';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return <Login />;
|
return <Login />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 />;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
import storybook from "eslint-plugin-storybook";
|
||||||
import nextTs from "eslint-config-next/typescript";
|
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
|
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||||
|
import nextTs from 'eslint-config-next/typescript';
|
||||||
|
|
||||||
const eslintConfig = defineConfig([
|
const eslintConfig = defineConfig([
|
||||||
...nextVitals,
|
...nextVitals,
|
||||||
|
|
@ -8,10 +11,10 @@ const eslintConfig = defineConfig([
|
||||||
// Override default ignores of eslint-config-next.
|
// Override default ignores of eslint-config-next.
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
// Default ignores of eslint-config-next:
|
// Default ignores of eslint-config-next:
|
||||||
".next/**",
|
'.next/**',
|
||||||
"out/**",
|
'out/**',
|
||||||
"build/**",
|
'build/**',
|
||||||
"next-env.d.ts",
|
'next-env.d.ts',
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,33 @@ import '@testing-library/jest-dom';
|
||||||
|
|
||||||
// Mock Next.js router
|
// Mock Next.js router
|
||||||
jest.mock('next/navigation', () => ({
|
jest.mock('next/navigation', () => ({
|
||||||
useRouter() {
|
useRouter() {
|
||||||
return {
|
return {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
replace: jest.fn(),
|
replace: jest.fn(),
|
||||||
prefetch: jest.fn(),
|
prefetch: jest.fn(),
|
||||||
back: jest.fn(),
|
back: jest.fn(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
usePathname() {
|
usePathname() {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
useSearchParams() {
|
useSearchParams() {
|
||||||
return new URLSearchParams();
|
return new URLSearchParams();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 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,
|
||||||
addListener: jest.fn(),
|
addListener: jest.fn(),
|
||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
addEventListener: jest.fn(),
|
addEventListener: jest.fn(),
|
||||||
removeEventListener: jest.fn(),
|
removeEventListener: jest.fn(),
|
||||||
dispatchEvent: jest.fn(),
|
dispatchEvent: jest.fn(),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
|
|
||||||
import { LicenseInfo } from '@mui/x-license';
|
import { LicenseInfo } from '@mui/x-license';
|
||||||
|
|
||||||
const PERPETUAL_LICENSE_KEY = 'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
|
const PERPETUAL_LICENSE_KEY =
|
||||||
const ALTERNATIVE_LICENSE_KEY = '61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=';
|
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y';
|
||||||
|
const ALTERNATIVE_LICENSE_KEY =
|
||||||
|
'61628ce74db2c1b62783a6d438593bc5Tz1NVUktRG9jLEU9MTY4MzQ0NzgyMTI4NCxTPXByZW1pdW0sTE09c3Vic2NyaXB0aW9uLEtWPTI=';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
|
LicenseInfo.setLicenseKey(PERPETUAL_LICENSE_KEY);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to set perpetual license key', error);
|
console.warn('Failed to set perpetual license key', error);
|
||||||
try {
|
try {
|
||||||
LicenseInfo.setLicenseKey(ALTERNATIVE_LICENSE_KEY);
|
LicenseInfo.setLicenseKey(ALTERNATIVE_LICENSE_KEY);
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error('Failed to set fallback license key', fallbackError);
|
console.error('Failed to set fallback license key', fallbackError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
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*',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
150
package.json
150
package.json
|
|
@ -1,69 +1,85 @@
|
||||||
{
|
{
|
||||||
"name": "portal-web-v2",
|
"name": "portal-web-v2",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"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",
|
||||||
"dependencies": {
|
"build-storybook": "storybook build"
|
||||||
"@emotion/cache": "^11.14.0",
|
},
|
||||||
"@emotion/react": "^11.14.0",
|
"dependencies": {
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/cache": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/react": "^11.14.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@emotion/server": "^11.11.0",
|
||||||
"@mui/icons-material": "^7.3.6",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/lab": "^7.0.1-beta.20",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@mui/material": "^7.3.6",
|
"@mui/icons-material": "^7.3.6",
|
||||||
"@mui/material-nextjs": "^7.3.6",
|
"@mui/lab": "^7.0.1-beta.20",
|
||||||
"@mui/styled-engine-sc": "^6.0.0-alpha.1",
|
"@mui/material": "^7.3.6",
|
||||||
"@mui/x-data-grid": "^8.23.0",
|
"@mui/material-nextjs": "^7.3.6",
|
||||||
"@mui/x-data-grid-generator": "^8.23.0",
|
"@mui/styled-engine-sc": "^6.0.0-alpha.1",
|
||||||
"@mui/x-data-grid-premium": "^8.23.0",
|
"@mui/x-data-grid": "^8.23.0",
|
||||||
"@mui/x-data-grid-pro": "^8.23.0",
|
"@mui/x-data-grid-generator": "^8.23.0",
|
||||||
"@mui/x-date-pickers": "^8.23.0",
|
"@mui/x-data-grid-premium": "^8.23.0",
|
||||||
"@mui/x-license": "^8.23.0",
|
"@mui/x-data-grid-pro": "^8.23.0",
|
||||||
"@mui/x-tree-view": "^8.23.0",
|
"@mui/x-date-pickers": "^8.23.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@mui/x-license": "^8.23.0",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@mui/x-tree-view": "^8.23.0",
|
||||||
"@tabler/icons-react": "^2.47.0",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
"@xstate/react": "^6.0.0",
|
"@tabler/icons-react": "^2.47.0",
|
||||||
"axios": "^1.13.2",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"date-fns": "^4.1.0",
|
"@xstate/react": "^6.0.0",
|
||||||
"jest": "^30.2.0",
|
"axios": "^1.13.2",
|
||||||
"lodash": "^4.17.21",
|
"date-fns": "^4.1.0",
|
||||||
"moment": "^2.29.4",
|
"jest": "^30.2.0",
|
||||||
"next": "16.1.1",
|
"lodash": "^4.17.21",
|
||||||
"next-auth": "latest",
|
"moment": "^2.29.4",
|
||||||
"nuqs": "^2.8.6",
|
"next": "16.1.1",
|
||||||
"react": "19.2.3",
|
"next-auth": "latest",
|
||||||
"react-big-calendar": "^1.19.4",
|
"nuqs": "^2.8.6",
|
||||||
"react-dom": "19.2.3",
|
"prettier": "^3.7.4",
|
||||||
"react-hook-form": "^7.69.0",
|
"react": "19.2.3",
|
||||||
"react-number-format": "^5.4.4",
|
"react-big-calendar": "^1.19.4",
|
||||||
"simplebar": "^6.3.3",
|
"react-dom": "19.2.3",
|
||||||
"simplebar-react": "^3.3.2",
|
"react-hook-form": "^7.69.0",
|
||||||
"stimulsoft-reports-js": "^2026.1.1",
|
"react-number-format": "^5.4.4",
|
||||||
"xstate": "^5.25.0",
|
"simplebar": "^6.3.3",
|
||||||
"zod": "^4.2.1",
|
"simplebar-react": "^3.3.2",
|
||||||
"zustand": "^5.0.9"
|
"stimulsoft-reports-js": "^2026.1.1",
|
||||||
},
|
"xstate": "^5.25.0",
|
||||||
"devDependencies": {
|
"zod": "^4.2.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"zustand": "^5.0.9"
|
||||||
"@types/lodash": "^4.17.21",
|
},
|
||||||
"@types/node": "^20",
|
"devDependencies": {
|
||||||
"@types/react": "^19",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/react-big-calendar": "^1.16.3",
|
"@types/lodash": "^4.17.21",
|
||||||
"@types/react-dom": "^19",
|
"@types/node": "^20",
|
||||||
"eslint": "^9",
|
"@types/react": "^19",
|
||||||
"eslint-config-next": "16.1.1",
|
"@types/react-big-calendar": "^1.16.3",
|
||||||
"tailwindcss": "^4",
|
"@types/react-dom": "^19",
|
||||||
"typescript": "^5"
|
"eslint": "^9",
|
||||||
}
|
"eslint-config-next": "16.1.1",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import Typography from '@mui/material/Typography';
|
||||||
import DashboardOverview from './DashboardOverview';
|
import DashboardOverview from './DashboardOverview';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 4 }}>
|
<Box sx={{ p: 4 }}>
|
||||||
<Typography variant="h4" fontWeight="700" mb={3}>
|
<Typography variant="h4" fontWeight="700" mb={3}>
|
||||||
Dashboard
|
Dashboard
|
||||||
</Typography>
|
</Typography>
|
||||||
<DashboardOverview />
|
<DashboardOverview />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ const StyledMain = styled(Box, {
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const lgUp = useMediaQuery(theme.breakpoints.up('lg'));
|
const lgUp = useMediaQuery(theme.breakpoints.up('lg'));
|
||||||
|
|
@ -37,10 +36,15 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
if (!isHydrated) {
|
if (!isHydrated) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', height: '100vh' }}>
|
<Box sx={{ display: 'flex', height: '100vh' }}>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
|
<Box
|
||||||
<StyledMain isMobile={!lgUp}>
|
sx={{
|
||||||
{children}
|
display: 'flex',
|
||||||
</StyledMain>
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledMain isMobile={!lgUp}>{children}</StyledMain>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
@ -49,13 +53,17 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', height: '100vh' }}>
|
<Box sx={{ display: 'flex', height: '100vh' }}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: 'hidden' }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<StyledMain isMobile={!lgUp}>
|
<StyledMain isMobile={!lgUp}>{children}</StyledMain>
|
||||||
{children}
|
|
||||||
</StyledMain>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,35 +6,34 @@ import Card from '@mui/material/Card';
|
||||||
import CardContent from '@mui/material/CardContent';
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
|
||||||
export default function DashboardOverview() {
|
export default function DashboardOverview() {
|
||||||
const handlePortalTorreClick = () => {
|
const handlePortalTorreClick = () => {
|
||||||
window.open('https://portaltorre.jurunense.com/', '_blank');
|
window.open('https://portaltorre.jurunense.com/', '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||||
<Card
|
<Card
|
||||||
onClick={handlePortalTorreClick}
|
onClick={handlePortalTorreClick}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.3s ease',
|
transition: 'all 0.3s ease',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
transform: 'translateY(-4px)',
|
transform: 'translateY(-4px)',
|
||||||
boxShadow: 6,
|
boxShadow: 6,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" fontWeight="600" mb={1}>
|
<Typography variant="h6" fontWeight="600" mb={1}>
|
||||||
Portal Torre
|
Portal Torre
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="textSecondary">
|
<Typography variant="body2" color="textSecondary">
|
||||||
Acesse o Portal Torre de Controle
|
Acesse o Portal Torre de Controle
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,3 @@ export default function Logo() {
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import SimpleBar from "simplebar-react";
|
import SimpleBar from 'simplebar-react';
|
||||||
import "simplebar-react/dist/simplebar.min.css";
|
import 'simplebar-react/dist/simplebar.min.css';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { SxProps } from '@mui/system';
|
import { SxProps } from '@mui/system';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { useMediaQuery } from "@mui/material";
|
import { useMediaQuery } from '@mui/material';
|
||||||
|
|
||||||
const SimpleBarStyle = styled(SimpleBar)(() => ({
|
const SimpleBarStyle = styled(SimpleBar)(() => ({
|
||||||
maxHeight: "100%",
|
maxHeight: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface PropsType {
|
interface PropsType {
|
||||||
|
|
@ -19,7 +19,7 @@ const Scrollbar = (props: PropsType) => {
|
||||||
const lgDown = useMediaQuery((theme: any) => theme.breakpoints.down('lg'));
|
const lgDown = useMediaQuery((theme: any) => theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
if (lgDown) {
|
if (lgDown) {
|
||||||
return <Box sx={{ overflowX: "auto" }}>{children}</Box>;
|
return <Box sx={{ overflowX: 'auto' }}>{children}</Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -30,4 +30,3 @@ const Scrollbar = (props: PropsType) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Scrollbar;
|
export default Scrollbar;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,46 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Box from "@mui/material/Box";
|
import Box from '@mui/material/Box';
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from '@mui/material/Stack';
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import { styled, Theme } from "@mui/material/styles";
|
import { styled, Theme } from '@mui/material/styles';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||||
import { useCustomizerStore } from "../store/useCustomizerStore";
|
import { useCustomizerStore } from '../store/useCustomizerStore';
|
||||||
|
|
||||||
import Notifications from "./Notification";
|
import Notifications from './Notification';
|
||||||
import Profile from "./Profile";
|
import Profile from './Profile';
|
||||||
import Search from "./Search";
|
import Search from './Search';
|
||||||
import Navigation from "./Navigation";
|
import Navigation from './Navigation';
|
||||||
import MobileRightSidebar from "./MobileRightSidebar";
|
import MobileRightSidebar from './MobileRightSidebar';
|
||||||
|
|
||||||
const AppBarStyled = styled(AppBar)(({ theme }) => ({
|
const AppBarStyled = styled(AppBar)(({ theme }) => ({
|
||||||
boxShadow: "none",
|
boxShadow: 'none',
|
||||||
background: theme.palette.background.paper,
|
background: theme.palette.background.paper,
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
backdropFilter: "blur(4px)",
|
backdropFilter: 'blur(4px)',
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ToolbarStyled = styled(Toolbar)(({ theme }) => ({
|
const ToolbarStyled = styled(Toolbar)(({ theme }) => ({
|
||||||
width: "100%",
|
width: '100%',
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up("lg"));
|
const lgUp = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||||
const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down("lg"));
|
const lgDown = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
activeMode,
|
activeMode,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
toggleMobileSidebar,
|
toggleMobileSidebar,
|
||||||
setDarkMode,
|
setDarkMode,
|
||||||
isHydrated
|
isHydrated,
|
||||||
} = useCustomizerStore();
|
} = useCustomizerStore();
|
||||||
|
|
||||||
if (!isHydrated) {
|
if (!isHydrated) {
|
||||||
|
|
@ -70,21 +70,18 @@ const Header = () => {
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
|
|
||||||
<Stack spacing={1} direction="row" alignItems="center">
|
<Stack spacing={1} direction="row" alignItems="center">
|
||||||
|
|
||||||
{/* ------------------------------------------- */}
|
{/* ------------------------------------------- */}
|
||||||
{/* Theme Toggle (Dark/Light) */}
|
{/* Theme Toggle (Dark/Light) */}
|
||||||
{/* ------------------------------------------- */}
|
{/* ------------------------------------------- */}
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => setDarkMode(activeMode === "light" ? "dark" : "light")}
|
onClick={() =>
|
||||||
|
setDarkMode(activeMode === 'light' ? 'dark' : 'light')
|
||||||
|
}
|
||||||
aria-label="alternar tema"
|
aria-label="alternar tema"
|
||||||
>
|
>
|
||||||
{activeMode === "light" ? (
|
{activeMode === 'light' ? <DarkModeIcon /> : <LightModeIcon />}
|
||||||
<DarkModeIcon />
|
|
||||||
) : (
|
|
||||||
<LightModeIcon />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
|
@ -100,4 +97,4 @@ const Header = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,7 @@ const MobileRightSidebar = () => {
|
||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box px={3} mt={3}>
|
<Box px={3} mt={3}></Box>
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
import { Box, Menu, Typography, Button, Divider, Grid } from "@mui/material";
|
import { Box, Menu, Typography, Button, Divider, Grid } from '@mui/material';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
import { IconChevronDown, IconHelp } from "@tabler/icons-react";
|
import { IconChevronDown, IconHelp } from '@tabler/icons-react';
|
||||||
import AppLinks from "./AppLinks";
|
import AppLinks from './AppLinks';
|
||||||
|
|
||||||
const AppDD = () => {
|
const AppDD = () => {
|
||||||
const [anchorEl2, setAnchorEl2] = useState(null);
|
const [anchorEl2, setAnchorEl2] = useState(null);
|
||||||
|
|
@ -25,17 +25,17 @@ const AppDD = () => {
|
||||||
aria-controls="msgs-menu"
|
aria-controls="msgs-menu"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: anchorEl2 ? "primary.light" : "",
|
bgcolor: anchorEl2 ? 'primary.light' : '',
|
||||||
color: anchorEl2
|
color: anchorEl2
|
||||||
? "primary.main"
|
? 'primary.main'
|
||||||
: (theme) => theme.palette.text.secondary,
|
: (theme) => theme.palette.text.secondary,
|
||||||
fontSize: "13px",
|
fontSize: '13px',
|
||||||
}}
|
}}
|
||||||
onClick={handleClick2}
|
onClick={handleClick2}
|
||||||
endIcon={
|
endIcon={
|
||||||
<IconChevronDown
|
<IconChevronDown
|
||||||
size="15"
|
size="15"
|
||||||
style={{ marginLeft: "-5px", marginTop: "2px" }}
|
style={{ marginLeft: '-5px', marginTop: '2px' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -50,13 +50,13 @@ const AppDD = () => {
|
||||||
keepMounted
|
keepMounted
|
||||||
open={Boolean(anchorEl2)}
|
open={Boolean(anchorEl2)}
|
||||||
onClose={handleClose2}
|
onClose={handleClose2}
|
||||||
anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
|
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||||
transformOrigin={{ horizontal: "left", vertical: "top" }}
|
transformOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiMenu-paper": {
|
'& .MuiMenu-paper': {
|
||||||
width: "850px",
|
width: '850px',
|
||||||
},
|
},
|
||||||
"& .MuiMenu-paper ul": {
|
'& .MuiMenu-paper ul': {
|
||||||
p: 0,
|
p: 0,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
@ -69,8 +69,8 @@ const AppDD = () => {
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: {
|
display: {
|
||||||
xs: "none",
|
xs: 'none',
|
||||||
sm: "flex",
|
sm: 'flex',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
|
@ -99,15 +99,17 @@ const AppDD = () => {
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ sm: 4 }}>
|
<Grid size={{ sm: 4 }}>
|
||||||
<Box p={4}>
|
<Box p={4}></Box>
|
||||||
</Box>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{ color: (theme) => theme.palette.text.secondary, fontSize: "13px" }}
|
sx={{
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
fontSize: '13px',
|
||||||
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
href="/apps/chats"
|
href="/apps/chats"
|
||||||
component={Link}
|
component={Link}
|
||||||
|
|
@ -116,7 +118,10 @@ const AppDD = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="inherit"
|
color="inherit"
|
||||||
sx={{ color: (theme) => theme.palette.text.secondary, fontSize: "13px" }}
|
sx={{
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
fontSize: '13px',
|
||||||
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
href="/apps/email"
|
href="/apps/email"
|
||||||
component={Link}
|
component={Link}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
Chip,
|
Chip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import * as dropdownData from './data';
|
import * as dropdownData from './data';
|
||||||
import Scrollbar from '../components/Scrollbar';
|
import { Scrollbar } from '@/shared/components';
|
||||||
|
|
||||||
import { IconBellRinging } from '@tabler/icons-react';
|
import { IconBellRinging } from '@tabler/icons-react';
|
||||||
import { Stack } from '@mui/system';
|
import { Stack } from '@mui/system';
|
||||||
|
|
@ -62,7 +62,13 @@ const Notifications = () => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" py={2} px={4} justifyContent="space-between" alignItems="center">
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
py={2}
|
||||||
|
px={4}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<Typography variant="h6">Notifications</Typography>
|
<Typography variant="h6">Notifications</Typography>
|
||||||
<Chip label="5 new" color="primary" size="small" />
|
<Chip label="5 new" color="primary" size="small" />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
@ -108,7 +114,13 @@ const Notifications = () => {
|
||||||
))}
|
))}
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
<Box p={3} pb={1}>
|
<Box p={3} pb={1}>
|
||||||
<Button href="/apps/email" variant="outlined" component={Link} color="primary" fullWidth>
|
<Button
|
||||||
|
href="/apps/email"
|
||||||
|
variant="outlined"
|
||||||
|
component={Link}
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
See all Notifications
|
See all Notifications
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { IconMail } from '@tabler/icons-react';
|
||||||
import { Stack } from '@mui/system';
|
import { Stack } from '@mui/system';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const [anchorEl2, setAnchorEl2] = useState(null);
|
const [anchorEl2, setAnchorEl2] = useState(null);
|
||||||
const handleClick2 = (event: any) => {
|
const handleClick2 = (event: any) => {
|
||||||
|
|
@ -40,7 +39,7 @@ const Profile = () => {
|
||||||
onClick={handleClick2}
|
onClick={handleClick2}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={"/images/profile/user-1.jpg"}
|
src={'/images/profile/user-1.jpg'}
|
||||||
alt={'ProfileImg'}
|
alt={'ProfileImg'}
|
||||||
sx={{
|
sx={{
|
||||||
width: 35,
|
width: 35,
|
||||||
|
|
@ -68,9 +67,17 @@ const Profile = () => {
|
||||||
>
|
>
|
||||||
<Typography variant="h5">User Profile</Typography>
|
<Typography variant="h5">User Profile</Typography>
|
||||||
<Stack direction="row" py={3} spacing={2} alignItems="center">
|
<Stack direction="row" py={3} spacing={2} alignItems="center">
|
||||||
<Avatar src={"/images/profile/user-1.jpg"} alt={"ProfileImg"} sx={{ width: 95, height: 95 }} />
|
<Avatar
|
||||||
|
src={'/images/profile/user-1.jpg'}
|
||||||
|
alt={'ProfileImg'}
|
||||||
|
sx={{ width: 95, height: 95 }}
|
||||||
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="subtitle2" color="textPrimary" fontWeight={600}>
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
color="textPrimary"
|
||||||
|
fontWeight={600}
|
||||||
|
>
|
||||||
Mathew Anderson
|
Mathew Anderson
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="subtitle2" color="textSecondary">
|
<Typography variant="subtitle2" color="textSecondary">
|
||||||
|
|
@ -100,7 +107,8 @@ const Profile = () => {
|
||||||
bgcolor="primary.light"
|
bgcolor="primary.light"
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center" flexShrink="0"
|
justifyContent="center"
|
||||||
|
flexShrink="0"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={profile.icon}
|
src={profile.icon}
|
||||||
|
|
@ -142,7 +150,13 @@ const Profile = () => {
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Box bgcolor="primary.light" p={3} mb={3} overflow="hidden" position="relative">
|
<Box
|
||||||
|
bgcolor="primary.light"
|
||||||
|
p={3}
|
||||||
|
mb={3}
|
||||||
|
overflow="hidden"
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h5" mb={2}>
|
<Typography variant="h5" mb={2}>
|
||||||
|
|
@ -153,10 +167,23 @@ const Profile = () => {
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Image src={"/images/backgrounds/unlimited-bg.png"} width={150} height={183} style={{ height: 'auto', width: 'auto' }} alt="unlimited" className="signup-bg" />
|
<Image
|
||||||
|
src={'/images/backgrounds/unlimited-bg.png'}
|
||||||
|
width={150}
|
||||||
|
height={183}
|
||||||
|
style={{ height: 'auto', width: 'auto' }}
|
||||||
|
alt="unlimited"
|
||||||
|
className="signup-bg"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Button href="/auth/auth1/login" variant="outlined" color="primary" component={Link} fullWidth>
|
<Button
|
||||||
|
href="/auth/auth1/login"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
component={Link}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ interface MenuType {
|
||||||
title: string;
|
title: string;
|
||||||
id: string;
|
id: string;
|
||||||
subheader: string;
|
subheader: string;
|
||||||
children: MenuType[];
|
children: MenuType[];
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +36,9 @@ const Search = () => {
|
||||||
const filterRoutes = (rotr: any, cSearch: string) => {
|
const filterRoutes = (rotr: any, cSearch: string) => {
|
||||||
if (rotr.length > 1)
|
if (rotr.length > 1)
|
||||||
return rotr.filter((t: any) =>
|
return rotr.filter((t: any) =>
|
||||||
t.title ? t.href.toLocaleLowerCase().includes(cSearch.toLocaleLowerCase()) : '',
|
t.title
|
||||||
|
? t.href.toLocaleLowerCase().includes(cSearch.toLocaleLowerCase())
|
||||||
|
: ''
|
||||||
);
|
);
|
||||||
|
|
||||||
return rotr;
|
return rotr;
|
||||||
|
|
@ -89,7 +91,11 @@ const Search = () => {
|
||||||
return (
|
return (
|
||||||
<Box key={menu.title ? menu.id : menu.subheader}>
|
<Box key={menu.title ? menu.id : menu.subheader}>
|
||||||
{menu.title && !menu.children ? (
|
{menu.title && !menu.children ? (
|
||||||
<ListItemButton sx={{ py: 0.5, px: 1 }} href={menu?.href} component={Link}>
|
<ListItemButton
|
||||||
|
sx={{ py: 0.5, px: 1 }}
|
||||||
|
href={menu?.href}
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={menu.title}
|
primary={menu.title}
|
||||||
secondary={menu?.href}
|
secondary={menu?.href}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
export { default as Dashboard } from './components/Dashboard';
|
export { default as Dashboard } from './components/Dashboard';
|
||||||
export { default as DashboardPage } from './pages/DashboardPage';
|
export { default as DashboardPage } from './pages/DashboardPage';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,3 @@ export default function DashboardPage() {
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,4 +36,3 @@ const Menuitems: MenuItemType[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default Menuitems;
|
export default Menuitems;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import Typography from '@mui/material/Typography';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import SidebarItems from './SidebarItems';
|
import SidebarItems from './SidebarItems';
|
||||||
import Scrollbar from '../components/Scrollbar';
|
import { Scrollbar } from '@/shared/components';
|
||||||
import { useCustomizerStore } from '../store/useCustomizerStore';
|
import { useCustomizerStore } from '../store/useCustomizerStore';
|
||||||
|
|
||||||
// Mixins para transições
|
// Mixins para transições
|
||||||
|
|
@ -44,8 +44,18 @@ interface StyledDrawerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledDrawer = styled(Drawer, {
|
const StyledDrawer = styled(Drawer, {
|
||||||
shouldForwardProp: (prop) => prop !== 'isCollapsed' && prop !== 'isMobile' && prop !== 'sidebarWidth' && prop !== 'miniSidebarWidth',
|
shouldForwardProp: (prop) =>
|
||||||
})<StyledDrawerProps & { sidebarWidth?: number; miniSidebarWidth?: number }>(({ theme, isCollapsed, isMobile, sidebarWidth = 270, miniSidebarWidth = 87 }) => {
|
prop !== 'isCollapsed' &&
|
||||||
|
prop !== 'isMobile' &&
|
||||||
|
prop !== 'sidebarWidth' &&
|
||||||
|
prop !== 'miniSidebarWidth',
|
||||||
|
})<StyledDrawerProps & { sidebarWidth?: number; miniSidebarWidth?: number }>(({
|
||||||
|
theme,
|
||||||
|
isCollapsed,
|
||||||
|
isMobile,
|
||||||
|
sidebarWidth = 270,
|
||||||
|
miniSidebarWidth = 87,
|
||||||
|
}) => {
|
||||||
const desktopWidth = isCollapsed ? miniSidebarWidth : sidebarWidth;
|
const desktopWidth = isCollapsed ? miniSidebarWidth : sidebarWidth;
|
||||||
const drawerWidth = isMobile ? sidebarWidth : desktopWidth;
|
const drawerWidth = isMobile ? sidebarWidth : desktopWidth;
|
||||||
|
|
||||||
|
|
@ -62,7 +72,9 @@ const StyledDrawer = styled(Drawer, {
|
||||||
}),
|
}),
|
||||||
...(!isMobile && {
|
...(!isMobile && {
|
||||||
'& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': {
|
||||||
...(isCollapsed ? closedMixin(theme, miniSidebarWidth) : openedMixin(theme, sidebarWidth)),
|
...(isCollapsed
|
||||||
|
? closedMixin(theme, miniSidebarWidth)
|
||||||
|
: openedMixin(theme, sidebarWidth)),
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -106,7 +118,6 @@ const StyledProfileBox = styled(Box, {
|
||||||
justifyContent: isCollapsed ? 'center' : 'flex-start',
|
justifyContent: isCollapsed ? 'center' : 'flex-start',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: theme.palette.primary.main,
|
||||||
}));
|
}));
|
||||||
|
|
@ -154,7 +165,7 @@ export default function Sidebar() {
|
||||||
toggleMobileSidebar,
|
toggleMobileSidebar,
|
||||||
isHydrated,
|
isHydrated,
|
||||||
sidebarWidth,
|
sidebarWidth,
|
||||||
miniSidebarWidth
|
miniSidebarWidth,
|
||||||
} = useCustomizerStore();
|
} = useCustomizerStore();
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
|
|
@ -227,7 +238,11 @@ export default function Sidebar() {
|
||||||
<StyledProfileContainer>
|
<StyledProfileContainer>
|
||||||
<StyledProfileBox isCollapsed={isCollapse && lgUp}>
|
<StyledProfileBox isCollapsed={isCollapse && lgUp}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={isCollapse && lgUp ? user?.nome || user?.userName || 'Usuário' : ''}
|
title={
|
||||||
|
isCollapse && lgUp
|
||||||
|
? user?.nome || user?.userName || 'Usuário'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
>
|
>
|
||||||
<StyledAvatar>
|
<StyledAvatar>
|
||||||
|
|
@ -240,7 +255,10 @@ export default function Sidebar() {
|
||||||
noWrap
|
noWrap
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => theme.palette.mode === 'dark' ? theme.palette.text.primary : '#1a1a1a',
|
color: (theme) =>
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: '#1a1a1a',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user?.nome || user?.userName || 'Usuário'}
|
{user?.nome || user?.userName || 'Usuário'}
|
||||||
|
|
@ -249,7 +267,10 @@ export default function Sidebar() {
|
||||||
variant="caption"
|
variant="caption"
|
||||||
noWrap
|
noWrap
|
||||||
sx={{
|
sx={{
|
||||||
color: (theme) => theme.palette.mode === 'dark' ? theme.palette.text.secondary : '#4a5568',
|
color: (theme) =>
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? theme.palette.text.secondary
|
||||||
|
: '#4a5568',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user?.nomeFilial || 'Usuário'}
|
{user?.nomeFilial || 'Usuário'}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,9 @@ const SidebarItems = ({ open, onItemClick }: SidebarItemsProps) => {
|
||||||
{Menuitems.map((item) => {
|
{Menuitems.map((item) => {
|
||||||
// SubHeader
|
// SubHeader
|
||||||
if (item.subheader) {
|
if (item.subheader) {
|
||||||
return <NavGroup item={item} hideMenu={hideMenu} key={item.subheader} />;
|
return (
|
||||||
|
<NavGroup item={item} hideMenu={hideMenu} key={item.subheader} />
|
||||||
|
);
|
||||||
|
|
||||||
// If Sub Menu
|
// If Sub Menu
|
||||||
} else if (item.children) {
|
} else if (item.children) {
|
||||||
|
|
|
||||||
|
|
@ -31,27 +31,34 @@ interface StyledListItemButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledListItemButton = styled(ListItemButton, {
|
const StyledListItemButton = styled(ListItemButton, {
|
||||||
shouldForwardProp: (prop) => prop !== 'open' && prop !== 'active' && prop !== 'level' && prop !== 'hideMenu',
|
shouldForwardProp: (prop) =>
|
||||||
})<StyledListItemButtonProps>(({ theme, open, active, level = 1, hideMenu }) => ({
|
prop !== 'open' &&
|
||||||
marginBottom: '2px',
|
prop !== 'active' &&
|
||||||
padding: '8px 10px',
|
prop !== 'level' &&
|
||||||
paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px',
|
prop !== 'hideMenu',
|
||||||
backgroundColor: open && level < 2 ? theme.palette.primary.main : '',
|
})<StyledListItemButtonProps>(
|
||||||
whiteSpace: 'nowrap',
|
({ theme, open, active, level = 1, hideMenu }) => ({
|
||||||
borderRadius: '7px',
|
marginBottom: '2px',
|
||||||
'&:hover': {
|
padding: '8px 10px',
|
||||||
backgroundColor: active || open
|
paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px',
|
||||||
? theme.palette.primary.main
|
backgroundColor: open && level < 2 ? theme.palette.primary.main : '',
|
||||||
: theme.palette.primary.light,
|
whiteSpace: 'nowrap',
|
||||||
color: active || open ? 'white' : theme.palette.primary.main,
|
borderRadius: '7px',
|
||||||
},
|
'&:hover': {
|
||||||
color:
|
backgroundColor:
|
||||||
open && level < 2
|
active || open
|
||||||
? 'white'
|
? theme.palette.primary.main
|
||||||
: level > 1 && open
|
: theme.palette.primary.light,
|
||||||
? theme.palette.primary.main
|
color: active || open ? 'white' : theme.palette.primary.main,
|
||||||
: theme.palette.text.secondary,
|
},
|
||||||
}));
|
color:
|
||||||
|
open && level < 2
|
||||||
|
? 'white'
|
||||||
|
: level > 1 && open
|
||||||
|
? theme.palette.primary.main
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const StyledListItemIcon = styled(ListItemIcon)(() => ({
|
const StyledListItemIcon = styled(ListItemIcon)(() => ({
|
||||||
minWidth: '36px',
|
minWidth: '36px',
|
||||||
|
|
@ -128,9 +135,7 @@ export default function NavCollapse({
|
||||||
hideMenu={hideMenu}
|
hideMenu={hideMenu}
|
||||||
key={menu?.id}
|
key={menu?.id}
|
||||||
>
|
>
|
||||||
<StyledListItemIcon>
|
<StyledListItemIcon>{menuIcon}</StyledListItemIcon>
|
||||||
{menuIcon}
|
|
||||||
</StyledListItemIcon>
|
|
||||||
<ListItemText color="inherit">
|
<ListItemText color="inherit">
|
||||||
{hideMenu ? '' : <>{menu.title}</>}
|
{hideMenu ? '' : <>{menu.title}</>}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,3 @@ export default function NavGroup({ item, hideMenu }: NavGroupProps) {
|
||||||
</ListSubheaderStyle>
|
</ListSubheaderStyle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,18 @@ interface StyledListItemButtonProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledListItemButton = styled(ListItemButton, {
|
const StyledListItemButton = styled(ListItemButton, {
|
||||||
shouldForwardProp: (prop) => prop !== 'active' && prop !== 'level' && prop !== 'hideMenu',
|
shouldForwardProp: (prop) =>
|
||||||
|
prop !== 'active' && prop !== 'level' && prop !== 'hideMenu',
|
||||||
})<StyledListItemButtonProps>(({ theme, active, level = 1, hideMenu }) => ({
|
})<StyledListItemButtonProps>(({ theme, active, level = 1, hideMenu }) => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
marginBottom: '2px',
|
marginBottom: '2px',
|
||||||
padding: '8px 10px',
|
padding: '8px 10px',
|
||||||
borderRadius: '7px',
|
borderRadius: '7px',
|
||||||
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
||||||
color: active && level > 1 ? `${theme.palette.primary.main}!important` : theme.palette.text.secondary,
|
color:
|
||||||
|
active && level > 1
|
||||||
|
? `${theme.palette.primary.main}!important`
|
||||||
|
: theme.palette.text.secondary,
|
||||||
paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px',
|
paddingLeft: hideMenu ? '10px' : level > 2 ? `${level * 15}px` : '10px',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.primary.light,
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
|
@ -61,7 +65,8 @@ const StyledListItemIcon = styled(ListItemIcon, {
|
||||||
})<StyledListItemIconProps>(({ theme, active, level = 1 }) => ({
|
})<StyledListItemIconProps>(({ theme, active, level = 1 }) => ({
|
||||||
minWidth: '36px',
|
minWidth: '36px',
|
||||||
padding: '3px 0',
|
padding: '3px 0',
|
||||||
color: active && level > 1 ? `${theme.palette.primary.main}!important` : 'inherit',
|
color:
|
||||||
|
active && level > 1 ? `${theme.palette.primary.main}!important` : 'inherit',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function NavItem({
|
export default function NavItem({
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,3 @@ export interface MenuItemType {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,29 @@ export const useCustomizerStore = create<CustomizerState>()(
|
||||||
isHydrated: false,
|
isHydrated: false,
|
||||||
|
|
||||||
setDarkMode: (mode) => set({ activeMode: mode }),
|
setDarkMode: (mode) => set({ activeMode: mode }),
|
||||||
|
|
||||||
toggleSidebar: () => set((state) => ({
|
toggleSidebar: () =>
|
||||||
isCollapse: !state.isCollapse
|
set((state) => ({
|
||||||
})),
|
isCollapse: !state.isCollapse,
|
||||||
|
})),
|
||||||
toggleMobileSidebar: () => set((state) => ({
|
|
||||||
isMobileSidebar: !state.isMobileSidebar
|
toggleMobileSidebar: () =>
|
||||||
})),
|
set((state) => ({
|
||||||
|
isMobileSidebar: !state.isMobileSidebar,
|
||||||
|
})),
|
||||||
|
|
||||||
setHydrated: () => set({ isHydrated: true }),
|
setHydrated: () => set({ isHydrated: true }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'modernize-layout-settings',
|
name: 'modernize-layout-settings',
|
||||||
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();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,42 @@
|
||||||
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
import axios, {
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
import { getAccessToken, handleTokenRefresh } from './utils/tokenRefresh';
|
import { getAccessToken, handleTokenRefresh } from './utils/tokenRefresh';
|
||||||
|
|
||||||
const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL;
|
const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL;
|
||||||
|
|
||||||
export const authApi: AxiosInstance = axios.create({
|
export const authApi: AxiosInstance = axios.create({
|
||||||
baseURL: AUTH_API_URL,
|
baseURL: AUTH_API_URL,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const addToken = (config: InternalAxiosRequestConfig) => {
|
const addToken = (config: InternalAxiosRequestConfig) => {
|
||||||
if (globalThis.window !== undefined) {
|
if (globalThis.window !== undefined) {
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return config;
|
}
|
||||||
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
authApi.interceptors.request.use(addToken);
|
authApi.interceptors.request.use(addToken);
|
||||||
|
|
||||||
const handleResponseError = async (error: AxiosError) => {
|
const handleResponseError = async (error: AxiosError) => {
|
||||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
||||||
|
_retry?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
if (!originalRequest) {
|
if (!originalRequest) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleTokenRefresh(error, originalRequest, authApi);
|
return handleTokenRefresh(error, originalRequest, authApi);
|
||||||
};
|
};
|
||||||
|
|
||||||
authApi.interceptors.response.use((response) => response, handleResponseError);
|
authApi.interceptors.response.use((response) => response, handleResponseError);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,198 +7,204 @@ import { useAuth } from '../hooks/useAuth';
|
||||||
jest.mock('../hooks/useAuth');
|
jest.mock('../hooks/useAuth');
|
||||||
|
|
||||||
const createWrapper = () => {
|
const createWrapper = () => {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: { retry: false },
|
queries: { retry: false },
|
||||||
mutations: { retry: false },
|
mutations: { retry: false },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return ({ children }: { children: React.ReactNode }) => (
|
return ({ children }: { children: React.ReactNode }) => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
{children}
|
);
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AuthLogin Component', () => {
|
describe('AuthLogin Component', () => {
|
||||||
const mockLoginMutation = {
|
const mockLoginMutation = {
|
||||||
mutate: jest.fn(),
|
mutate: jest.fn(),
|
||||||
isPending: false,
|
isPending: false,
|
||||||
isError: false,
|
isError: false,
|
||||||
isSuccess: false,
|
isSuccess: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
(useAuth as jest.Mock).mockReturnValue({
|
(useAuth as jest.Mock).mockReturnValue({
|
||||||
loginMutation: mockLoginMutation,
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Renderização', () => {
|
it('deve renderizar título quando fornecido', () => {
|
||||||
it('deve renderizar formulário de login', () => {
|
render(<AuthLogin title="Bem-vindo" />, { wrapper: createWrapper() });
|
||||||
render(<AuthLogin title="Login" />, { wrapper: createWrapper() });
|
expect(screen.getByText('Bem-vindo')).toBeInTheDocument();
|
||||||
|
|
||||||
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 renderizar checkbox "Manter-me conectado"', () => {
|
||||||
it('deve validar campos obrigatórios', async () => {
|
render(<AuthLogin />, { wrapper: createWrapper() });
|
||||||
render(<AuthLogin />, { wrapper: createWrapper() });
|
expect(screen.getByText(/manter-me conectado/i)).toBeInTheDocument();
|
||||||
|
|
||||||
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 renderizar link "Esqueceu sua senha"', () => {
|
||||||
it('deve submeter formulário com credenciais válidas', async () => {
|
render(<AuthLogin />, { wrapper: createWrapper() });
|
||||||
render(<AuthLogin />, { wrapper: createWrapper() });
|
const link = screen.getByText(/esqueceu sua senha/i);
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const usernameInput = screen.getByLabelText(/usuário/i);
|
describe('Validação', () => {
|
||||||
const passwordInput = screen.getByLabelText(/senha/i);
|
it('deve validar campos obrigatórios', async () => {
|
||||||
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
render(<AuthLogin />, { wrapper: createWrapper() });
|
||||||
|
|
||||||
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
|
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
||||||
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
fireEvent.click(submitButton);
|
||||||
fireEvent.click(submitButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockLoginMutation.mutate).toHaveBeenCalledWith({
|
expect(screen.getByText(/usuário é obrigatório/i)).toBeInTheDocument();
|
||||||
username: 'testuser',
|
});
|
||||||
password: 'password123',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Estados de Loading e Erro', () => {
|
it('deve validar senha mínima', async () => {
|
||||||
it('deve desabilitar botão durante loading', () => {
|
render(<AuthLogin />, { wrapper: createWrapper() });
|
||||||
const loadingMutation = {
|
|
||||||
...mockLoginMutation,
|
|
||||||
isPending: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
(useAuth as jest.Mock).mockReturnValue({
|
const usernameInput = screen.getByLabelText(/usuário/i);
|
||||||
loginMutation: loadingMutation,
|
const passwordInput = screen.getByLabelText(/senha/i);
|
||||||
});
|
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
||||||
|
|
||||||
render(<AuthLogin />, { wrapper: createWrapper() });
|
fireEvent.change(usernameInput, { target: { value: 'testuser' } });
|
||||||
|
fireEvent.change(passwordInput, { target: { value: '123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
const submitButton = screen.getByRole('button', { name: /logging in/i });
|
await waitFor(() => {
|
||||||
expect(submitButton).toBeDisabled();
|
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',
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('deve mostrar mensagem de erro quando login falha', () => {
|
describe('Estados de Loading e Erro', () => {
|
||||||
const errorMutation = {
|
it('deve desabilitar botão durante loading', () => {
|
||||||
...mockLoginMutation,
|
const loadingMutation = {
|
||||||
isError: true,
|
...mockLoginMutation,
|
||||||
error: {
|
isPending: true,
|
||||||
response: {
|
};
|
||||||
data: { message: 'Credenciais inválidas' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
(useAuth as jest.Mock).mockReturnValue({
|
(useAuth as jest.Mock).mockReturnValue({
|
||||||
loginMutation: errorMutation,
|
loginMutation: loadingMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<AuthLogin />, { wrapper: createWrapper() });
|
render(<AuthLogin />, { wrapper: createWrapper() });
|
||||||
|
|
||||||
expect(screen.getByText(/credenciais inválidas/i)).toBeInTheDocument();
|
const submitButton = screen.getByRole('button', { name: /logging in/i });
|
||||||
});
|
expect(submitButton).toBeDisabled();
|
||||||
|
|
||||||
// 🐛 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', () => {
|
it('deve mostrar mensagem de erro quando login falha', () => {
|
||||||
// 🐛 BUG: Link "Esqueceu senha" vai para home
|
const errorMutation = {
|
||||||
it('🐛 BUG: link "Esqueceu senha" deve ir para /forgot-password', () => {
|
...mockLoginMutation,
|
||||||
render(<AuthLogin />, { wrapper: createWrapper() });
|
isError: true,
|
||||||
|
error: {
|
||||||
|
response: {
|
||||||
|
data: { message: 'Credenciais inválidas' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const link = screen.getByText(/esqueceu sua senha/i).closest('a');
|
(useAuth as jest.Mock).mockReturnValue({
|
||||||
|
loginMutation: errorMutation,
|
||||||
|
});
|
||||||
|
|
||||||
// ❌ ESTE TESTE VAI FALHAR - href é "/"
|
render(<AuthLogin />, { wrapper: createWrapper() });
|
||||||
expect(link).toHaveAttribute('href', '/forgot-password');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🐛 BUG: Checkbox não funciona
|
expect(screen.getByText(/credenciais inválidas/i)).toBeInTheDocument();
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🐛 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,150 +1,157 @@
|
||||||
"use client"
|
'use client';
|
||||||
import Box from "@mui/material/Box";
|
import Box from '@mui/material/Box';
|
||||||
import Button from "@mui/material/Button";
|
import Button from '@mui/material/Button';
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from '@mui/material/Divider';
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import FormGroup from "@mui/material/FormGroup";
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from '@mui/material/Stack';
|
||||||
import { TextField, Alert } from "@mui/material";
|
import { TextField, Alert } from '@mui/material';
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from '@mui/material/Typography';
|
||||||
import NextLink from "next/link";
|
import NextLink from 'next/link';
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { loginSchema, LoginInput, AuthLoginProps } from "../interfaces/types";
|
import { loginSchema, LoginInput, AuthLoginProps } from '../interfaces/types';
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import CustomCheckbox from "../components/forms/theme-elements/CustomCheckbox";
|
import CustomCheckbox from '../components/forms/theme-elements/CustomCheckbox';
|
||||||
import CustomFormLabel from "../components/forms/theme-elements/CustomFormLabel";
|
import CustomFormLabel from '../components/forms/theme-elements/CustomFormLabel';
|
||||||
import AuthSocialButtons from "./AuthSocialButtons";
|
import AuthSocialButtons from './AuthSocialButtons';
|
||||||
|
|
||||||
const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
|
const AuthLogin = ({ title, subtitle, subtext }: AuthLoginProps) => {
|
||||||
const { loginMutation } = useAuth();
|
const { loginMutation } = useAuth();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<LoginInput>({
|
} = useForm<LoginInput>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: '',
|
||||||
password: "",
|
password: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: LoginInput) => {
|
const onSubmit = (data: LoginInput) => {
|
||||||
loginMutation.mutate(data);
|
loginMutation.mutate(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{title ? (
|
{title ? (
|
||||||
<Typography fontWeight="700" variant="h4" mb={1}>
|
<Typography fontWeight="700" variant="h4" mb={1}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{subtext}
|
{subtext}
|
||||||
|
|
||||||
<AuthSocialButtons title="Sign in with" />
|
<AuthSocialButtons title="Sign in with" />
|
||||||
<Box mt={4} mb={2}>
|
<Box mt={4} mb={2}>
|
||||||
<Divider>
|
<Divider>
|
||||||
<Typography
|
<Typography
|
||||||
component="span"
|
component="span"
|
||||||
color="textSecondary"
|
color="textSecondary"
|
||||||
variant="h6"
|
variant="h6"
|
||||||
fontWeight="400"
|
fontWeight="400"
|
||||||
position="relative"
|
position="relative"
|
||||||
px={2}
|
px={2}
|
||||||
>
|
>
|
||||||
ou faça login com
|
ou faça login com
|
||||||
</Typography>
|
</Typography>
|
||||||
</Divider>
|
</Divider>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{loginMutation.isError && (
|
{loginMutation.isError && (
|
||||||
<Box mt={3}>
|
<Box mt={3}>
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
{(() => {
|
{(() => {
|
||||||
const error = loginMutation.error;
|
const error = loginMutation.error;
|
||||||
if (error && typeof error === 'object' && 'response' in error) {
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
const axiosError = error as { response?: { data?: { message?: string } } };
|
const axiosError = error as {
|
||||||
return axiosError.response?.data?.message || 'Erro ao realizar login';
|
response?: { data?: { message?: string } };
|
||||||
}
|
};
|
||||||
return 'Erro ao realizar login';
|
return (
|
||||||
})()}
|
axiosError.response?.data?.message || 'Erro ao realizar login'
|
||||||
</Alert>
|
);
|
||||||
</Box>
|
}
|
||||||
)}
|
return 'Erro ao realizar login';
|
||||||
|
})()}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Box>
|
<Box>
|
||||||
<CustomFormLabel htmlFor="username">Usuário</CustomFormLabel>
|
<CustomFormLabel htmlFor="username">Usuário</CustomFormLabel>
|
||||||
<TextField
|
<TextField
|
||||||
id="username"
|
id="username"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
{...register('username')}
|
{...register('username')}
|
||||||
error={!!errors.username}
|
error={!!errors.username}
|
||||||
helperText={errors.username?.message}
|
helperText={errors.username?.message}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<CustomFormLabel htmlFor="password">Senha</CustomFormLabel>
|
<CustomFormLabel htmlFor="password">Senha</CustomFormLabel>
|
||||||
<TextField
|
<TextField
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
error={!!errors.password}
|
error={!!errors.password}
|
||||||
helperText={errors.password?.message}
|
helperText={errors.password?.message}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack
|
<Stack
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
my={2}
|
my={2}
|
||||||
spacing={1}
|
spacing={1}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
>
|
>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<CustomCheckbox defaultChecked />}
|
control={<CustomCheckbox defaultChecked />}
|
||||||
label="Manter-me conectado"
|
label="Manter-me conectado"
|
||||||
sx={{ whiteSpace: 'nowrap' }}
|
sx={{ whiteSpace: 'nowrap' }}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<Typography
|
<Typography
|
||||||
fontWeight="500"
|
fontWeight="500"
|
||||||
sx={{
|
sx={{
|
||||||
textDecoration: "none",
|
textDecoration: 'none',
|
||||||
color: "primary.main",
|
color: 'primary.main',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NextLink href="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
<NextLink
|
||||||
Esqueceu sua senha ?
|
href="/"
|
||||||
</NextLink>
|
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||||
</Typography>
|
>
|
||||||
</Stack>
|
Esqueceu sua senha ?
|
||||||
</Stack>
|
</NextLink>
|
||||||
<Box>
|
</Typography>
|
||||||
<Button
|
</Stack>
|
||||||
color="primary"
|
</Stack>
|
||||||
variant="contained"
|
<Box>
|
||||||
size="large"
|
<Button
|
||||||
fullWidth
|
color="primary"
|
||||||
type="submit"
|
variant="contained"
|
||||||
disabled={loginMutation.isPending}
|
size="large"
|
||||||
>
|
fullWidth
|
||||||
{loginMutation.isPending ? 'Logging in...' : 'Sign In'}
|
type="submit"
|
||||||
</Button>
|
disabled={loginMutation.isPending}
|
||||||
</Box>
|
>
|
||||||
</form>
|
{loginMutation.isPending ? 'Logging in...' : 'Sign In'}
|
||||||
{subtitle}
|
</Button>
|
||||||
</>
|
</Box>
|
||||||
);
|
</form>
|
||||||
|
{subtitle}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthLogin;
|
export default AuthLogin;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
'use client';
|
||||||
import CustomSocialButton from "../components/forms/theme-elements/CustomSocialButton";
|
import CustomSocialButton from '../components/forms/theme-elements/CustomSocialButton';
|
||||||
import { Stack } from "@mui/system";
|
import { Stack } from '@mui/system';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
|
|
@ -18,11 +18,20 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack direction="row" justifyContent="center" spacing={2} mt={3} flexWrap="wrap">
|
<Stack
|
||||||
<CustomSocialButton onClick={handleGoogleSignIn} sx={{ flex: 1, minWidth: '140px' }}>
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
spacing={2}
|
||||||
|
mt={3}
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
<CustomSocialButton
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
sx={{ flex: 1, minWidth: '140px' }}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={"/images/svgs/google-icon.svg"}
|
src={'/images/svgs/google-icon.svg'}
|
||||||
alt={"icon1"}
|
alt={'icon1'}
|
||||||
sx={{
|
sx={{
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
|
|
@ -32,18 +41,21 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: "none", sm: "flex" },
|
display: { xs: 'none', sm: 'flex' },
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
mr: { sm: "3px" },
|
mr: { sm: '3px' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Google
|
Google
|
||||||
</Box>
|
</Box>
|
||||||
</CustomSocialButton>
|
</CustomSocialButton>
|
||||||
<CustomSocialButton onClick={handleGithubSignIn} sx={{ flex: 1, minWidth: '140px' }}>
|
<CustomSocialButton
|
||||||
|
onClick={handleGithubSignIn}
|
||||||
|
sx={{ flex: 1, minWidth: '140px' }}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={"/images/svgs/git-icon.svg"}
|
src={'/images/svgs/git-icon.svg'}
|
||||||
alt={"icon2"}
|
alt={'icon2'}
|
||||||
sx={{
|
sx={{
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
|
|
@ -53,9 +65,9 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: "none", sm: "flex" },
|
display: { xs: 'none', sm: 'flex' },
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
mr: { sm: "3px" },
|
mr: { sm: '3px' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
GitHub
|
GitHub
|
||||||
|
|
@ -64,6 +76,6 @@ const AuthSocialButtons = ({ title }: AuthSocialButtonsProps) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AuthSocialButtons;
|
export default AuthSocialButtons;
|
||||||
|
|
|
||||||
|
|
@ -7,42 +7,41 @@ import { profileService } from '../../profile/services/profile.service';
|
||||||
import { mapToSafeProfile } from '../utils/mappers';
|
import { mapToSafeProfile } from '../utils/mappers';
|
||||||
|
|
||||||
export function AuthInitializer({ children }: { children: React.ReactNode }) {
|
export function AuthInitializer({ children }: { children: React.ReactNode }) {
|
||||||
const { setUser, logout } = useAuthStore();
|
const { setUser, logout } = useAuthStore();
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
const [isChecking, setIsChecking] = useState(true);
|
const [isChecking, setIsChecking] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialized.current) return;
|
if (initialized.current) return;
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
|
|
||||||
const validateSession = async () => {
|
const validateSession = async () => {
|
||||||
try {
|
try {
|
||||||
|
await loginService.refreshToken();
|
||||||
|
|
||||||
await loginService.refreshToken();
|
const profile = await profileService.getMe();
|
||||||
|
setUser(mapToSafeProfile(profile));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Sessão expirada ou inválida', error);
|
||||||
|
logout();
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const profile = await profileService.getMe();
|
validateSession();
|
||||||
setUser(mapToSafeProfile(profile));
|
}, [setUser, logout]);
|
||||||
} catch (error) {
|
|
||||||
console.warn('Sessão expirada ou inválida', error);
|
|
||||||
logout();
|
|
||||||
} finally {
|
|
||||||
setIsChecking(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validateSession();
|
if (isChecking) {
|
||||||
}, [setUser, logout]);
|
return (
|
||||||
|
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||||
|
<div className="animate-pulse flex flex-col items-center gap-4">
|
||||||
|
<div className="h-12 w-12 rounded-full bg-primary/20" />
|
||||||
|
<p className="text-sm text-muted-foreground">Validando acesso...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isChecking) {
|
return <>{children}</>;
|
||||||
return (
|
}
|
||||||
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
|
||||||
<div className="animate-pulse flex flex-col items-center gap-4">
|
|
||||||
<div className="h-12 w-12 rounded-full bg-primary/20" />
|
|
||||||
<p className="text-sm text-muted-foreground">Validando acesso...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import AuthLogin from '../authForms/AuthLogin';
|
||||||
const GradientGrid = styled(Grid)(({ theme }) => ({
|
const GradientGrid = styled(Grid)(({ theme }) => ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundColor: '#d2f1df',
|
backgroundColor: '#d2f1df',
|
||||||
backgroundImage: 'linear-gradient(45deg, #d2f1df 0%, #d3d7fa 50%, #bad8f4 100%)',
|
backgroundImage:
|
||||||
|
'linear-gradient(45deg, #d2f1df 0%, #d3d7fa 50%, #bad8f4 100%)',
|
||||||
backgroundSize: '400% 400%',
|
backgroundSize: '400% 400%',
|
||||||
backgroundPosition: '0% 50%',
|
backgroundPosition: '0% 50%',
|
||||||
animation: 'gradient 15s ease infinite',
|
animation: 'gradient 15s ease infinite',
|
||||||
|
|
@ -25,7 +26,12 @@ const GradientGrid = styled(Grid)(({ theme }) => ({
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
return (
|
return (
|
||||||
<PageContainer title="Login" description="Página de Login">
|
<PageContainer title="Login" description="Página de Login">
|
||||||
<Grid container spacing={0} justifyContent="center" sx={{ height: '100vh', width: '100%' }}>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
justifyContent="center"
|
||||||
|
sx={{ height: '100vh', width: '100%' }}
|
||||||
|
>
|
||||||
<GradientGrid size={{ xs: 12, lg: 7, xl: 8 }}>
|
<GradientGrid size={{ xs: 12, lg: 7, xl: 8 }}>
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
|
|
@ -54,7 +60,12 @@ export default function Login() {
|
||||||
</Box>
|
</Box>
|
||||||
</GradientGrid>
|
</GradientGrid>
|
||||||
<Grid size={{ xs: 12, lg: 5, xl: 4 }}>
|
<Grid size={{ xs: 12, lg: 5, xl: 4 }}>
|
||||||
<Box display="flex" justifyContent="center" alignItems="center" sx={{ backgroundColor: 'white' }}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
sx={{ backgroundColor: 'white' }}
|
||||||
|
>
|
||||||
<Box p={4} sx={{ width: '100%', maxWidth: '450px' }}>
|
<Box p={4} sx={{ width: '100%', maxWidth: '450px' }}>
|
||||||
<AuthLogin
|
<AuthLogin
|
||||||
subtitle={
|
subtitle={
|
||||||
|
|
@ -66,7 +77,14 @@ export default function Login() {
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NextLink href="/auth/auth1/register" style={{ textDecoration: 'none', color: 'inherit', whiteSpace: 'nowrap' }}>
|
<NextLink
|
||||||
|
href="/auth/auth1/register"
|
||||||
|
style={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
Criar uma conta
|
Criar uma conta
|
||||||
</NextLink>
|
</NextLink>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
@ -78,5 +96,5 @@ export default function Login() {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
)
|
);
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ type Props = {
|
||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageContainer = ({ children }: Props) => (
|
const PageContainer = ({ children }: Props) => <div>{children}</div>;
|
||||||
<div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default PageContainer;
|
export default PageContainer;
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,10 @@ const DashboardCard = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
sx={{ padding: 0, border: !isCardShadow ? `1px solid ${borderColor}` : 'none' }}
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
border: !isCardShadow ? `1px solid ${borderColor}` : 'none',
|
||||||
|
}}
|
||||||
elevation={isCardShadow ? 9 : 0}
|
elevation={isCardShadow ? 9 : 0}
|
||||||
variant={!isCardShadow ? 'outlined' : undefined}
|
variant={!isCardShadow ? 'outlined' : undefined}
|
||||||
>
|
>
|
||||||
|
|
@ -43,7 +46,7 @@ const DashboardCard = ({
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
) : (
|
) : (
|
||||||
<CardContent sx={{p: "30px"}}>
|
<CardContent sx={{ p: '30px' }}>
|
||||||
{title ? (
|
{title ? (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { useEffect, ReactElement } from 'react';
|
import { useEffect, ReactElement } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export default function ScrollToTop({ children }: { children: ReactElement | null }) {
|
export default function ScrollToTop({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactElement | null;
|
||||||
|
}) {
|
||||||
const { pathname } = useRouter();
|
const { pathname } = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { styled } from "@mui/system";
|
import { styled } from '@mui/system';
|
||||||
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
|
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
|
||||||
|
|
||||||
const BpIcon = styled('span')(({ theme }) => ({
|
const BpIcon = styled('span')(({ theme }) => ({
|
||||||
|
|
@ -21,7 +21,10 @@ const BpIcon = styled('span')(({ theme }) => ({
|
||||||
outlineOffset: 2,
|
outlineOffset: 2,
|
||||||
},
|
},
|
||||||
'input:hover ~ &': {
|
'input:hover ~ &': {
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.primary : theme.palette.primary,
|
backgroundColor:
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? theme.palette.primary
|
||||||
|
: theme.palette.primary,
|
||||||
},
|
},
|
||||||
'input:disabled ~ &': {
|
'input:disabled ~ &': {
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
|
|
@ -54,7 +57,9 @@ function CustomCheckbox(props: CheckboxProps) {
|
||||||
checkedIcon={
|
checkedIcon={
|
||||||
<BpCheckedIcon
|
<BpCheckedIcon
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: props.color ? `${props.color}.main` : 'primary.main',
|
backgroundColor: props.color
|
||||||
|
? `${props.color}.main`
|
||||||
|
: 'primary.main',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,20 @@ import React from 'react';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
|
|
||||||
const CustomTextField = styled((props: any) => <TextField {...props} />)(({ theme }) => ({
|
const CustomTextField = styled((props: any) => <TextField {...props} />)(
|
||||||
'& .MuiOutlinedInput-input::-webkit-input-placeholder': {
|
({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
'& .MuiOutlinedInput-input::-webkit-input-placeholder': {
|
||||||
opacity: '0.8',
|
color: theme.palette.text.secondary,
|
||||||
},
|
opacity: '0.8',
|
||||||
'& .MuiOutlinedInput-input.Mui-disabled::-webkit-input-placeholder': {
|
},
|
||||||
color: theme.palette.text.secondary,
|
'& .MuiOutlinedInput-input.Mui-disabled::-webkit-input-placeholder': {
|
||||||
opacity: '1',
|
color: theme.palette.text.secondary,
|
||||||
},
|
opacity: '1',
|
||||||
'& .Mui-disabled .MuiOutlinedInput-notchedOutline': {
|
},
|
||||||
borderColor: theme.palette.grey[200],
|
'& .Mui-disabled .MuiOutlinedInput-notchedOutline': {
|
||||||
},
|
borderColor: theme.palette.grey[200],
|
||||||
}));
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export default CustomTextField;
|
export default CustomTextField;
|
||||||
|
|
|
||||||
|
|
@ -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,8 +30,7 @@ refreshToken
|
||||||
: UUID do refresh token (HttpOnly, Secure, 7 dias)
|
: UUID do refresh token (HttpOnly, Secure, 7 dias)
|
||||||
Erros Comuns
|
Erros Comuns
|
||||||
400 Bad Request: Campos obrigatórios ausentes.
|
400 Bad Request: Campos obrigatórios ausentes.
|
||||||
401/500: Usuário ou senha inválidos.
|
401/500: Usuário ou senha inválidos. 2. Renovar Token (Refresh)
|
||||||
2. Renovar Token (Refresh)
|
|
||||||
Gera um novo par de tokens usando um Refresh Token válido.
|
Gera um novo par de tokens usando um Refresh Token válido.
|
||||||
|
|
||||||
Método: POST
|
Método: POST
|
||||||
|
|
@ -41,24 +40,23 @@ 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
|
||||||
: Enviado automaticamente pelo navegador.
|
: Enviado automaticamente pelo navegador.
|
||||||
Response (200 OK)
|
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.
|
403 Forbidden: Token expirado ou inválido. 3. Obter Usuário Atual (Me)
|
||||||
3. Obter Usuário Atual (Me)
|
|
||||||
Retorna dados detalhados do usuário logado.
|
Retorna dados detalhados do usuário logado.
|
||||||
|
|
||||||
Método: GET
|
Método: GET
|
||||||
|
|
@ -66,21 +64,20 @@ 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.
|
404 Not Found: Usuário não encontrado no banco. 4. Logout
|
||||||
4. Logout
|
|
||||||
Invalida a sessão atual.
|
Invalida a sessão atual.
|
||||||
|
|
||||||
Método: POST
|
Método: POST
|
||||||
|
|
@ -90,16 +87,16 @@ 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:
|
||||||
|
|
||||||
Adiciona o Access Token à Blacklist (Redis).
|
Adiciona o Access Token à Blacklist (Redis).
|
||||||
Remove o Refresh Token do banco.
|
Remove o Refresh Token do banco.
|
||||||
Remove o Cookie
|
Remove o Cookie
|
||||||
refreshToken
|
refreshToken
|
||||||
.
|
.
|
||||||
|
|
|
||||||
|
|
@ -18,29 +18,30 @@ export function useAuth() {
|
||||||
try {
|
try {
|
||||||
const profile = await profileService.getMe();
|
const profile = await profileService.getMe();
|
||||||
const safeProfile = mapToSafeProfile(profile);
|
const safeProfile = mapToSafeProfile(profile);
|
||||||
|
|
||||||
setUser(safeProfile);
|
setUser(safeProfile);
|
||||||
queryClient.setQueryData(['auth-me'], safeProfile);
|
queryClient.setQueryData(['auth-me'], safeProfile);
|
||||||
|
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Falha ao carregar perfil:', error);
|
console.error('Falha ao carregar perfil:', error);
|
||||||
logout();
|
logout();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const useMe = () => useQuery({
|
const useMe = () =>
|
||||||
queryKey: ['auth-me'],
|
useQuery({
|
||||||
queryFn: async () => {
|
queryKey: ['auth-me'],
|
||||||
const data = await profileService.getMe();
|
queryFn: async () => {
|
||||||
const safeData = mapToSafeProfile(data);
|
const data = await profileService.getMe();
|
||||||
setUser(safeData);
|
const safeData = mapToSafeProfile(data);
|
||||||
return safeData;
|
setUser(safeData);
|
||||||
},
|
return safeData;
|
||||||
retry: false,
|
},
|
||||||
staleTime: Infinity,
|
retry: false,
|
||||||
});
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -48,10 +49,10 @@ export function useAuth() {
|
||||||
} finally {
|
} finally {
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
logoutStore();
|
logoutStore();
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
globalThis.location.href = '/';
|
globalThis.location.href = '/';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { loginMutation, useMe, logout };
|
return { loginMutation, useMe, logout };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Login feature barrel export
|
||||||
|
export { default as LoginForm } from './components/LoginForm';
|
||||||
|
export { AuthInitializer } from './components/AuthInitializer';
|
||||||
|
export { useAuth } from './hooks/useAuth';
|
||||||
|
export { useAuthStore } from './store/useAuthStore';
|
||||||
|
export type {
|
||||||
|
LoginInput,
|
||||||
|
TokenResponse,
|
||||||
|
AuthState,
|
||||||
|
AuthLoginProps,
|
||||||
|
} from './interfaces/types';
|
||||||
|
|
@ -2,9 +2,17 @@ import { z } from 'zod';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { UserProfile } from '../../profile/types';
|
import type { UserProfile } from '../../profile/types';
|
||||||
|
|
||||||
import { loginSchema, tokenResponseSchema, logoutResponseSchema } from '../schemas/schemas';
|
import {
|
||||||
|
loginSchema,
|
||||||
|
tokenResponseSchema,
|
||||||
|
logoutResponseSchema,
|
||||||
|
} from '../schemas/schemas';
|
||||||
|
|
||||||
export { loginSchema, tokenResponseSchema, logoutResponseSchema } from '../schemas/schemas';
|
export {
|
||||||
|
loginSchema,
|
||||||
|
tokenResponseSchema,
|
||||||
|
logoutResponseSchema,
|
||||||
|
} from '../schemas/schemas';
|
||||||
|
|
||||||
export type LoginInput = z.infer<typeof loginSchema>;
|
export type LoginInput = z.infer<typeof loginSchema>;
|
||||||
export type TokenResponse = z.infer<typeof tokenResponseSchema>;
|
export type TokenResponse = z.infer<typeof tokenResponseSchema>;
|
||||||
|
|
@ -35,4 +43,4 @@ export interface AuthState {
|
||||||
setUser: (user: UserProfile | null) => void;
|
setUser: (user: UserProfile | null) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
hydrate: () => void;
|
hydrate: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,115 @@
|
||||||
import { loginSchema, tokenResponseSchema } from './schemas';
|
import { loginSchema, tokenResponseSchema } from './schemas';
|
||||||
|
|
||||||
describe('Login Schemas', () => {
|
describe('Login Schemas', () => {
|
||||||
describe('loginSchema', () => {
|
describe('loginSchema', () => {
|
||||||
it('deve validar credenciais válidas', () => {
|
it('deve validar credenciais válidas', () => {
|
||||||
const result = loginSchema.safeParse({
|
const result = loginSchema.safeParse({
|
||||||
username: 'user123',
|
username: 'user123',
|
||||||
password: 'pass1234',
|
password: 'pass1234',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.data.username).toBe('user123');
|
expect(result.data.username).toBe('user123');
|
||||||
expect(result.data.password).toBe('pass1234');
|
expect(result.data.password).toBe('pass1234');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
it('deve rejeitar username muito curto', () => {
|
|
||||||
const result = loginSchema.safeParse({
|
|
||||||
username: 'ab',
|
|
||||||
password: 'pass1234',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
if (!result.success) {
|
|
||||||
expect(result.error.issues[0].message).toContain('obrigatório');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deve rejeitar senha muito curta', () => {
|
|
||||||
const result = loginSchema.safeParse({
|
|
||||||
username: 'user123',
|
|
||||||
password: '123',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
if (!result.success) {
|
|
||||||
expect(result.error.issues[0].message).toContain('mínimo 4 caracteres');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🐛 TESTE QUE REVELA BUG: Senha muito fraca é aceita
|
|
||||||
it('🐛 BUG: deve rejeitar senhas fracas (menos de 8 caracteres)', () => {
|
|
||||||
const result = loginSchema.safeParse({
|
|
||||||
username: 'user123',
|
|
||||||
password: '1234', // Apenas 4 caracteres - muito fraco!
|
|
||||||
});
|
|
||||||
|
|
||||||
// ❌ ESTE TESTE VAI FALHAR - senha fraca é aceita!
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deve rejeitar username vazio', () => {
|
|
||||||
const result = loginSchema.safeParse({
|
|
||||||
username: '',
|
|
||||||
password: 'pass1234',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deve rejeitar password vazio', () => {
|
|
||||||
const result = loginSchema.safeParse({
|
|
||||||
username: 'user123',
|
|
||||||
password: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tokenResponseSchema', () => {
|
it('deve rejeitar username muito curto', () => {
|
||||||
it('deve validar resposta de token válida', () => {
|
const result = loginSchema.safeParse({
|
||||||
const result = tokenResponseSchema.safeParse({
|
username: 'ab',
|
||||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
password: 'pass1234',
|
||||||
type: 'Bearer',
|
});
|
||||||
expiresIn: 3600,
|
|
||||||
username: 'user123',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(false);
|
||||||
});
|
if (!result.success) {
|
||||||
|
expect(result.error.issues[0].message).toContain('obrigatório');
|
||||||
it('deve rejeitar token sem campos obrigatórios', () => {
|
}
|
||||||
const result = tokenResponseSchema.safeParse({
|
|
||||||
token: 'some-token',
|
|
||||||
// faltando type, expiresIn, username
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deve rejeitar expiresIn negativo', () => {
|
|
||||||
const result = tokenResponseSchema.safeParse({
|
|
||||||
token: 'some-token',
|
|
||||||
type: 'Bearer',
|
|
||||||
expiresIn: -100,
|
|
||||||
username: 'user123',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deve rejeitar expiresIn zero', () => {
|
|
||||||
const result = tokenResponseSchema.safeParse({
|
|
||||||
token: 'some-token',
|
|
||||||
type: 'Bearer',
|
|
||||||
expiresIn: 0,
|
|
||||||
username: 'user123',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deve rejeitar senha muito curta', () => {
|
||||||
|
const result = loginSchema.safeParse({
|
||||||
|
username: 'user123',
|
||||||
|
password: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error.issues[0].message).toContain('mínimo 4 caracteres');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🐛 TESTE QUE REVELA BUG: Senha muito fraca é aceita
|
||||||
|
it('🐛 BUG: deve rejeitar senhas fracas (menos de 8 caracteres)', () => {
|
||||||
|
const result = loginSchema.safeParse({
|
||||||
|
username: 'user123',
|
||||||
|
password: '1234', // Apenas 4 caracteres - muito fraco!
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ ESTE TESTE VAI FALHAR - senha fraca é aceita!
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve rejeitar username vazio', () => {
|
||||||
|
const result = loginSchema.safeParse({
|
||||||
|
username: '',
|
||||||
|
password: 'pass1234',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve rejeitar password vazio', () => {
|
||||||
|
const result = loginSchema.safeParse({
|
||||||
|
username: 'user123',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tokenResponseSchema', () => {
|
||||||
|
it('deve validar resposta de token válida', () => {
|
||||||
|
const result = tokenResponseSchema.safeParse({
|
||||||
|
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||||
|
type: 'Bearer',
|
||||||
|
expiresIn: 3600,
|
||||||
|
username: 'user123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve rejeitar token sem campos obrigatórios', () => {
|
||||||
|
const result = tokenResponseSchema.safeParse({
|
||||||
|
token: 'some-token',
|
||||||
|
// faltando type, expiresIn, username
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve rejeitar expiresIn negativo', () => {
|
||||||
|
const result = tokenResponseSchema.safeParse({
|
||||||
|
token: 'some-token',
|
||||||
|
type: 'Bearer',
|
||||||
|
expiresIn: -100,
|
||||||
|
username: 'user123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deve rejeitar expiresIn zero', () => {
|
||||||
|
const result = tokenResponseSchema.safeParse({
|
||||||
|
token: 'some-token',
|
||||||
|
type: 'Bearer',
|
||||||
|
expiresIn: 0,
|
||||||
|
username: 'user123',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = z.object({
|
||||||
username: z.string().min(3, 'Usuário é obrigatório'),
|
username: z.string().min(3, 'Usuário é obrigatório'),
|
||||||
password: z.string().min(4, 'Senha deve ter no mínimo 4 caracteres'),
|
password: z.string().min(4, 'Senha deve ter no mínimo 4 caracteres'),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema de resposta de autenticação
|
* Schema de resposta de autenticação
|
||||||
*
|
*
|
||||||
* Arquitetura Híbrida:
|
* Arquitetura Híbrida:
|
||||||
* - accessToken: Retornado no body, armazenado em memória
|
* - accessToken: Retornado no body, armazenado em memória
|
||||||
* - refreshToken: Enviado via cookie HTTP-only (não acessível ao JS)
|
* - refreshToken: Enviado via cookie HTTP-only (não acessível ao JS)
|
||||||
*/
|
*/
|
||||||
export const tokenResponseSchema = z.object({
|
export const tokenResponseSchema = z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
expiresIn: z.number().positive(),
|
expiresIn: z.number().positive(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
// refreshToken removido - agora apenas em cookie HTTP-only
|
// refreshToken removido - agora apenas em cookie HTTP-only
|
||||||
});
|
});
|
||||||
|
|
||||||
export const logoutResponseSchema = z.object({
|
export const logoutResponseSchema = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@ import {
|
||||||
LoginInput,
|
LoginInput,
|
||||||
TokenResponse,
|
TokenResponse,
|
||||||
LogoutResponse,
|
LogoutResponse,
|
||||||
tokenResponseSchema
|
tokenResponseSchema,
|
||||||
} from '../interfaces/types';
|
} from '../interfaces/types';
|
||||||
import { setTemporaryToken } from '../utils/tokenRefresh';
|
import { setTemporaryToken } from '../utils/tokenRefresh';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
|
|
||||||
|
|
||||||
const handleAuthSuccess = async (data: any): Promise<TokenResponse> => {
|
const handleAuthSuccess = async (data: any): Promise<TokenResponse> => {
|
||||||
// 1. Valida o schema do token
|
// 1. Valida o schema do token
|
||||||
const validatedData = tokenResponseSchema.parse(data);
|
const validatedData = tokenResponseSchema.parse(data);
|
||||||
|
|
@ -39,4 +38,4 @@ export const loginService = {
|
||||||
useAuthStore.getState().logout();
|
useAuthStore.getState().logout();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import { clearAuthData } from '../utils/tokenRefresh';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store de autenticação usando apenas dados não sensíveis
|
* Store de autenticação usando apenas dados não sensíveis
|
||||||
*
|
*
|
||||||
* Arquitetura Híbrida:
|
* Arquitetura Híbrida:
|
||||||
* - accessToken: Armazenado em memória (tokenRefresh.ts)
|
* - accessToken: Armazenado em memória (tokenRefresh.ts)
|
||||||
* - refreshToken: Gerenciado via cookies HTTP-only pelo backend
|
* - refreshToken: Gerenciado via cookies HTTP-only pelo backend
|
||||||
* - user profile: Persistido no localStorage (dados não sensíveis)
|
* - user profile: Persistido no localStorage (dados não sensíveis)
|
||||||
*
|
*
|
||||||
* O método hydrate() valida a sincronização entre token e estado ao recarregar
|
* O método hydrate() valida a sincronização entre token e estado ao recarregar
|
||||||
*/
|
*/
|
||||||
export const useAuthStore = create<AuthState>()(
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
|
@ -31,8 +31,7 @@ export const useAuthStore = create<AuthState>()(
|
||||||
* Valida sincronização entre token em memória e estado persistido
|
* Valida sincronização entre token em memória e estado persistido
|
||||||
* Chamado automaticamente ao recarregar a página (onRehydrateStorage)
|
* Chamado automaticamente ao recarregar a página (onRehydrateStorage)
|
||||||
*/
|
*/
|
||||||
hydrate: () => {
|
hydrate: () => {},
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'auth-storage',
|
name: 'auth-storage',
|
||||||
|
|
@ -45,4 +44,4 @@ export const useAuthStore = create<AuthState>()(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
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,
|
||||||
userName: data.userName,
|
userName: data.userName,
|
||||||
nome: data.nome,
|
nome: data.nome,
|
||||||
codigoFilial: data.codigoFilial,
|
codigoFilial: data.codigoFilial,
|
||||||
nomeFilial: data.nomeFilial,
|
nomeFilial: data.nomeFilial,
|
||||||
rca: data.rca,
|
rca: data.rca,
|
||||||
discountPercent: data.discountPercent,
|
discountPercent: data.discountPercent,
|
||||||
sectorId: data.sectorId,
|
sectorId: data.sectorId,
|
||||||
sectorManagerId: data.sectorManagerId,
|
sectorManagerId: data.sectorManagerId,
|
||||||
supervisorId: data.supervisorId,
|
supervisorId: data.supervisorId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import axios, {
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosResponse,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
import { TokenResponse } from '../interfaces/types';
|
import { TokenResponse } from '../interfaces/types';
|
||||||
|
|
||||||
const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL;
|
const AUTH_API_URL = process.env.NEXT_PUBLIC_AUTH_API_URL;
|
||||||
|
|
@ -62,7 +67,10 @@ export function handleTokenRefresh<T = unknown>(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.url?.includes('/auth/login') || request.url?.includes('/auth/refresh')) {
|
if (
|
||||||
|
request.url?.includes('/auth/login') ||
|
||||||
|
request.url?.includes('/auth/refresh')
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
import axios, {
|
||||||
import { getAccessToken, handleTokenRefresh } from '../../login/utils/tokenRefresh';
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
|
import {
|
||||||
|
getAccessToken,
|
||||||
|
handleTokenRefresh,
|
||||||
|
} from '../../login/utils/tokenRefresh';
|
||||||
import {
|
import {
|
||||||
OrderFilters,
|
OrderFilters,
|
||||||
orderApiParamsSchema,
|
orderApiParamsSchema,
|
||||||
|
|
@ -8,9 +15,18 @@ import {
|
||||||
storesResponseSchema,
|
storesResponseSchema,
|
||||||
customersResponseSchema,
|
customersResponseSchema,
|
||||||
sellersResponseSchema,
|
sellersResponseSchema,
|
||||||
unwrapApiData
|
unwrapApiData,
|
||||||
} from '../schemas/order.schema';
|
} from '../schemas/order.schema';
|
||||||
import { orderItemsResponseSchema, OrderItem } from '../schemas/order.item.schema';
|
import {
|
||||||
|
orderItemsResponseSchema,
|
||||||
|
OrderItem,
|
||||||
|
shipmentResponseSchema,
|
||||||
|
Shipment,
|
||||||
|
cargoMovementResponseSchema,
|
||||||
|
CargoMovement,
|
||||||
|
cuttingItemResponseSchema,
|
||||||
|
CuttingItem,
|
||||||
|
} from '../schemas/order.item.schema';
|
||||||
import { Store } from '../schemas/store.schema';
|
import { Store } from '../schemas/store.schema';
|
||||||
import { Seller } from '../schemas/seller.schema';
|
import { Seller } from '../schemas/seller.schema';
|
||||||
import { Order } from '../types';
|
import { Order } from '../types';
|
||||||
|
|
@ -28,7 +44,7 @@ export const ordersApi: AxiosInstance = axios.create({
|
||||||
/**
|
/**
|
||||||
* Adiciona o token de autenticação aos cabeçalhos da requisição.
|
* Adiciona o token de autenticação aos cabeçalhos da requisição.
|
||||||
* Executa apenas no ambiente do navegador (verifica a existência do objeto window).
|
* Executa apenas no ambiente do navegador (verifica a existência do objeto window).
|
||||||
*
|
*
|
||||||
* @param {InternalAxiosRequestConfig} config - A configuração da requisição Axios
|
* @param {InternalAxiosRequestConfig} config - A configuração da requisição Axios
|
||||||
* @returns {InternalAxiosRequestConfig} A configuração modificada com o cabeçalho Authorization
|
* @returns {InternalAxiosRequestConfig} A configuração modificada com o cabeçalho Authorization
|
||||||
*/
|
*/
|
||||||
|
|
@ -47,13 +63,15 @@ ordersApi.interceptors.request.use(addToken);
|
||||||
/**
|
/**
|
||||||
* Trata erros de resposta das requisições da API.
|
* Trata erros de resposta das requisições da API.
|
||||||
* Tenta atualizar o token de autenticação se a requisição falhar.
|
* Tenta atualizar o token de autenticação se a requisição falhar.
|
||||||
*
|
*
|
||||||
* @param {AxiosError} error - O objeto de erro do Axios
|
* @param {AxiosError} error - O objeto de erro do Axios
|
||||||
* @returns {Promise} Resultado da tentativa de atualização do token
|
* @returns {Promise} Resultado da tentativa de atualização do token
|
||||||
* @throws {AxiosError} Se não houver configuração de requisição original disponível
|
* @throws {AxiosError} Se não houver configuração de requisição original disponível
|
||||||
*/
|
*/
|
||||||
const handleResponseError = async (error: AxiosError) => {
|
const handleResponseError = async (error: AxiosError) => {
|
||||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
||||||
|
_retry?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
if (!originalRequest) {
|
if (!originalRequest) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -62,20 +80,25 @@ const handleResponseError = async (error: AxiosError) => {
|
||||||
return handleTokenRefresh(error, originalRequest, ordersApi);
|
return handleTokenRefresh(error, originalRequest, ordersApi);
|
||||||
};
|
};
|
||||||
|
|
||||||
ordersApi.interceptors.response.use((response) => response, handleResponseError);
|
ordersApi.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
handleResponseError
|
||||||
|
);
|
||||||
|
|
||||||
export const orderService = {
|
export const orderService = {
|
||||||
/**
|
/**
|
||||||
* Busca pedidos com base nos filtros fornecidos.
|
* Busca pedidos com base nos filtros fornecidos.
|
||||||
* Utiliza Zod para limpar e transformar os parâmetros automaticamente.
|
* Utiliza Zod para limpar e transformar os parâmetros automaticamente.
|
||||||
*
|
*
|
||||||
* @param {OrderFilters} filters - Critérios de filtro para buscar pedidos
|
* @param {OrderFilters} filters - Critérios de filtro para buscar pedidos
|
||||||
* @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 {
|
try {
|
||||||
const cleanParams = orderApiParamsSchema.parse(filters);
|
const cleanParams = orderApiParamsSchema.parse(filters);
|
||||||
const response = await ordersApi.get('/api/v1/orders/find', { params: cleanParams });
|
const response = await ordersApi.get('/api/v1/orders/find', {
|
||||||
|
params: cleanParams,
|
||||||
|
});
|
||||||
return unwrapApiData(response, ordersResponseSchema, []);
|
return unwrapApiData(response, ordersResponseSchema, []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao buscar pedidos:', error);
|
console.error('Erro ao buscar pedidos:', error);
|
||||||
|
|
@ -85,7 +108,7 @@ export const orderService = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca um pedido específico pelo seu ID.
|
* Busca um pedido específico pelo seu ID.
|
||||||
*
|
*
|
||||||
* @param {number} id - O identificador único do pedido
|
* @param {number} id - O identificador único do pedido
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
|
|
@ -101,7 +124,7 @@ export const orderService = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recupera todas as lojas disponíveis.
|
* Recupera todas as lojas disponíveis.
|
||||||
*
|
*
|
||||||
* @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[]> => {
|
||||||
|
|
@ -117,14 +140,18 @@ export const orderService = {
|
||||||
/**
|
/**
|
||||||
* Busca clientes por nome.
|
* Busca clientes por nome.
|
||||||
* Retorna array vazio se o termo de busca tiver menos de 2 caracteres.
|
* Retorna array vazio se o termo de busca tiver menos de 2 caracteres.
|
||||||
*
|
*
|
||||||
* @param {string} name - O nome do cliente a ser buscado (mínimo 2 caracteres)
|
* @param {string} name - O nome do cliente a ser buscado (mínimo 2 caracteres)
|
||||||
* @returns {Promise<Array<{id: number, name: string, estcob: string}>>} Array de clientes correspondentes com os campos id, name e estcob
|
* @returns {Promise<Array<{id: number, name: string, estcob: string}>>} Array de clientes correspondentes com os campos id, name e estcob
|
||||||
*/
|
*/
|
||||||
findCustomers: async (name: string): Promise<Array<{ id: number; name: string; estcob: string }>> => {
|
findCustomers: async (
|
||||||
|
name: string
|
||||||
|
): Promise<Array<{ id: number; name: string; estcob: string }>> => {
|
||||||
if (!name || name.trim().length < 2) return [];
|
if (!name || name.trim().length < 2) return [];
|
||||||
try {
|
try {
|
||||||
const response = await ordersApi.get(`/api/v1/clientes/${encodeURIComponent(name)}`);
|
const response = await ordersApi.get(
|
||||||
|
`/api/v1/clientes/${encodeURIComponent(name)}`
|
||||||
|
);
|
||||||
return unwrapApiData(response, customersResponseSchema, []);
|
return unwrapApiData(response, customersResponseSchema, []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao buscar clientes:', error);
|
console.error('Erro ao buscar clientes:', error);
|
||||||
|
|
@ -151,4 +178,49 @@ export const orderService = {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
findDelivery: async (
|
||||||
|
orderId: number,
|
||||||
|
includeCompletedDeliveries: boolean = true
|
||||||
|
): Promise<Shipment[]> => {
|
||||||
|
try {
|
||||||
|
const response = await ordersApi.get(
|
||||||
|
`/api/v1/orders/delivery/${orderId}`,
|
||||||
|
{
|
||||||
|
params: { includeCompletedDeliveries },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return unwrapApiData(response, shipmentResponseSchema, []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erro ao buscar entregas do pedido ${orderId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findCargoMovement: async (orderId: number): Promise<CargoMovement[]> => {
|
||||||
|
try {
|
||||||
|
const response = await ordersApi.get(
|
||||||
|
`/api/v1/orders/transfer/${orderId}`
|
||||||
|
);
|
||||||
|
return unwrapApiData(response, cargoMovementResponseSchema, []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erro ao buscar movimentação de carga do pedido ${orderId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findCuttingItems: async (orderId: number): Promise<CuttingItem[]> => {
|
||||||
|
try {
|
||||||
|
const response = await ordersApi.get(
|
||||||
|
`/api/v1/orders/cut-itens/${orderId}`
|
||||||
|
);
|
||||||
|
return unwrapApiData(response, cuttingItemResponseSchema, []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erro ao buscar itens de corte do pedido ${orderId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,50 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import TablePagination from '@mui/material/TablePagination';
|
import TablePagination from '@mui/material/TablePagination';
|
||||||
import { useGridApiContext, useGridSelector, gridPageCountSelector, gridPageSelector, gridPageSizeSelector, gridRowCountSelector } from '@mui/x-data-grid-premium';
|
import {
|
||||||
|
useGridApiContext,
|
||||||
|
useGridSelector,
|
||||||
|
gridPageCountSelector,
|
||||||
|
gridPageSelector,
|
||||||
|
gridPageSizeSelector,
|
||||||
|
gridRowCountSelector,
|
||||||
|
} from '@mui/x-data-grid-premium';
|
||||||
|
|
||||||
function CustomPagination() {
|
function CustomPagination() {
|
||||||
const apiRef = useGridApiContext();
|
const apiRef = useGridApiContext();
|
||||||
const pageCount = useGridSelector(apiRef, gridPageCountSelector);
|
const pageCount = useGridSelector(apiRef, gridPageCountSelector);
|
||||||
const page = useGridSelector(apiRef, gridPageSelector);
|
const page = useGridSelector(apiRef, gridPageSelector);
|
||||||
const pageSize = useGridSelector(apiRef, gridPageSizeSelector);
|
const pageSize = useGridSelector(apiRef, gridPageSizeSelector);
|
||||||
const rowCount = useGridSelector(apiRef, gridRowCountSelector);
|
const rowCount = useGridSelector(apiRef, gridRowCountSelector);
|
||||||
|
|
||||||
const labelDisplayedRows = ({ from, to, count }: { from: number; to: number; count: number }) => {
|
const labelDisplayedRows = ({
|
||||||
const currentPage = page + 1;
|
from,
|
||||||
const displayCount = count === -1 ? `mais de ${to}` : count;
|
to,
|
||||||
return `${from}–${to} de ${displayCount} | Página ${currentPage} de ${pageCount}`;
|
count,
|
||||||
};
|
}: {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
count: number;
|
||||||
|
}) => {
|
||||||
|
const currentPage = page + 1;
|
||||||
|
const displayCount = count === -1 ? `mais de ${to}` : count;
|
||||||
|
return `${from}–${to} de ${displayCount} | Página ${currentPage} de ${pageCount}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div"
|
component="div"
|
||||||
count={rowCount}
|
count={rowCount}
|
||||||
page={page}
|
page={page}
|
||||||
onPageChange={(event, newPage) => apiRef.current.setPage(newPage)}
|
onPageChange={(event, newPage) => apiRef.current.setPage(newPage)}
|
||||||
rowsPerPage={pageSize}
|
rowsPerPage={pageSize}
|
||||||
onRowsPerPageChange={(event) => apiRef.current.setPageSize(Number.parseInt(event.target.value, 10))}
|
onRowsPerPageChange={(event) =>
|
||||||
labelRowsPerPage="Pedidos por página:"
|
apiRef.current.setPageSize(Number.parseInt(event.target.value, 10))
|
||||||
labelDisplayedRows={labelDisplayedRows}
|
}
|
||||||
/>
|
labelRowsPerPage="Pedidos por página:"
|
||||||
);
|
labelDisplayedRows={labelDisplayedRows}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CustomPagination;
|
export default CustomPagination;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||||
import MoveToInboxIcon from '@mui/icons-material/MoveToInbox';
|
import MoveToInboxIcon from '@mui/icons-material/MoveToInbox';
|
||||||
import TvIcon from '@mui/icons-material/Tv';
|
import TvIcon from '@mui/icons-material/Tv';
|
||||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||||
import { TabPanel } from './TabPanel';
|
import { TabPanel } from '@/shared/components';
|
||||||
import { OrderItemsTable } from './tabs/OrderItemsTable';
|
import { OrderItemsTable } from './tabs/OrderItemsTable';
|
||||||
import { PreBoxPanel } from './tabs/PreBoxPanel';
|
import { PreBoxPanel } from './tabs/PreBoxPanel';
|
||||||
import { InformationPanel } from './tabs/InformationPanel';
|
import { InformationPanel } from './tabs/InformationPanel';
|
||||||
|
|
@ -25,142 +25,142 @@ import { TV8Panel } from './tabs/TV8Panel';
|
||||||
import { CashAdjustmentPanel } from './tabs/CashAdjustmentPanel';
|
import { CashAdjustmentPanel } from './tabs/CashAdjustmentPanel';
|
||||||
|
|
||||||
interface OrderDetailsTabsProps {
|
interface OrderDetailsTabsProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderDetailsTabs = ({ orderId }: OrderDetailsTabsProps) => {
|
export const OrderDetailsTabs = ({ orderId }: OrderDetailsTabsProps) => {
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||||
setActiveTab(newValue);
|
setActiveTab(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiTab-root': {
|
'& .MuiTab-root': {
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
minHeight: 48,
|
minHeight: 48,
|
||||||
color: 'text.secondary',
|
color: 'text.secondary',
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'& .MuiTabs-indicator': {
|
'& .MuiTabs-indicator': {
|
||||||
height: 3,
|
height: 3,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<ListAltIcon />}
|
icon={<ListAltIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Itens"
|
label="Itens"
|
||||||
id="order-tab-0"
|
id="order-tab-0"
|
||||||
aria-controls="order-tabpanel-0"
|
aria-controls="order-tabpanel-0"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<InventoryIcon />}
|
icon={<InventoryIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Pré-box"
|
label="Pré-box"
|
||||||
id="order-tab-1"
|
id="order-tab-1"
|
||||||
aria-controls="order-tabpanel-1"
|
aria-controls="order-tabpanel-1"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<InfoIcon />}
|
icon={<InfoIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Informações"
|
label="Informações"
|
||||||
id="order-tab-2"
|
id="order-tab-2"
|
||||||
aria-controls="order-tabpanel-2"
|
aria-controls="order-tabpanel-2"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<TimelineIcon />}
|
icon={<TimelineIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Timeline"
|
label="Timeline"
|
||||||
id="order-tab-3"
|
id="order-tab-3"
|
||||||
aria-controls="order-tabpanel-3"
|
aria-controls="order-tabpanel-3"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<ContentCutIcon />}
|
icon={<ContentCutIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Cortes"
|
label="Cortes"
|
||||||
id="order-tab-4"
|
id="order-tab-4"
|
||||||
aria-controls="order-tabpanel-4"
|
aria-controls="order-tabpanel-4"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<LocalShippingIcon />}
|
icon={<LocalShippingIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Entrega"
|
label="Entrega"
|
||||||
id="order-tab-5"
|
id="order-tab-5"
|
||||||
aria-controls="order-tabpanel-5"
|
aria-controls="order-tabpanel-5"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<MoveToInboxIcon />}
|
icon={<MoveToInboxIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Mov. Carga"
|
label="Mov. Carga"
|
||||||
id="order-tab-6"
|
id="order-tab-6"
|
||||||
aria-controls="order-tabpanel-6"
|
aria-controls="order-tabpanel-6"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<TvIcon />}
|
icon={<TvIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="TV8"
|
label="TV8"
|
||||||
id="order-tab-7"
|
id="order-tab-7"
|
||||||
aria-controls="order-tabpanel-7"
|
aria-controls="order-tabpanel-7"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<AccountBalanceWalletIcon />}
|
icon={<AccountBalanceWalletIcon />}
|
||||||
iconPosition="start"
|
iconPosition="start"
|
||||||
label="Acerta Caixa"
|
label="Acerta Caixa"
|
||||||
id="order-tab-8"
|
id="order-tab-8"
|
||||||
aria-controls="order-tabpanel-8"
|
aria-controls="order-tabpanel-8"
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={0}>
|
<TabPanel value={activeTab} index={0}>
|
||||||
<OrderItemsTable orderId={orderId} />
|
<OrderItemsTable orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={1}>
|
<TabPanel value={activeTab} index={1}>
|
||||||
<PreBoxPanel orderId={orderId} />
|
<PreBoxPanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={2}>
|
<TabPanel value={activeTab} index={2}>
|
||||||
<InformationPanel orderId={orderId} />
|
<InformationPanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={3}>
|
<TabPanel value={activeTab} index={3}>
|
||||||
<TimelinePanel orderId={orderId} />
|
<TimelinePanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={4}>
|
<TabPanel value={activeTab} index={4}>
|
||||||
<CuttingPanel orderId={orderId} />
|
<CuttingPanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={5}>
|
<TabPanel value={activeTab} index={5}>
|
||||||
<DeliveryPanel orderId={orderId} />
|
<DeliveryPanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={6}>
|
<TabPanel value={activeTab} index={6}>
|
||||||
<CargoMovementPanel orderId={orderId} />
|
<CargoMovementPanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={7}>
|
<TabPanel value={activeTab} index={7}>
|
||||||
<TV8Panel orderId={orderId} />
|
<TV8Panel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={activeTab} index={8}>
|
<TabPanel value={activeTab} index={8}>
|
||||||
<CashAdjustmentPanel orderId={orderId} />
|
<CashAdjustmentPanel orderId={orderId} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,164 +3,206 @@ import Typography from '@mui/material/Typography';
|
||||||
import { formatCurrency, formatNumber } from '../utils/orderFormatters';
|
import { formatCurrency, formatNumber } from '../utils/orderFormatters';
|
||||||
|
|
||||||
export const createOrderItemsColumns = (): GridColDef[] => [
|
export const createOrderItemsColumns = (): GridColDef[] => [
|
||||||
{
|
{
|
||||||
field: 'productId',
|
field: 'productId',
|
||||||
headerName: 'Cód. Produto',
|
headerName: 'Cód. Produto',
|
||||||
width: 110,
|
width: 110,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
headerAlign: 'right',
|
headerAlign: 'right',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}>
|
<Typography
|
||||||
{params.value}
|
variant="body2"
|
||||||
</Typography>
|
color="text.primary"
|
||||||
),
|
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
|
||||||
},
|
>
|
||||||
{
|
{params.value}
|
||||||
field: 'description',
|
</Typography>
|
||||||
headerName: 'Descrição',
|
),
|
||||||
width: 300,
|
},
|
||||||
minWidth: 250,
|
{
|
||||||
flex: 1,
|
field: 'description',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
headerName: 'Descrição',
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem' }} noWrap>
|
width: 300,
|
||||||
{params.value}
|
minWidth: 250,
|
||||||
</Typography>
|
flex: 1,
|
||||||
),
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
},
|
<Typography
|
||||||
{
|
variant="body2"
|
||||||
field: 'pacth',
|
color="text.primary"
|
||||||
headerName: 'Unidade',
|
sx={{ fontSize: '0.75rem' }}
|
||||||
width: 100,
|
noWrap
|
||||||
minWidth: 80,
|
>
|
||||||
headerAlign: 'center',
|
{params.value}
|
||||||
align: 'center',
|
</Typography>
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
),
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'center' }}>
|
},
|
||||||
{params.value || '-'}
|
{
|
||||||
</Typography>
|
field: 'pacth',
|
||||||
),
|
headerName: 'Unidade',
|
||||||
},
|
width: 100,
|
||||||
{
|
minWidth: 80,
|
||||||
field: 'color',
|
headerAlign: 'center',
|
||||||
headerName: 'Cor',
|
align: 'center',
|
||||||
width: 80,
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
minWidth: 70,
|
<Typography
|
||||||
headerAlign: 'center',
|
variant="body2"
|
||||||
align: 'center',
|
color="text.primary"
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
sx={{ fontSize: '0.75rem', textAlign: 'center' }}
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'center' }}>
|
>
|
||||||
{params.value || '-'}
|
{params.value || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'stockId',
|
field: 'color',
|
||||||
headerName: 'Cód. Estoque',
|
headerName: 'Cor',
|
||||||
width: 110,
|
width: 80,
|
||||||
minWidth: 100,
|
minWidth: 70,
|
||||||
headerAlign: 'right',
|
headerAlign: 'center',
|
||||||
align: 'right',
|
align: 'center',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
<Typography
|
||||||
{params.value}
|
variant="body2"
|
||||||
</Typography>
|
color="text.primary"
|
||||||
),
|
sx={{ fontSize: '0.75rem', textAlign: 'center' }}
|
||||||
},
|
>
|
||||||
{
|
{params.value || '-'}
|
||||||
field: 'quantity',
|
</Typography>
|
||||||
headerName: 'Qtd.',
|
),
|
||||||
width: 90,
|
},
|
||||||
minWidth: 80,
|
{
|
||||||
type: 'number',
|
field: 'stockId',
|
||||||
aggregable: true,
|
headerName: 'Cód. Estoque',
|
||||||
headerAlign: 'right',
|
width: 110,
|
||||||
align: 'right',
|
minWidth: 100,
|
||||||
valueFormatter: (value) => formatNumber(value as number),
|
headerAlign: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
align: 'right',
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}>
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
{formatNumber(params.value)}
|
<Typography
|
||||||
</Typography>
|
variant="body2"
|
||||||
),
|
color="text.primary"
|
||||||
},
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
{
|
>
|
||||||
field: 'salePrice',
|
{params.value}
|
||||||
headerName: 'Preço Unitário',
|
</Typography>
|
||||||
width: 130,
|
),
|
||||||
minWidth: 120,
|
},
|
||||||
type: 'number',
|
{
|
||||||
headerAlign: 'right',
|
field: 'quantity',
|
||||||
align: 'right',
|
headerName: 'Qtd.',
|
||||||
valueFormatter: (value) => formatCurrency(value as number),
|
width: 90,
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
minWidth: 80,
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
type: 'number',
|
||||||
{formatCurrency(params.value)}
|
aggregable: true,
|
||||||
</Typography>
|
headerAlign: 'right',
|
||||||
),
|
align: 'right',
|
||||||
},
|
valueFormatter: (value) => formatNumber(value as number),
|
||||||
{
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
field: 'total',
|
<Typography
|
||||||
headerName: 'Valor Total',
|
variant="body2"
|
||||||
width: 130,
|
color="text.primary"
|
||||||
minWidth: 120,
|
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
|
||||||
type: 'number',
|
>
|
||||||
aggregable: true,
|
{formatNumber(params.value)}
|
||||||
headerAlign: 'right',
|
</Typography>
|
||||||
align: 'right',
|
),
|
||||||
valueFormatter: (value) => formatCurrency(value as number),
|
},
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
{
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', fontWeight: 600, textAlign: 'right' }}>
|
field: 'salePrice',
|
||||||
{formatCurrency(params.value)}
|
headerName: 'Preço Unitário',
|
||||||
</Typography>
|
width: 130,
|
||||||
),
|
minWidth: 120,
|
||||||
},
|
type: 'number',
|
||||||
{
|
headerAlign: 'right',
|
||||||
field: 'deliveryType',
|
align: 'right',
|
||||||
headerName: 'Tipo Entrega',
|
valueFormatter: (value) => formatCurrency(value as number),
|
||||||
width: 140,
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
minWidth: 130,
|
<Typography
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
variant="body2"
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem' }} noWrap>
|
color="text.primary"
|
||||||
{params.value}
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
</Typography>
|
>
|
||||||
),
|
{formatCurrency(params.value)}
|
||||||
},
|
</Typography>
|
||||||
{
|
),
|
||||||
field: 'weight',
|
},
|
||||||
headerName: 'Peso (kg)',
|
{
|
||||||
width: 100,
|
field: 'total',
|
||||||
minWidth: 90,
|
headerName: 'Valor Total',
|
||||||
type: 'number',
|
width: 130,
|
||||||
aggregable: true,
|
minWidth: 120,
|
||||||
headerAlign: 'right',
|
type: 'number',
|
||||||
align: 'right',
|
aggregable: true,
|
||||||
valueFormatter: (value) => formatNumber(value as number),
|
headerAlign: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
align: 'right',
|
||||||
<Typography variant="body2" color="text.primary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
valueFormatter: (value) => formatCurrency(value as number),
|
||||||
{formatNumber(params.value)}
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
</Typography>
|
<Typography
|
||||||
),
|
variant="body2"
|
||||||
},
|
color="text.primary"
|
||||||
{
|
sx={{ fontSize: '0.75rem', fontWeight: 600, textAlign: 'right' }}
|
||||||
field: 'department',
|
>
|
||||||
headerName: 'Departamento',
|
{formatCurrency(params.value)}
|
||||||
width: 150,
|
</Typography>
|
||||||
minWidth: 140,
|
),
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
},
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
|
{
|
||||||
{params.value}
|
field: 'deliveryType',
|
||||||
</Typography>
|
headerName: 'Tipo Entrega',
|
||||||
),
|
width: 140,
|
||||||
},
|
minWidth: 130,
|
||||||
{
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
field: 'brand',
|
<Typography
|
||||||
headerName: 'Marca',
|
variant="body2"
|
||||||
width: 150,
|
color="text.primary"
|
||||||
minWidth: 140,
|
sx={{ fontSize: '0.75rem' }}
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
noWrap
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
|
>
|
||||||
{params.value}
|
{params.value}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'weight',
|
||||||
|
headerName: 'Peso (kg)',
|
||||||
|
width: 100,
|
||||||
|
minWidth: 90,
|
||||||
|
type: 'number',
|
||||||
|
aggregable: true,
|
||||||
|
headerAlign: 'right',
|
||||||
|
align: 'right',
|
||||||
|
valueFormatter: (value) => formatNumber(value as number),
|
||||||
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.primary"
|
||||||
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
|
>
|
||||||
|
{formatNumber(params.value)}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'department',
|
||||||
|
headerName: 'Departamento',
|
||||||
|
width: 150,
|
||||||
|
minWidth: 140,
|
||||||
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
|
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
|
||||||
|
{params.value}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'brand',
|
||||||
|
headerName: 'Marca',
|
||||||
|
width: 150,
|
||||||
|
minWidth: 140,
|
||||||
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
|
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
|
||||||
|
{params.value}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,381 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { DataGridPremium } from '@mui/x-data-grid-premium';
|
||||||
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import { LicenseInfo } from '@mui/x-license';
|
||||||
|
|
||||||
|
// Mock license for DataGrid Premium (for Storybook demo only)
|
||||||
|
LicenseInfo.setLicenseKey(
|
||||||
|
'e0d9bb8070ce0054c9d9ecb6e82cb58fTz0wLEU9MzI0NzIxNDQwMDAwMDAsUz1wcmVtaXVtLExNPXBlcnBldHVhbCxLVj0y'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock data que simula os dados normalizados
|
||||||
|
const mockOrders = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
orderId: 123456,
|
||||||
|
createDate: '2026-01-15T10:00:00',
|
||||||
|
customerName: 'Maria Silva',
|
||||||
|
status: 'Faturado',
|
||||||
|
orderType: 'Venda',
|
||||||
|
amount: 1250.5,
|
||||||
|
invoiceNumber: 'NF-001234',
|
||||||
|
sellerName: 'João Vendedor',
|
||||||
|
deliveryType: 'Entrega',
|
||||||
|
totalWeight: 15.5,
|
||||||
|
fatUserName: 'Admin',
|
||||||
|
deliveryLocal: 'São Paulo - SP',
|
||||||
|
masterDeliveryLocal: 'Região Sul',
|
||||||
|
deliveryPriority: 'Alta',
|
||||||
|
paymentName: 'Boleto',
|
||||||
|
partnerName: 'Parceiro ABC',
|
||||||
|
codusur2Name: 'Rep. Regional',
|
||||||
|
releaseUserName: 'Supervisor',
|
||||||
|
driver: 'Carlos Motorista',
|
||||||
|
carDescription: 'Sprinter',
|
||||||
|
carrier: 'Transportadora XYZ',
|
||||||
|
schedulerDelivery: '15/01/2026',
|
||||||
|
fatUserDescription: 'Faturamento',
|
||||||
|
emitenteNome: 'Empresa LTDA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
orderId: 123457,
|
||||||
|
createDate: '2026-01-14T14:30:00',
|
||||||
|
customerName: 'João Santos',
|
||||||
|
status: 'Pendente',
|
||||||
|
orderType: 'Venda',
|
||||||
|
amount: 3450.0,
|
||||||
|
invoiceNumber: '',
|
||||||
|
sellerName: 'Ana Vendedora',
|
||||||
|
deliveryType: 'Retirada',
|
||||||
|
totalWeight: 8.2,
|
||||||
|
fatUserName: '',
|
||||||
|
deliveryLocal: 'Rio de Janeiro - RJ',
|
||||||
|
masterDeliveryLocal: 'Região Sudeste',
|
||||||
|
deliveryPriority: 'Normal',
|
||||||
|
paymentName: 'Cartão de Crédito',
|
||||||
|
partnerName: '',
|
||||||
|
codusur2Name: '',
|
||||||
|
releaseUserName: '',
|
||||||
|
driver: '',
|
||||||
|
carDescription: '',
|
||||||
|
carrier: '',
|
||||||
|
schedulerDelivery: '',
|
||||||
|
fatUserDescription: '',
|
||||||
|
emitenteNome: 'Empresa LTDA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
orderId: 123458,
|
||||||
|
createDate: '2026-01-13T09:15:00',
|
||||||
|
customerName: 'Pedro Oliveira',
|
||||||
|
status: 'Entregue',
|
||||||
|
orderType: 'Venda',
|
||||||
|
amount: 890.75,
|
||||||
|
invoiceNumber: 'NF-001235',
|
||||||
|
sellerName: 'João Vendedor',
|
||||||
|
deliveryType: 'Entrega',
|
||||||
|
totalWeight: 5.0,
|
||||||
|
fatUserName: 'Admin',
|
||||||
|
deliveryLocal: 'Belo Horizonte - MG',
|
||||||
|
masterDeliveryLocal: 'Região Sudeste',
|
||||||
|
deliveryPriority: 'Baixa',
|
||||||
|
paymentName: 'PIX',
|
||||||
|
partnerName: 'Parceiro DEF',
|
||||||
|
codusur2Name: 'Rep. Nacional',
|
||||||
|
releaseUserName: 'Gerente',
|
||||||
|
driver: 'Paulo Motorista',
|
||||||
|
carDescription: 'Fiorino',
|
||||||
|
carrier: 'Transportadora ABC',
|
||||||
|
schedulerDelivery: '13/01/2026',
|
||||||
|
fatUserDescription: 'Faturamento Automático',
|
||||||
|
emitenteNome: 'Outra Empresa LTDA',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Colunas simplificadas para a demo
|
||||||
|
const demoColumns = [
|
||||||
|
{ field: 'orderId', headerName: 'Pedido', width: 100 },
|
||||||
|
{ field: 'createDate', headerName: 'Data Criação', width: 150 },
|
||||||
|
{ field: 'customerName', headerName: 'Cliente', width: 180 },
|
||||||
|
{ field: 'status', headerName: 'Status', width: 120 },
|
||||||
|
{ field: 'orderType', headerName: 'Tipo', width: 100 },
|
||||||
|
{
|
||||||
|
field: 'amount',
|
||||||
|
headerName: 'Valor',
|
||||||
|
width: 120,
|
||||||
|
valueFormatter: (value: number) =>
|
||||||
|
new Intl.NumberFormat('pt-BR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'BRL',
|
||||||
|
}).format(value),
|
||||||
|
},
|
||||||
|
{ field: 'invoiceNumber', headerName: 'Nota Fiscal', width: 130 },
|
||||||
|
{ field: 'sellerName', headerName: 'Vendedor', width: 150 },
|
||||||
|
{ field: 'deliveryType', headerName: 'Entrega', width: 100 },
|
||||||
|
{ field: 'deliveryLocal', headerName: 'Local Entrega', width: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Componente wrapper para o Storybook
|
||||||
|
const OrderTableDemo = ({
|
||||||
|
rows = mockOrders,
|
||||||
|
loading = false,
|
||||||
|
emptyState = false,
|
||||||
|
}: {
|
||||||
|
rows?: typeof mockOrders;
|
||||||
|
loading?: boolean;
|
||||||
|
emptyState?: boolean;
|
||||||
|
}) => {
|
||||||
|
const displayRows = emptyState ? [] : rows;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Box sx={{ width: '100%', p: 2 }}>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
boxShadow: 'none',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DataGridPremium
|
||||||
|
rows={displayRows}
|
||||||
|
columns={demoColumns}
|
||||||
|
loading={loading}
|
||||||
|
density="compact"
|
||||||
|
autoHeight={false}
|
||||||
|
initialState={{
|
||||||
|
pagination: {
|
||||||
|
paginationModel: {
|
||||||
|
pageSize: 10,
|
||||||
|
page: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sorting: {
|
||||||
|
sortModel: [{ field: 'createDate', sort: 'desc' }],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[10, 25, 50]}
|
||||||
|
paginationMode="client"
|
||||||
|
pagination
|
||||||
|
sx={{
|
||||||
|
height: 400,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
borderBottom: '2px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-row': {
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-cell': {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-footerContainer': {
|
||||||
|
borderTop: '2px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
localeText={{
|
||||||
|
noRowsLabel: 'Nenhum pedido encontrado.',
|
||||||
|
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta<typeof OrderTableDemo> = {
|
||||||
|
title: 'Features/Orders/OrderTable',
|
||||||
|
component: OrderTableDemo,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
## OrderTable
|
||||||
|
|
||||||
|
Tabela de pedidos com as seguintes funcionalidades:
|
||||||
|
|
||||||
|
- **Paginação**: Suporta 10, 25 ou 50 itens por página
|
||||||
|
- **Ordenação**: Por qualquer coluna clicando no header
|
||||||
|
- **Seleção**: Clique em uma linha para ver detalhes
|
||||||
|
- **Responsividade**: Adapta altura para mobile/desktop
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof OrderTableDemo>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado padrão da tabela com dados de pedidos.
|
||||||
|
*/
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
orderId: 1232236,
|
||||||
|
createDate: '2026-01-15T10:00:00',
|
||||||
|
customerName: 'Maria Silva',
|
||||||
|
status: 'Faturado',
|
||||||
|
orderType: 'Venda',
|
||||||
|
amount: 1250.5,
|
||||||
|
invoiceNumber: 'NF-001234',
|
||||||
|
sellerName: 'João Vendedor',
|
||||||
|
deliveryType: 'Entrega',
|
||||||
|
totalWeight: 15.5,
|
||||||
|
fatUserName: 'Admin',
|
||||||
|
deliveryLocal: 'São Paulo - SP',
|
||||||
|
masterDeliveryLocal: 'Região Sul',
|
||||||
|
deliveryPriority: 'Alta',
|
||||||
|
paymentName: 'Boleto',
|
||||||
|
partnerName: 'Parceiro ABC',
|
||||||
|
codusur2Name: 'Rep. Regional',
|
||||||
|
releaseUserName: 'Supervisor',
|
||||||
|
driver: 'Carlos Motorista',
|
||||||
|
carDescription: 'Sprinter',
|
||||||
|
carrier: 'Transportadora XYZ',
|
||||||
|
schedulerDelivery: '15/01/2026',
|
||||||
|
fatUserDescription: 'Faturamento',
|
||||||
|
emitenteNome: 'Empresa LTDA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
orderId: 123457,
|
||||||
|
createDate: '2026-01-14T14:30:00',
|
||||||
|
customerName: 'João Santos',
|
||||||
|
status: 'Pendente',
|
||||||
|
orderType: 'Venda',
|
||||||
|
amount: 3450,
|
||||||
|
invoiceNumber: '',
|
||||||
|
sellerName: 'Ana Vendedora',
|
||||||
|
deliveryType: 'Retirada',
|
||||||
|
totalWeight: 8.2,
|
||||||
|
fatUserName: '',
|
||||||
|
deliveryLocal: 'Rio de Janeiro - RJ',
|
||||||
|
masterDeliveryLocal: 'Região Sudeste',
|
||||||
|
deliveryPriority: 'Normal',
|
||||||
|
paymentName: 'Cartão de Crédito',
|
||||||
|
partnerName: '',
|
||||||
|
codusur2Name: '',
|
||||||
|
releaseUserName: '',
|
||||||
|
driver: '',
|
||||||
|
carDescription: '',
|
||||||
|
carrier: '',
|
||||||
|
schedulerDelivery: '',
|
||||||
|
fatUserDescription: '',
|
||||||
|
emitenteNome: 'Empresa LTDA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
orderId: 123458,
|
||||||
|
createDate: '2026-01-13T09:15:00',
|
||||||
|
customerName: 'Pedro Oliveira',
|
||||||
|
status: 'Entregue',
|
||||||
|
orderType: 'Venda',
|
||||||
|
amount: 890.75,
|
||||||
|
invoiceNumber: 'NF-001235',
|
||||||
|
sellerName: 'João Vendedor',
|
||||||
|
deliveryType: 'Entrega',
|
||||||
|
totalWeight: 5,
|
||||||
|
fatUserName: 'Admin',
|
||||||
|
deliveryLocal: 'Belo Horizonte - MG',
|
||||||
|
masterDeliveryLocal: 'Região Sudeste',
|
||||||
|
deliveryPriority: 'Baixa',
|
||||||
|
paymentName: 'PIX',
|
||||||
|
partnerName: 'Parceiro DEF',
|
||||||
|
codusur2Name: 'Rep. Nacional',
|
||||||
|
releaseUserName: 'Gerente',
|
||||||
|
driver: 'Paulo Motorista',
|
||||||
|
carDescription: 'Fiorino',
|
||||||
|
carrier: 'Transportadora ABC',
|
||||||
|
schedulerDelivery: '13/01/2026',
|
||||||
|
fatUserDescription: 'Faturamento Automático',
|
||||||
|
emitenteNome: 'Outra Empresa LTDA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
loading: false,
|
||||||
|
emptyState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado de carregamento enquanto busca pedidos da API.
|
||||||
|
*/
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
rows: [],
|
||||||
|
loading: true,
|
||||||
|
emptyState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado vazio quando não há pedidos para exibir.
|
||||||
|
*/
|
||||||
|
export const Empty: Story = {
|
||||||
|
args: {
|
||||||
|
rows: [],
|
||||||
|
loading: false,
|
||||||
|
emptyState: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tabela com muitos pedidos para demonstrar a paginação.
|
||||||
|
*/
|
||||||
|
export const ManyOrders: Story = {
|
||||||
|
args: {
|
||||||
|
rows: Array.from({ length: 50 }, (_, i) => ({
|
||||||
|
...mockOrders[i % 3],
|
||||||
|
id: String(i + 1),
|
||||||
|
orderId: 123456 + i,
|
||||||
|
customerName: `Cliente ${i + 1}`,
|
||||||
|
amount: Math.random() * 10000,
|
||||||
|
})),
|
||||||
|
loading: false,
|
||||||
|
emptyState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { SearchBar } from './SearchBar';
|
import { SearchBar } from './SearchBar';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
|
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import { DataGridPremium, GridCellSelectionModel } from '@mui/x-data-grid-premium';
|
import {
|
||||||
|
DataGridPremium,
|
||||||
|
GridCellSelectionModel,
|
||||||
|
} from '@mui/x-data-grid-premium';
|
||||||
import { useOrders } from '../hooks/useOrders';
|
import { useOrders } from '../hooks/useOrders';
|
||||||
|
import { useStores } from '../store/useStores';
|
||||||
import { createOrderColumns } from './OrderTableColumns';
|
import { createOrderColumns } from './OrderTableColumns';
|
||||||
import { calculateTableHeight } from '../utils/tableHelpers';
|
import { calculateTableHeight } from '../utils/tableHelpers';
|
||||||
import { normalizeOrder } from '../utils/orderNormalizer';
|
import { normalizeOrder } from '../utils/orderNormalizer';
|
||||||
|
|
@ -17,21 +21,41 @@ import { OrderDetailsTabs } from './OrderDetailsTabs';
|
||||||
|
|
||||||
export const OrderTable = () => {
|
export const OrderTable = () => {
|
||||||
const { data: orders, isLoading, error } = useOrders();
|
const { data: orders, isLoading, error } = useOrders();
|
||||||
const [cellSelectionModel, setCellSelectionModel] = useState<GridCellSelectionModel>({});
|
const { data: stores } = useStores();
|
||||||
|
const [cellSelectionModel, setCellSelectionModel] =
|
||||||
|
useState<GridCellSelectionModel>({});
|
||||||
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!orders || orders.length === 0) {
|
||||||
|
setSelectedOrderId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedOrderId((current) => {
|
||||||
|
if (!current) return null;
|
||||||
|
const exists = orders.some((order: Order) => order.orderId === current);
|
||||||
|
return exists ? current : null;
|
||||||
|
});
|
||||||
|
}, [orders]);
|
||||||
|
|
||||||
|
// Cria mapa de storeId -> storeName
|
||||||
|
const storesMap = useMemo(() => {
|
||||||
|
if (!stores) return new Map<string, string>();
|
||||||
|
return new Map(
|
||||||
|
stores.map((store) => [String(store.id), store.store || store.name || String(store.id)])
|
||||||
|
);
|
||||||
|
}, [stores]);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
if (!Array.isArray(orders) || orders.length === 0) return [];
|
if (!Array.isArray(orders) || orders.length === 0) return [];
|
||||||
|
|
||||||
return orders.map((order: Order, index: number) => normalizeOrder(order, index));
|
return orders.map((order: Order, index: number) =>
|
||||||
|
normalizeOrder(order, index)
|
||||||
|
);
|
||||||
}, [orders]);
|
}, [orders]);
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(() => createOrderColumns({ storesMap }), [storesMap]);
|
||||||
() => createOrderColumns(),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -47,11 +71,24 @@ export const OrderTable = () => {
|
||||||
|
|
||||||
const tableHeight = calculateTableHeight(rows.length, 10);
|
const tableHeight = calculateTableHeight(rows.length, 10);
|
||||||
|
|
||||||
|
const mobileTableHeight = calculateTableHeight(rows.length, 5, {
|
||||||
|
minHeight: 300,
|
||||||
|
rowHeight: 40,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
|
|
||||||
<Paper sx={{ mt: 3, boxShadow: 'none', border: 'none', backgroundColor: 'transparent', overflow: 'hidden' }}>
|
<Paper
|
||||||
|
sx={{
|
||||||
|
mt: { xs: 2, md: 3 },
|
||||||
|
boxShadow: 'none',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DataGridPremium
|
<DataGridPremium
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|
@ -73,7 +110,7 @@ export const OrderTable = () => {
|
||||||
sortModel: [{ field: 'createDate', sort: 'desc' }],
|
sortModel: [{ field: 'createDate', sort: 'desc' }],
|
||||||
},
|
},
|
||||||
pinnedColumns: {
|
pinnedColumns: {
|
||||||
left: ['orderId', 'customerName'],
|
/// left: ['orderId', 'customerName'],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
pageSizeOptions={[10, 25, 50]}
|
pageSizeOptions={[10, 25, 50]}
|
||||||
|
|
@ -88,7 +125,7 @@ export const OrderTable = () => {
|
||||||
params.row.orderId === selectedOrderId ? 'Mui-selected' : ''
|
params.row.orderId === selectedOrderId ? 'Mui-selected' : ''
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
height: tableHeight,
|
height: { xs: mobileTableHeight, md: tableHeight },
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||||
|
|
@ -253,7 +290,8 @@ export const OrderTable = () => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
localeText={{
|
localeText={{
|
||||||
noRowsLabel: 'Nenhum pedido encontrado para os filtros selecionados.',
|
noRowsLabel:
|
||||||
|
'Nenhum pedido encontrado para os filtros selecionados.',
|
||||||
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
|
noResultsOverlayLabel: 'Nenhum resultado encontrado.',
|
||||||
footerTotalRows: 'Total de registros:',
|
footerTotalRows: 'Total de registros:',
|
||||||
footerTotalVisibleRows: (visibleCount, totalCount) =>
|
footerTotalVisibleRows: (visibleCount, totalCount) =>
|
||||||
|
|
@ -263,11 +301,18 @@ export const OrderTable = () => {
|
||||||
? `${count.toLocaleString()} linha selecionada`
|
? `${count.toLocaleString()} linha selecionada`
|
||||||
: `${count.toLocaleString()} linhas selecionadas`,
|
: `${count.toLocaleString()} linhas selecionadas`,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
slotProps={{
|
slotProps={{
|
||||||
pagination: {
|
pagination: {
|
||||||
labelRowsPerPage: 'Pedidos por página:',
|
labelRowsPerPage: 'Pedidos por página:',
|
||||||
labelDisplayedRows: ({ from, to, count }: { from: number; to: number; count: number }) => {
|
labelDisplayedRows: ({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
count: number;
|
||||||
|
}) => {
|
||||||
const pageSize = to >= from ? to - from + 1 : 10;
|
const pageSize = to >= from ? to - from + 1 : 10;
|
||||||
const currentPage = Math.floor((from - 1) / pageSize) + 1;
|
const currentPage = Math.floor((from - 1) / pageSize) + 1;
|
||||||
const totalPages = Math.ceil(count / pageSize);
|
const totalPages = Math.ceil(count / pageSize);
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,25 @@ import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import { formatDate, formatDateTime, formatCurrency, formatNumber } from '../utils/orderFormatters';
|
import {
|
||||||
import { getStatusChipProps, getPriorityChipProps } from '../utils/tableHelpers';
|
formatDate,
|
||||||
|
formatDateTime,
|
||||||
|
formatCurrency,
|
||||||
|
formatNumber,
|
||||||
|
} from '../utils/orderFormatters';
|
||||||
|
import {
|
||||||
|
getStatusChipProps,
|
||||||
|
getPriorityChipProps,
|
||||||
|
} from '../utils/tableHelpers';
|
||||||
|
|
||||||
export const createOrderColumns = (): GridColDef[] => [
|
interface CreateOrderColumnsOptions {
|
||||||
|
storesMap?: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createOrderColumns = (options?: CreateOrderColumnsOptions): GridColDef[] => {
|
||||||
|
const storesMap = options?.storesMap;
|
||||||
|
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
field: 'orderId',
|
field: 'orderId',
|
||||||
headerName: 'Pedido',
|
headerName: 'Pedido',
|
||||||
|
|
@ -15,7 +30,10 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
headerAlign: 'right',
|
headerAlign: 'right',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
|
||||||
|
>
|
||||||
{params.value}
|
{params.value}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
|
|
@ -33,7 +51,11 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
{formatDate(params.value)}
|
{formatDate(params.value)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{dateTime && (
|
{dateTime && (
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.6875rem' }}>
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ fontSize: '0.6875rem' }}
|
||||||
|
>
|
||||||
{dateTime}
|
{dateTime}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
@ -58,15 +80,19 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
headerName: 'Filial',
|
headerName: 'Filial',
|
||||||
width: 200,
|
width: 200,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => {
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
|
const storeId = String(params.value);
|
||||||
{params.value}
|
const storeName = storesMap?.get(storeId) || storeId;
|
||||||
</Typography>
|
return (
|
||||||
),
|
<Typography variant="body2" sx={{ fontSize: '0.75rem' }} noWrap>
|
||||||
|
{storeName}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'store',
|
field: 'store',
|
||||||
headerName: 'Filial Faturamento',
|
headerName: 'Supervisor',
|
||||||
width: 200,
|
width: 200,
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
|
|
@ -122,7 +148,10 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
align: 'right',
|
align: 'right',
|
||||||
valueFormatter: (value) => formatCurrency(value as number),
|
valueFormatter: (value) => formatCurrency(value as number),
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontSize: '0.75rem', fontWeight: 500, textAlign: 'right' }}
|
||||||
|
>
|
||||||
{formatCurrency(params.value)}
|
{formatCurrency(params.value)}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
|
|
@ -135,7 +164,10 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
headerAlign: 'right',
|
headerAlign: 'right',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
|
>
|
||||||
{params.value && params.value !== '-' ? params.value : '-'}
|
{params.value && params.value !== '-' ? params.value : '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
|
|
@ -184,7 +216,10 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
align: 'right',
|
align: 'right',
|
||||||
valueFormatter: (value) => formatNumber(value as number),
|
valueFormatter: (value) => formatNumber(value as number),
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
|
>
|
||||||
{formatNumber(params.value)}
|
{formatNumber(params.value)}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
|
|
@ -208,7 +243,11 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
headerAlign: 'right',
|
headerAlign: 'right',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
|
>
|
||||||
{params.value || '-'}
|
{params.value || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
|
|
@ -290,10 +329,16 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
const timeStr = params.row.invoiceTime;
|
const timeStr = params.row.invoiceTime;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
{dateStr}
|
{dateStr}
|
||||||
{timeStr && (
|
{timeStr && (
|
||||||
<Box component="span" sx={{ color: 'text.secondary', fontSize: '0.6875rem', ml: 0.5 }}>
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ color: 'text.secondary', fontSize: '0.6875rem', ml: 0.5 }}
|
||||||
|
>
|
||||||
{timeStr}
|
{timeStr}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
@ -342,7 +387,11 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
headerAlign: 'right',
|
headerAlign: 'right',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', textAlign: 'right' }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ fontSize: '0.75rem', textAlign: 'right' }}
|
||||||
|
>
|
||||||
{params.value || '-'}
|
{params.value || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
),
|
),
|
||||||
|
|
@ -392,4 +441,4 @@ export const createOrderColumns = (): GridColDef[] => [
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import {
|
||||||
Paper,
|
Paper,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
CircularProgress,
|
||||||
|
Badge,
|
||||||
|
type AutocompleteRenderInputParams,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useStores } from '../store/useStores';
|
import { useStores } from '../store/useStores';
|
||||||
import { useCustomers } from '../hooks/useCustomers';
|
import { useCustomers } from '../hooks/useCustomers';
|
||||||
|
|
@ -30,7 +33,6 @@ import 'moment/locale/pt-br';
|
||||||
|
|
||||||
moment.locale('pt-br');
|
moment.locale('pt-br');
|
||||||
|
|
||||||
// Tipo para os filtros locais (não precisam da flag searchTriggered)
|
|
||||||
interface LocalFilters {
|
interface LocalFilters {
|
||||||
status: string | null;
|
status: string | null;
|
||||||
orderId: number | null;
|
orderId: number | null;
|
||||||
|
|
@ -44,7 +46,9 @@ interface LocalFilters {
|
||||||
sellerName: string | null;
|
sellerName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInitialLocalFilters = (urlFilters: any): LocalFilters => ({
|
const getInitialLocalFilters = (
|
||||||
|
urlFilters: Partial<LocalFilters>
|
||||||
|
): LocalFilters => ({
|
||||||
status: urlFilters.status ?? null,
|
status: urlFilters.status ?? null,
|
||||||
orderId: urlFilters.orderId ?? null,
|
orderId: urlFilters.orderId ?? null,
|
||||||
customerId: urlFilters.customerId ?? null,
|
customerId: urlFilters.customerId ?? null,
|
||||||
|
|
@ -60,7 +64,6 @@ const getInitialLocalFilters = (urlFilters: any): LocalFilters => ({
|
||||||
export const SearchBar = () => {
|
export const SearchBar = () => {
|
||||||
const [urlFilters, setUrlFilters] = useOrderFilters();
|
const [urlFilters, setUrlFilters] = useOrderFilters();
|
||||||
|
|
||||||
// Estado local para inputs (não dispara busca ao mudar)
|
|
||||||
const [localFilters, setLocalFilters] = useState<LocalFilters>(() =>
|
const [localFilters, setLocalFilters] = useState<LocalFilters>(() =>
|
||||||
getInitialLocalFilters(urlFilters)
|
getInitialLocalFilters(urlFilters)
|
||||||
);
|
);
|
||||||
|
|
@ -70,22 +73,22 @@ export const SearchBar = () => {
|
||||||
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
|
||||||
const customers = useCustomers(customerSearchTerm);
|
const customers = useCustomers(customerSearchTerm);
|
||||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [touchedFields, setTouchedFields] = useState<{
|
const [touchedFields, setTouchedFields] = useState<{
|
||||||
createDateIni?: boolean;
|
createDateIni?: boolean;
|
||||||
createDateEnd?: boolean;
|
createDateEnd?: boolean;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
// Sync local state com URL (para navegação Back/Forward)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalFilters(getInitialLocalFilters(urlFilters));
|
setLocalFilters(getInitialLocalFilters(urlFilters));
|
||||||
}, [urlFilters]);
|
}, [urlFilters]);
|
||||||
|
|
||||||
const updateLocalFilter = useCallback(<K extends keyof LocalFilters>(
|
const updateLocalFilter = useCallback(
|
||||||
key: K,
|
<K extends keyof LocalFilters>(key: K, value: LocalFilters[K]) => {
|
||||||
value: LocalFilters[K]
|
setLocalFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
) => {
|
},
|
||||||
setLocalFilters(prev => ({ ...prev, [key]: value }));
|
[]
|
||||||
}, []);
|
);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setTouchedFields({});
|
setTouchedFields({});
|
||||||
|
|
@ -110,7 +113,6 @@ export const SearchBar = () => {
|
||||||
customerName: null,
|
customerName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset local + URL
|
|
||||||
setLocalFilters(getInitialLocalFilters(resetState));
|
setLocalFilters(getInitialLocalFilters(resetState));
|
||||||
setUrlFilters(resetState);
|
setUrlFilters(resetState);
|
||||||
}, [setUrlFilters]);
|
}, [setUrlFilters]);
|
||||||
|
|
@ -129,42 +131,55 @@ export const SearchBar = () => {
|
||||||
|
|
||||||
const handleFilter = useCallback(() => {
|
const handleFilter = useCallback(() => {
|
||||||
if (!localFilters.createDateIni) {
|
if (!localFilters.createDateIni) {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
setTouchedFields((prev) => ({ ...prev, createDateIni: true }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateError = validateDates();
|
const dateError = validateDates();
|
||||||
if (dateError) {
|
if (dateError) {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
|
setTouchedFields((prev) => ({ ...prev, createDateEnd: true }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit local filters to URL (triggers search)
|
setIsSearching(true);
|
||||||
setUrlFilters({
|
setUrlFilters({
|
||||||
...localFilters,
|
...localFilters,
|
||||||
searchTriggered: true,
|
searchTriggered: true,
|
||||||
});
|
});
|
||||||
|
// Reset loading state after a short delay
|
||||||
|
setTimeout(() => setIsSearching(false), 500);
|
||||||
}, [localFilters, setUrlFilters, validateDates]);
|
}, [localFilters, setUrlFilters, validateDates]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback(
|
||||||
if (e.key === 'Enter') {
|
(e: React.KeyboardEvent) => {
|
||||||
const isValid = !!localFilters.createDateIni;
|
if (e.key === 'Enter') {
|
||||||
const dateErr = validateDates();
|
const isValid = !!localFilters.createDateIni;
|
||||||
if (isValid && !dateErr) {
|
const dateErr = validateDates();
|
||||||
handleFilter();
|
if (isValid && !dateErr) {
|
||||||
|
handleFilter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, [localFilters.createDateIni, validateDates, handleFilter]);
|
[localFilters.createDateIni, validateDates, handleFilter]
|
||||||
|
);
|
||||||
|
|
||||||
const isDateValid = !!localFilters.createDateIni;
|
const isDateValid = !!localFilters.createDateIni;
|
||||||
const dateError = validateDates();
|
const dateError = validateDates();
|
||||||
const showDateIniError = touchedFields.createDateIni && !localFilters.createDateIni;
|
const showDateIniError =
|
||||||
|
touchedFields.createDateIni && !localFilters.createDateIni;
|
||||||
const showDateEndError = touchedFields.createDateEnd && dateError;
|
const showDateEndError = touchedFields.createDateEnd && dateError;
|
||||||
|
|
||||||
|
// Contador de filtros avançados ativos
|
||||||
|
const advancedFiltersCount = [
|
||||||
|
localFilters.store?.length,
|
||||||
|
localFilters.stockId?.length,
|
||||||
|
localFilters.sellerId,
|
||||||
|
].filter(Boolean).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
p: 3,
|
p: { xs: 2, md: 3 },
|
||||||
mb: 2,
|
mb: 2,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
|
|
@ -173,8 +188,7 @@ export const SearchBar = () => {
|
||||||
elevation={0}
|
elevation={0}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<Grid container spacing={2} alignItems="flex-end">
|
<Grid container spacing={{ xs: 1.5, md: 2 }} alignItems="flex-end">
|
||||||
|
|
||||||
{/* --- Primary Filters (Always Visible) --- */}
|
{/* --- Primary Filters (Always Visible) --- */}
|
||||||
|
|
||||||
{/* Campo de Texto Simples (Nº Pedido) */}
|
{/* Campo de Texto Simples (Nº Pedido) */}
|
||||||
|
|
@ -222,9 +236,11 @@ export const SearchBar = () => {
|
||||||
options={customers.options}
|
options={customers.options}
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
value={customers.options.find(option =>
|
value={
|
||||||
localFilters.customerId === option.customer?.id
|
customers.options.find(
|
||||||
) || null}
|
(option) => localFilters.customerId === option.customer?.id
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
updateLocalFilter('customerName', null);
|
updateLocalFilter('customerName', null);
|
||||||
|
|
@ -234,7 +250,10 @@ export const SearchBar = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalFilter('customerId', newValue.customer?.id || null);
|
updateLocalFilter('customerId', newValue.customer?.id || null);
|
||||||
updateLocalFilter('customerName', newValue.customer?.name || null);
|
updateLocalFilter(
|
||||||
|
'customerName',
|
||||||
|
newValue.customer?.name || null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={(_, newInputValue, reason) => {
|
onInputChange={(_, newInputValue, reason) => {
|
||||||
if (reason === 'clear') {
|
if (reason === 'clear') {
|
||||||
|
|
@ -254,14 +273,18 @@ export const SearchBar = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={customers.isLoading}
|
loading={customers.isLoading}
|
||||||
renderInput={(params: Readonly<any>) => (
|
renderInput={(params: AutocompleteRenderInputParams) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Cliente"
|
label="Cliente"
|
||||||
placeholder="Digite para buscar..."
|
placeholder="Digite para buscar..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
noOptionsText={customerSearchTerm.length < 2 ? 'Digite pelo menos 2 caracteres' : 'Nenhum cliente encontrado'}
|
noOptionsText={
|
||||||
|
customerSearchTerm.length < 2
|
||||||
|
? 'Digite pelo menos 2 caracteres'
|
||||||
|
: 'Nenhum cliente encontrado'
|
||||||
|
}
|
||||||
loadingText="Buscando clientes..."
|
loadingText="Buscando clientes..."
|
||||||
filterOptions={(x) => x}
|
filterOptions={(x) => x}
|
||||||
clearOnBlur={false}
|
clearOnBlur={false}
|
||||||
|
|
@ -272,54 +295,95 @@ export const SearchBar = () => {
|
||||||
|
|
||||||
{/* Campos de Data */}
|
{/* Campos de Data */}
|
||||||
<Grid size={{ xs: 12, sm: 12, md: 4 }}>
|
<Grid size={{ xs: 12, sm: 12, md: 4 }}>
|
||||||
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="pt-br">
|
<LocalizationProvider
|
||||||
<Box display="flex" gap={2}>
|
dateAdapter={AdapterMoment}
|
||||||
|
adapterLocale="pt-br"
|
||||||
|
>
|
||||||
|
<Box display="flex" gap={{ xs: 1.5, md: 2 }} flexDirection={{ xs: 'column', sm: 'row' }}>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Data Inicial"
|
label="Data Inicial"
|
||||||
value={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : null}
|
value={
|
||||||
|
localFilters.createDateIni
|
||||||
|
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
|
||||||
|
: null
|
||||||
|
}
|
||||||
onChange={(date: moment.Moment | null) => {
|
onChange={(date: moment.Moment | null) => {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateIni: true }));
|
setTouchedFields((prev) => ({
|
||||||
updateLocalFilter('createDateIni', date ? date.format('YYYY-MM-DD') : null);
|
...prev,
|
||||||
|
createDateIni: true,
|
||||||
|
}));
|
||||||
|
updateLocalFilter(
|
||||||
|
'createDateIni',
|
||||||
|
date ? date.format('YYYY-MM-DD') : null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
format="DD/MM/YYYY"
|
format="DD/MM/YYYY"
|
||||||
maxDate={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : undefined}
|
maxDate={
|
||||||
|
localFilters.createDateEnd
|
||||||
|
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
required: true,
|
required: true,
|
||||||
error: showDateIniError,
|
error: showDateIniError,
|
||||||
helperText: showDateIniError ? 'Data inicial é obrigatória' : '',
|
helperText: showDateIniError
|
||||||
onBlur: () => setTouchedFields(prev => ({ ...prev, createDateIni: true })),
|
? 'Data inicial é obrigatória'
|
||||||
|
: '',
|
||||||
|
onBlur: () =>
|
||||||
|
setTouchedFields((prev) => ({
|
||||||
|
...prev,
|
||||||
|
createDateIni: true,
|
||||||
|
})),
|
||||||
inputProps: {
|
inputProps: {
|
||||||
'aria-required': true,
|
'aria-required': true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Data Final"
|
label="Data Final (opcional)"
|
||||||
value={localFilters.createDateEnd ? moment(localFilters.createDateEnd, 'YYYY-MM-DD') : null}
|
value={
|
||||||
|
localFilters.createDateEnd
|
||||||
|
? moment(localFilters.createDateEnd, 'YYYY-MM-DD')
|
||||||
|
: null
|
||||||
|
}
|
||||||
onChange={(date: moment.Moment | null) => {
|
onChange={(date: moment.Moment | null) => {
|
||||||
setTouchedFields(prev => ({ ...prev, createDateEnd: true }));
|
setTouchedFields((prev) => ({
|
||||||
updateLocalFilter('createDateEnd', date ? date.format('YYYY-MM-DD') : null);
|
...prev,
|
||||||
|
createDateEnd: true,
|
||||||
|
}));
|
||||||
|
updateLocalFilter(
|
||||||
|
'createDateEnd',
|
||||||
|
date ? date.format('YYYY-MM-DD') : null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
format="DD/MM/YYYY"
|
format="DD/MM/YYYY"
|
||||||
minDate={localFilters.createDateIni ? moment(localFilters.createDateIni, 'YYYY-MM-DD') : undefined}
|
minDate={
|
||||||
|
localFilters.createDateIni
|
||||||
|
? moment(localFilters.createDateIni, 'YYYY-MM-DD')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
error: !!showDateEndError,
|
error: !!showDateEndError,
|
||||||
helperText: showDateEndError || '',
|
helperText: showDateEndError || '',
|
||||||
onBlur: () => setTouchedFields(prev => ({ ...prev, createDateEnd: true })),
|
onBlur: () =>
|
||||||
|
setTouchedFields((prev) => ({
|
||||||
|
...prev,
|
||||||
|
createDateEnd: true,
|
||||||
|
})),
|
||||||
inputProps: {
|
inputProps: {
|
||||||
placeholder: 'Opcional',
|
placeholder: 'Opcional',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -328,28 +392,41 @@ export const SearchBar = () => {
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Botões de Ação */}
|
{/* Botões de Ação */}
|
||||||
<Grid size={{ xs: 12, sm: 12, md: 1.5 }} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<Grid
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
size={{ xs: 12, sm: 12, md: 1.5 }}
|
||||||
|
sx={{ display: 'flex', justifyContent: { xs: 'stretch', sm: 'flex-end' } }}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, width: { xs: '100%', sm: 'auto' } }}>
|
||||||
<Tooltip title="Limpar filtros" arrow>
|
<Tooltip title="Limpar filtros" arrow>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
|
aria-label="Limpar filtros"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 40,
|
minWidth: { xs: 48, md: 40 },
|
||||||
|
minHeight: 44,
|
||||||
px: 1.5,
|
px: 1.5,
|
||||||
|
flex: { xs: 1, sm: 'none' },
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: 'action.hover',
|
bgcolor: 'action.hover',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResetIcon />
|
<ResetIcon />
|
||||||
|
<Box component="span" sx={{ display: { xs: 'none', md: 'inline' }, ml: 0.5 }}>
|
||||||
|
Limpar
|
||||||
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={isDateValid ? 'Buscar pedidos' : 'Preencha a data inicial para buscar'}
|
title={
|
||||||
|
isDateValid
|
||||||
|
? 'Buscar pedidos'
|
||||||
|
: 'Preencha a data inicial para buscar'
|
||||||
|
}
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -357,16 +434,28 @@ export const SearchBar = () => {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleFilter}
|
onClick={handleFilter}
|
||||||
disabled={!isDateValid || !!dateError}
|
disabled={!isDateValid || !!dateError || isSearching}
|
||||||
|
aria-label="Buscar pedidos"
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 40,
|
minWidth: { xs: 48, md: 40 },
|
||||||
|
minHeight: 44,
|
||||||
px: 1.5,
|
px: 1.5,
|
||||||
|
flex: { xs: 2, sm: 'none' },
|
||||||
'&:disabled': {
|
'&:disabled': {
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
{isSearching ? (
|
||||||
|
<CircularProgress size={20} color="inherit" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SearchIcon />
|
||||||
|
<Box component="span" sx={{ display: { xs: 'none', md: 'inline' }, ml: 0.5 }}>
|
||||||
|
Buscar
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -376,14 +465,23 @@ export const SearchBar = () => {
|
||||||
{/* --- Advanced Filters (Collapsible) --- */}
|
{/* --- Advanced Filters (Collapsible) --- */}
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
||||||
<Button
|
<Badge
|
||||||
size="small"
|
badgeContent={advancedFiltersCount}
|
||||||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
color="primary"
|
||||||
endIcon={showAdvancedFilters ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
invisible={advancedFiltersCount === 0}
|
||||||
sx={{ textTransform: 'none', color: 'text.secondary' }}
|
|
||||||
>
|
>
|
||||||
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
|
<Button
|
||||||
</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' }}
|
||||||
|
>
|
||||||
|
{showAdvancedFilters ? 'Menos filtros' : 'Mais filtros'}
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
<Collapse in={showAdvancedFilters}>
|
<Collapse in={showAdvancedFilters}>
|
||||||
<Grid container spacing={2} sx={{ pt: 2 }}>
|
<Grid container spacing={2} sx={{ pt: 2 }}>
|
||||||
|
|
@ -394,19 +492,28 @@ export const SearchBar = () => {
|
||||||
size="small"
|
size="small"
|
||||||
options={stores.options}
|
options={stores.options}
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) =>
|
||||||
value={stores.options.filter(option =>
|
option.id === value.id
|
||||||
|
}
|
||||||
|
value={stores.options.filter((option) =>
|
||||||
localFilters.store?.includes(option.value)
|
localFilters.store?.includes(option.value)
|
||||||
)}
|
)}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
updateLocalFilter('store', newValue.map(option => option.value));
|
updateLocalFilter(
|
||||||
|
'store',
|
||||||
|
newValue.map((option) => option.value)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
loading={stores.isLoading}
|
loading={stores.isLoading}
|
||||||
renderInput={(params: Readonly<any>) => (
|
renderInput={(params: AutocompleteRenderInputParams) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Filiais"
|
label="Filiais"
|
||||||
placeholder={localFilters.store?.length ? `${localFilters.store.length} selecionadas` : 'Selecione'}
|
placeholder={
|
||||||
|
localFilters.store?.length
|
||||||
|
? `${localFilters.store.length} selecionadas`
|
||||||
|
: 'Selecione'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -419,19 +526,28 @@ export const SearchBar = () => {
|
||||||
size="small"
|
size="small"
|
||||||
options={stores.options}
|
options={stores.options}
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) =>
|
||||||
value={stores.options.filter(option =>
|
option.id === value.id
|
||||||
|
}
|
||||||
|
value={stores.options.filter((option) =>
|
||||||
localFilters.stockId?.includes(option.value)
|
localFilters.stockId?.includes(option.value)
|
||||||
)}
|
)}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
updateLocalFilter('stockId', newValue.map(option => option.value));
|
updateLocalFilter(
|
||||||
|
'stockId',
|
||||||
|
newValue.map((option) => option.value)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
loading={stores.isLoading}
|
loading={stores.isLoading}
|
||||||
renderInput={(params: Readonly<any>) => (
|
renderInput={(params: AutocompleteRenderInputParams) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...params}
|
{...params}
|
||||||
label="Filial de Estoque"
|
label="Filial de Estoque"
|
||||||
placeholder={localFilters.stockId?.length ? `${localFilters.stockId.length} selecionadas` : 'Selecione'}
|
placeholder={
|
||||||
|
localFilters.stockId?.length
|
||||||
|
? `${localFilters.stockId.length} selecionadas`
|
||||||
|
: 'Selecione'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -443,13 +559,24 @@ export const SearchBar = () => {
|
||||||
size="small"
|
size="small"
|
||||||
options={sellers.options}
|
options={sellers.options}
|
||||||
getOptionLabel={(option) => option.label}
|
getOptionLabel={(option) => option.label}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) =>
|
||||||
value={sellers.options.find(option =>
|
option.id === value.id
|
||||||
localFilters.sellerId === option.seller.id.toString()
|
}
|
||||||
) || null}
|
value={
|
||||||
|
sellers.options.find(
|
||||||
|
(option) =>
|
||||||
|
localFilters.sellerId === option.seller.id.toString()
|
||||||
|
) || null
|
||||||
|
}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
updateLocalFilter('sellerId', newValue?.seller.id.toString() || null);
|
updateLocalFilter(
|
||||||
updateLocalFilter('sellerName', newValue?.seller.name || null);
|
'sellerId',
|
||||||
|
newValue?.seller.id.toString() || null
|
||||||
|
);
|
||||||
|
updateLocalFilter(
|
||||||
|
'sellerName',
|
||||||
|
newValue?.seller.name || null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
loading={sellers.isLoading}
|
loading={sellers.isLoading}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
|
|
@ -472,8 +599,7 @@ export const SearchBar = () => {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,20 @@ import { ReactNode } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
index: number;
|
index: number;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabPanel = ({ children, value, index }: TabPanelProps) => {
|
export const TabPanel = ({ children, value, index }: TabPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
hidden={value !== index}
|
hidden={value !== index}
|
||||||
id={`order-tabpanel-${index}`}
|
id={`order-tabpanel-${index}`}
|
||||||
aria-labelledby={`order-tab-${index}`}
|
aria-labelledby={`order-tab-${index}`}
|
||||||
>
|
>
|
||||||
{value === index && (
|
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
|
||||||
<Box sx={{ py: 3 }}>
|
</div>
|
||||||
{children}
|
);
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,63 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import { DataGridPremium } from '@mui/x-data-grid-premium';
|
||||||
|
import { useCargoMovement } from '../../hooks/useCargoMovement';
|
||||||
|
import { createCargoMovementColumns } from './CargoMovementPanelColumns';
|
||||||
|
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
|
||||||
|
|
||||||
interface CargoMovementPanelProps {
|
interface CargoMovementPanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CargoMovementPanel = ({ orderId }: CargoMovementPanelProps) => {
|
export const CargoMovementPanel = ({ orderId }: CargoMovementPanelProps) => {
|
||||||
|
const { data: movements, isLoading, error } = useCargoMovement(orderId);
|
||||||
|
|
||||||
|
const columns = useMemo(() => createCargoMovementColumns(), []);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (!movements || movements.length === 0) return [];
|
||||||
|
return movements.map((movement, index) => ({
|
||||||
|
id: `${movement.transactionId}-${index}`,
|
||||||
|
...movement,
|
||||||
|
}));
|
||||||
|
}, [movements]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<CircularProgress size={30} />
|
||||||
<Typography variant="body2">
|
</Box>
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { GridColDef } from '@mui/x-data-grid-premium';
|
||||||
|
import { formatDate } from '../../utils/orderFormatters';
|
||||||
|
|
||||||
|
export const createCargoMovementColumns = (): GridColDef[] => [
|
||||||
|
{
|
||||||
|
field: 'transactionId',
|
||||||
|
headerName: ' Transação',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Identificador da transação',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transferDate',
|
||||||
|
headerName: 'Data Transf.',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Data da transferência',
|
||||||
|
valueFormatter: (value) => formatDate(value as string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'invoiceId',
|
||||||
|
headerName: 'Nota Fiscal',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Número da nota fiscal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'oldShipment',
|
||||||
|
headerName: 'Carreg anterior',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Código do carregamento anterior',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'newShipment',
|
||||||
|
headerName: 'Carreg atual',
|
||||||
|
width: 110,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Código do carregamento atual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transferText',
|
||||||
|
headerName: 'Motivo',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Descrição da transferência',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cause',
|
||||||
|
headerName: 'Causa',
|
||||||
|
minWidth: 180,
|
||||||
|
flex: 1.2,
|
||||||
|
description: 'Causa da movimentação',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'userName',
|
||||||
|
headerName: 'Usuário',
|
||||||
|
minWidth: 180,
|
||||||
|
flex: 1.2,
|
||||||
|
description: 'Nome do usuário responsável',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'program',
|
||||||
|
headerName: 'Programa',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Programa utilizado',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -5,17 +5,17 @@ import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
interface CashAdjustmentPanelProps {
|
interface CashAdjustmentPanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CashAdjustmentPanel = ({ orderId }: CashAdjustmentPanelProps) => {
|
export const CashAdjustmentPanel = ({ orderId }: CashAdjustmentPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
Funcionalidade em desenvolvimento para o pedido {orderId}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,63 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import { DataGridPremium } from '@mui/x-data-grid-premium';
|
||||||
|
import { useCuttingItems } from '../../hooks/useCuttingItems';
|
||||||
|
import { createCuttingPanelColumns } from './CuttingPanelColumns';
|
||||||
|
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
|
||||||
|
|
||||||
interface CuttingPanelProps {
|
interface CuttingPanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CuttingPanel = ({ orderId }: CuttingPanelProps) => {
|
export const CuttingPanel = ({ orderId }: CuttingPanelProps) => {
|
||||||
|
const { data: items, isLoading, error } = useCuttingItems(orderId);
|
||||||
|
|
||||||
|
const columns = useMemo(() => createCuttingPanelColumns(), []);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (!items || items.length === 0) return [];
|
||||||
|
return items.map((item) => ({
|
||||||
|
id: item.productId,
|
||||||
|
...item,
|
||||||
|
}));
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<CircularProgress size={30} />
|
||||||
<Typography variant="body2">
|
</Box>
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { GridColDef } from '@mui/x-data-grid-premium';
|
||||||
|
|
||||||
|
export const createCuttingPanelColumns = (): GridColDef[] => [
|
||||||
|
{
|
||||||
|
field: 'productId',
|
||||||
|
headerName: 'Código',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Código do produto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
headerName: 'Descrição',
|
||||||
|
minWidth: 250,
|
||||||
|
flex: 2,
|
||||||
|
description: 'Descrição do produto',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pacth',
|
||||||
|
headerName: 'Unidade',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Unidade de medida',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stockId',
|
||||||
|
headerName: 'Estoque',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Código do estoque',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'saleQuantity',
|
||||||
|
headerName: 'Qtd Vendida',
|
||||||
|
width: 100,
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right',
|
||||||
|
description: 'Quantidade vendida',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cutQuantity',
|
||||||
|
headerName: 'Qtd Cortada',
|
||||||
|
width: 100,
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right',
|
||||||
|
description: 'Quantidade cortada',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'separedQuantity',
|
||||||
|
headerName: 'Qtd Separada',
|
||||||
|
width: 110,
|
||||||
|
align: 'right',
|
||||||
|
headerAlign: 'right',
|
||||||
|
description: 'Quantidade separada',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,21 +1,63 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import { DataGridPremium } from '@mui/x-data-grid-premium';
|
||||||
|
import { useDelivery } from '../../hooks/useDelivery';
|
||||||
|
import { createDeliveryPanelColumns } from './DeliveryPanelColumns';
|
||||||
|
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
|
||||||
|
|
||||||
interface DeliveryPanelProps {
|
interface DeliveryPanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeliveryPanel = ({ orderId }: DeliveryPanelProps) => {
|
export const DeliveryPanel = ({ orderId }: DeliveryPanelProps) => {
|
||||||
|
const { data: deliveries, isLoading, error } = useDelivery(orderId);
|
||||||
|
|
||||||
|
const columns = useMemo(() => createDeliveryPanelColumns(), []);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (!deliveries || deliveries.length === 0) return [];
|
||||||
|
return deliveries.map((delivery) => ({
|
||||||
|
id: delivery.shippimentId,
|
||||||
|
...delivery,
|
||||||
|
}));
|
||||||
|
}, [deliveries]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<CircularProgress size={30} />
|
||||||
<Typography variant="body2">
|
</Box>
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
|
||||||
</Typography>
|
|
||||||
</Alert>
|
|
||||||
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { GridColDef } from '@mui/x-data-grid-premium';
|
||||||
|
import { formatDate } from '../../utils/orderFormatters';
|
||||||
|
|
||||||
|
export const createDeliveryPanelColumns = (): GridColDef[] => [
|
||||||
|
{
|
||||||
|
field: 'shippimentId',
|
||||||
|
headerName: 'Código da entrega',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Identificador da entrega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'placeName',
|
||||||
|
headerName: 'Praça',
|
||||||
|
minWidth: 150,
|
||||||
|
flex: 1,
|
||||||
|
description: 'Nome do local de entrega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'street',
|
||||||
|
headerName: 'Rua',
|
||||||
|
minWidth: 180,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Rua do endereço de entrega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'addressNumber',
|
||||||
|
headerName: 'Nº',
|
||||||
|
width: 60,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Número do endereço',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'bairro',
|
||||||
|
headerName: 'Bairro',
|
||||||
|
minWidth: 120,
|
||||||
|
flex: 1,
|
||||||
|
description: 'Bairro do endereço de entrega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'city',
|
||||||
|
headerName: 'Cidade',
|
||||||
|
minWidth: 120,
|
||||||
|
flex: 1,
|
||||||
|
description: 'Cidade de entrega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'state',
|
||||||
|
headerName: 'UF',
|
||||||
|
width: 50,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Estado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cep',
|
||||||
|
headerName: 'CEP',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'CEP do endereço',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'shippimentDate',
|
||||||
|
headerName: 'Data Entrega',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Data programada para entrega',
|
||||||
|
valueFormatter: (value) => formatDate(value as string),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'driver',
|
||||||
|
headerName: 'Motorista',
|
||||||
|
minWidth: 120,
|
||||||
|
flex: 1,
|
||||||
|
description: 'Nome do motorista',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'car',
|
||||||
|
headerName: 'Veículo',
|
||||||
|
minWidth: 100,
|
||||||
|
flex: 0.8,
|
||||||
|
description: 'Veículo utilizado na entrega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'separatorName',
|
||||||
|
headerName: 'Separador',
|
||||||
|
minWidth: 100,
|
||||||
|
flex: 0.8,
|
||||||
|
description: 'Nome do separador',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'closeDate',
|
||||||
|
headerName: 'Data Fechamento',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
headerAlign: 'center',
|
||||||
|
description: 'Data de fechamento da entrega',
|
||||||
|
valueFormatter: (value) => (value ? formatDate(value as string) : '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'commentOrder1',
|
||||||
|
headerName: 'Obs. Pedido 1',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Observação do pedido 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'commentOrder2',
|
||||||
|
headerName: 'Obs. Pedido 2',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Observação do pedido 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'commentDelivery1',
|
||||||
|
headerName: 'Obs. Entrega 1',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Observação da entrega 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'commentDelivery2',
|
||||||
|
headerName: 'Obs. Entrega 2',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Observação da entrega 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'commentDelivery3',
|
||||||
|
headerName: 'Obs. Entrega 3',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Observação da entrega 3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'commentDelivery4',
|
||||||
|
headerName: 'Obs. Entrega 4',
|
||||||
|
minWidth: 200,
|
||||||
|
flex: 1.5,
|
||||||
|
description: 'Observação da entrega 4',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -10,54 +10,56 @@ import { createInformationPanelColumns } from './InformationPanelColumns';
|
||||||
import { dataGridStylesSimple } from '../../utils/dataGridStyles';
|
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 { data: order, isLoading, error } = useOrderDetails(orderId);
|
||||||
|
|
||||||
const columns = useMemo(() => createInformationPanelColumns(), []);
|
const columns = useMemo(() => createInformationPanelColumns(), []);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
if (!order) return [];
|
if (!order) return [];
|
||||||
return [{
|
return [
|
||||||
id: order.orderId || orderId,
|
{
|
||||||
...order
|
id: order.orderId || orderId,
|
||||||
}];
|
...order,
|
||||||
}, [order, orderId]);
|
},
|
||||||
|
];
|
||||||
if (isLoading) {
|
}, [order, orderId]);
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|
||||||
<CircularProgress size={30} />
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<DataGridPremium
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||||
rows={rows}
|
<CircularProgress size={30} />
|
||||||
columns={columns}
|
</Box>
|
||||||
density="compact"
|
|
||||||
autoHeight
|
|
||||||
hideFooter
|
|
||||||
sx={dataGridStylesSimple}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,89 @@
|
||||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
|
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import { formatCurrency, formatDate, getStatusColor } from '../../utils/orderFormatters';
|
import {
|
||||||
|
formatCurrency,
|
||||||
/**
|
formatDate,
|
||||||
* Mapeia códigos de status para texto legível.
|
getStatusColor,
|
||||||
*/
|
getStatusLabel,
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
} from '../../utils/orderFormatters';
|
||||||
'F': 'Faturado',
|
|
||||||
'C': 'Cancelado',
|
|
||||||
'P': 'Pendente',
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusLabel = (status: string): string => STATUS_LABELS[status] || status;
|
|
||||||
|
|
||||||
export const createInformationPanelColumns = (): GridColDef[] => [
|
export const createInformationPanelColumns = (): GridColDef[] => [
|
||||||
{
|
{
|
||||||
field: 'customerName',
|
field: 'customerName',
|
||||||
headerName: 'Cliente',
|
headerName: 'Cliente',
|
||||||
width: 250,
|
minWidth: 180,
|
||||||
description: 'Nome do cliente do pedido',
|
flex: 1.5,
|
||||||
},
|
description: 'Nome do cliente do pedido',
|
||||||
{
|
},
|
||||||
field: 'storeId',
|
{
|
||||||
headerName: 'Filial',
|
field: 'storeId',
|
||||||
width: 80,
|
headerName: 'Filial',
|
||||||
align: 'center',
|
width: 60,
|
||||||
headerAlign: 'center',
|
align: 'center',
|
||||||
description: 'Código da filial',
|
headerAlign: 'center',
|
||||||
},
|
description: 'Código da filial',
|
||||||
{
|
},
|
||||||
field: 'createDate',
|
{
|
||||||
headerName: 'Data Criação',
|
field: 'createDate',
|
||||||
width: 110,
|
headerName: 'Data Criação',
|
||||||
align: 'center',
|
width: 95,
|
||||||
headerAlign: 'center',
|
align: 'center',
|
||||||
description: 'Data de criação do pedido',
|
headerAlign: 'center',
|
||||||
valueFormatter: (value) => formatDate(value as string),
|
description: 'Data de criação do pedido',
|
||||||
},
|
valueFormatter: (value) => formatDate(value as string),
|
||||||
{
|
},
|
||||||
field: 'status',
|
{
|
||||||
headerName: 'Situação',
|
field: 'status',
|
||||||
width: 120,
|
headerName: 'Situação',
|
||||||
align: 'center',
|
width: 90,
|
||||||
headerAlign: 'center',
|
align: 'center',
|
||||||
description: 'Situação atual do pedido',
|
headerAlign: 'center',
|
||||||
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
description: 'Situação atual do pedido',
|
||||||
<Chip
|
renderCell: (params: Readonly<GridRenderCellParams>) => (
|
||||||
label={getStatusLabel(params.value as string)}
|
<Chip
|
||||||
size="small"
|
label={getStatusLabel(params.value as string)}
|
||||||
color={getStatusColor(params.value as string)}
|
size="small"
|
||||||
variant="outlined"
|
color={getStatusColor(params.value as string)}
|
||||||
sx={{ height: 24 }}
|
variant="outlined"
|
||||||
/>
|
sx={{ height: 22, fontSize: '0.7rem' }}
|
||||||
),
|
/>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
field: 'paymentName',
|
{
|
||||||
headerName: 'Forma Pagamento',
|
field: 'paymentName',
|
||||||
width: 150,
|
headerName: 'Forma Pgto',
|
||||||
description: 'Forma de pagamento utilizada',
|
minWidth: 100,
|
||||||
},
|
flex: 1,
|
||||||
{
|
description: 'Forma de pagamento utilizada',
|
||||||
field: 'billingName',
|
},
|
||||||
headerName: 'Cond. Pagamento',
|
{
|
||||||
width: 150,
|
field: 'billingName',
|
||||||
description: 'Condição de pagamento',
|
headerName: 'Cond. Pgto',
|
||||||
},
|
minWidth: 100,
|
||||||
{
|
flex: 1,
|
||||||
field: 'amount',
|
description: 'Condição de pagamento',
|
||||||
headerName: 'Valor Total',
|
},
|
||||||
width: 120,
|
{
|
||||||
align: 'right',
|
field: 'amount',
|
||||||
headerAlign: 'right',
|
headerName: 'Valor',
|
||||||
description: 'Valor total do pedido',
|
width: 100,
|
||||||
valueFormatter: (value) => formatCurrency(value as number),
|
align: 'right',
|
||||||
},
|
headerAlign: 'right',
|
||||||
{
|
description: 'Valor total do pedido',
|
||||||
field: 'deliveryType',
|
valueFormatter: (value) => formatCurrency(value as number),
|
||||||
headerName: 'Tipo Entrega',
|
},
|
||||||
width: 150,
|
{
|
||||||
description: 'Tipo de entrega selecionado',
|
field: 'deliveryType',
|
||||||
},
|
headerName: 'Tipo Entrega',
|
||||||
{
|
minWidth: 100,
|
||||||
field: 'deliveryLocal',
|
flex: 1,
|
||||||
headerName: 'Local Entrega',
|
description: 'Tipo de entrega selecionado',
|
||||||
width: 200,
|
},
|
||||||
flex: 1,
|
{
|
||||||
description: 'Local de entrega do pedido',
|
field: 'deliveryLocal',
|
||||||
},
|
headerName: 'Local Entrega',
|
||||||
|
minWidth: 120,
|
||||||
|
flex: 1.2,
|
||||||
|
description: 'Local de entrega do pedido',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -12,88 +12,103 @@ import { OrderItem } from '../../schemas/order.item.schema';
|
||||||
import { dataGridStyles } from '../../utils/dataGridStyles';
|
import { dataGridStyles } from '../../utils/dataGridStyles';
|
||||||
|
|
||||||
interface OrderItemsTableProps {
|
interface OrderItemsTableProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
|
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
|
||||||
const { data: items, isLoading, error } = useOrderItems(orderId);
|
const { data: items, isLoading, error } = useOrderItems(orderId);
|
||||||
|
|
||||||
const columns = useMemo(() => createOrderItemsColumns(), []);
|
const columns = useMemo(() => createOrderItemsColumns(), []);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
if (!Array.isArray(items) || items.length === 0) return [];
|
if (!Array.isArray(items) || items.length === 0) return [];
|
||||||
return items.map((item: OrderItem, index: number) => ({
|
return items.map((item: OrderItem, index: number) => ({
|
||||||
id: `${orderId}-${item.productId}-${index}`,
|
id: `${orderId}-${item.productId}-${index}`,
|
||||||
...item,
|
...item,
|
||||||
}));
|
}));
|
||||||
}, [items, orderId]);
|
}, [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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<DataGridPremium
|
<Box
|
||||||
rows={rows}
|
sx={{
|
||||||
columns={columns}
|
display: 'flex',
|
||||||
density="compact"
|
justifyContent: 'center',
|
||||||
autoHeight
|
alignItems: 'center',
|
||||||
hideFooter={rows.length <= 10}
|
py: 4,
|
||||||
initialState={{
|
}}
|
||||||
pagination: {
|
>
|
||||||
paginationModel: {
|
<CircularProgress size={40} />
|
||||||
pageSize: 10,
|
<Typography variant="body2" color="text.secondary" sx={{ ml: 2 }}>
|
||||||
page: 0,
|
Carregando itens do pedido...
|
||||||
},
|
</Typography>
|
||||||
},
|
</Box>
|
||||||
}}
|
|
||||||
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}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
interface PreBoxPanelProps {
|
interface PreBoxPanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PreBoxPanel = ({ orderId }: PreBoxPanelProps) => {
|
export const PreBoxPanel = ({ orderId }: PreBoxPanelProps) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
Funcionalidade em desenvolvimento para o pedido {orderId}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
interface TV8PanelProps {
|
interface TV8PanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TV8Panel = ({ orderId }: TV8PanelProps) => {
|
export const TV8Panel = ({ orderId }: TV8PanelProps) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
Funcionalidade em desenvolvimento para o pedido {orderId}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
interface TimelinePanelProps {
|
interface TimelinePanelProps {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimelinePanel = ({ orderId }: TimelinePanelProps) => {
|
export const TimelinePanel = ({ orderId }: TimelinePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Alert severity="info" sx={{ mb: 2 }}>
|
<Alert severity="info" sx={{ mb: 2 }}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Funcionalidade em desenvolvimento para o pedido {orderId}
|
Funcionalidade em desenvolvimento para o pedido {orderId}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,13 @@
|
||||||
{
|
{
|
||||||
"status": {
|
"status": {
|
||||||
"success": [
|
"success": ["FATURADO", "F"],
|
||||||
"FATURADO",
|
"error": ["CANCELADO", "C"],
|
||||||
"F"
|
"warning": []
|
||||||
],
|
},
|
||||||
"error": [
|
"priority": {
|
||||||
"CANCELADO",
|
"error": ["ALTA"],
|
||||||
"C"
|
"warning": ["MÉDIA", "MEDIA"],
|
||||||
],
|
"success": ["BAIXA"],
|
||||||
"warning": []
|
"default": []
|
||||||
},
|
}
|
||||||
"priority": {
|
}
|
||||||
"error": [
|
|
||||||
"ALTA"
|
|
||||||
],
|
|
||||||
"warning": [
|
|
||||||
"MÉDIA",
|
|
||||||
"MEDIA"
|
|
||||||
],
|
|
||||||
"success": [
|
|
||||||
"BAIXA"
|
|
||||||
],
|
|
||||||
"default": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ Esta pasta contém a documentação técnica da feature de pedidos do Portal Jur
|
||||||
## Documentos Disponíveis
|
## Documentos Disponíveis
|
||||||
|
|
||||||
### [order-items-implementation.md](./order-items-implementation.md)
|
### [order-items-implementation.md](./order-items-implementation.md)
|
||||||
|
|
||||||
Documentação completa da implementação da funcionalidade de visualização de itens do pedido.
|
Documentação completa da implementação da funcionalidade de visualização de itens do pedido.
|
||||||
|
|
||||||
**Conteúdo:**
|
**Conteúdo:**
|
||||||
|
|
||||||
- Arquitetura da solução
|
- Arquitetura da solução
|
||||||
- Arquivos criados e modificados
|
- Arquivos criados e modificados
|
||||||
- Fluxo de execução
|
- Fluxo de execução
|
||||||
|
|
@ -20,9 +22,11 @@ Documentação completa da implementação da funcionalidade de visualização d
|
||||||
## Funcionalidades Documentadas
|
## Funcionalidades Documentadas
|
||||||
|
|
||||||
### Tabela de Itens do Pedido
|
### Tabela de Itens do Pedido
|
||||||
|
|
||||||
Permite visualizar os produtos/itens de um pedido ao clicar na linha da tabela principal.
|
Permite visualizar os produtos/itens de um pedido ao clicar na linha da tabela principal.
|
||||||
|
|
||||||
**Arquivos principais:**
|
**Arquivos principais:**
|
||||||
|
|
||||||
- `schemas/order.item.schema.ts` - Schema de validação
|
- `schemas/order.item.schema.ts` - Schema de validação
|
||||||
- `api/order.service.ts` - Serviço de API
|
- `api/order.service.ts` - Serviço de API
|
||||||
- `hooks/useOrderItems.ts` - Hook React Query
|
- `hooks/useOrderItems.ts` - Hook React Query
|
||||||
|
|
|
||||||
|
|
@ -25,24 +25,25 @@ 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:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const orderItemSchema = z.object({
|
export const orderItemSchema = z.object({
|
||||||
productId: z.coerce.number(), // Código do produto
|
productId: z.coerce.number(), // Código do produto
|
||||||
description: z.string(), // Descrição
|
description: z.string(), // Descrição
|
||||||
pacth: z.string(), // Unidade de medida
|
pacth: z.string(), // Unidade de medida
|
||||||
color: z.coerce.number(), // Código da cor
|
color: z.coerce.number(), // Código da cor
|
||||||
stockId: z.coerce.number(), // Código do estoque
|
stockId: z.coerce.number(), // Código do estoque
|
||||||
quantity: z.coerce.number(), // Quantidade
|
quantity: z.coerce.number(), // Quantidade
|
||||||
salePrice: z.coerce.number(), // Preço unitário
|
salePrice: z.coerce.number(), // Preço unitário
|
||||||
deliveryType: z.string(), // Tipo de entrega
|
deliveryType: z.string(), // Tipo de entrega
|
||||||
total: z.coerce.number(), // Valor total
|
total: z.coerce.number(), // Valor total
|
||||||
weight: z.coerce.number(), // Peso
|
weight: z.coerce.number(), // Peso
|
||||||
department: z.string(), // Departamento
|
department: z.string(), // Departamento
|
||||||
brand: z.string(), // Marca
|
brand: z.string(), // Marca
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -51,6 +52,7 @@ export const orderItemSchema = z.object({
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Serviço de API
|
### 2. Serviço de API
|
||||||
|
|
||||||
**Arquivo:** `src/features/orders/api/order.service.ts` (linha 145)
|
**Arquivo:** `src/features/orders/api/order.service.ts` (linha 145)
|
||||||
|
|
||||||
Adiciona método para buscar itens do pedido:
|
Adiciona método para buscar itens do pedido:
|
||||||
|
|
@ -64,12 +66,13 @@ findOrderItems: async (orderId: number): Promise<OrderItem[]> => {
|
||||||
console.error(`Erro ao buscar itens do pedido ${orderId}:`, error);
|
console.error(`Erro ao buscar itens do pedido ${orderId}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Endpoint:** `GET /api/v1/orders/itens/{orderId}`
|
**Endpoint:** `GET /api/v1/orders/itens/{orderId}`
|
||||||
|
|
||||||
**Resposta esperada:**
|
**Resposta esperada:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
|
@ -95,6 +98,7 @@ findOrderItems: async (orderId: number): Promise<OrderItem[]> => {
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Hook React Query
|
### 3. Hook React Query
|
||||||
|
|
||||||
**Arquivo:** `src/features/orders/hooks/useOrderItems.ts`
|
**Arquivo:** `src/features/orders/hooks/useOrderItems.ts`
|
||||||
|
|
||||||
Gerencia o estado e cache dos itens:
|
Gerencia o estado e cache dos itens:
|
||||||
|
|
@ -114,6 +118,7 @@ export function useOrderItems(orderId: number | null | undefined) {
|
||||||
```
|
```
|
||||||
|
|
||||||
**Características:**
|
**Características:**
|
||||||
|
|
||||||
- Cache de 5 minutos
|
- Cache de 5 minutos
|
||||||
- Só executa quando há `orderId` válido
|
- Só executa quando há `orderId` válido
|
||||||
- Retry automático (1 tentativa)
|
- Retry automático (1 tentativa)
|
||||||
|
|
@ -122,30 +127,32 @@ 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 |
|
||||||
| `color` | Cor | number | Não | Centralizado |
|
| `color` | Cor | number | Não | Centralizado |
|
||||||
| `stockId` | Cód. Estoque | number | Não | - |
|
| `stockId` | Cód. Estoque | number | Não | - |
|
||||||
| `quantity` | Qtd. | number | Sim | `formatNumber()` |
|
| `quantity` | Qtd. | number | Sim | `formatNumber()` |
|
||||||
| `salePrice` | Preço Unitário | number | Não | `formatCurrency()` |
|
| `salePrice` | Preço Unitário | number | Não | `formatCurrency()` |
|
||||||
| `total` | Valor Total | number | Sim | `formatCurrency()` |
|
| `total` | Valor Total | number | Sim | `formatCurrency()` |
|
||||||
| `deliveryType` | Tipo Entrega | string | Não | - |
|
| `deliveryType` | Tipo Entrega | string | Não | - |
|
||||||
| `weight` | Peso (kg) | number | Sim | `formatNumber()` |
|
| `weight` | Peso (kg) | number | Sim | `formatNumber()` |
|
||||||
| `department` | Departamento | string | Não | - |
|
| `department` | Departamento | string | Não | - |
|
||||||
| `brand` | Marca | string | Não | - |
|
| `brand` | Marca | string | Não | - |
|
||||||
|
|
||||||
**Agregação:** Colunas marcadas com "Sim" suportam totalização automática.
|
**Agregação:** Colunas marcadas com "Sim" suportam totalização automática.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Componente da Tabela
|
### 5. Componente da Tabela
|
||||||
|
|
||||||
**Arquivo:** `src/features/orders/components/OrderItemsTable.tsx`
|
**Arquivo:** `src/features/orders/components/OrderItemsTable.tsx`
|
||||||
|
|
||||||
Componente principal que renderiza a tabela de itens:
|
Componente principal que renderiza a tabela de itens:
|
||||||
|
|
@ -158,10 +165,11 @@ interface OrderItemsTableProps {
|
||||||
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
|
export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
|
||||||
const { data: items, isLoading, error } = useOrderItems(orderId);
|
const { data: items, isLoading, error } = useOrderItems(orderId);
|
||||||
// ... renderização
|
// ... renderização
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Estados:**
|
**Estados:**
|
||||||
|
|
||||||
- **Loading:** Exibe spinner + mensagem "Carregando itens..."
|
- **Loading:** Exibe spinner + mensagem "Carregando itens..."
|
||||||
- **Erro:** Exibe Alert vermelho com mensagem de erro
|
- **Erro:** Exibe Alert vermelho com mensagem de erro
|
||||||
- **Vazio:** Exibe Alert azul "Nenhum item encontrado"
|
- **Vazio:** Exibe Alert azul "Nenhum item encontrado"
|
||||||
|
|
@ -170,16 +178,19 @@ export const OrderItemsTable = ({ orderId }: OrderItemsTableProps) => {
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Integração na Tabela Principal
|
### 6. Integração na Tabela Principal
|
||||||
|
|
||||||
**Arquivo:** `src/features/orders/components/OrderTable.tsx`
|
**Arquivo:** `src/features/orders/components/OrderTable.tsx`
|
||||||
|
|
||||||
**Mudanças realizadas:**
|
**Mudanças realizadas:**
|
||||||
|
|
||||||
#### a) Estado para pedido selecionado
|
#### a) Estado para pedido selecionado
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### b) Handler de clique na linha
|
#### b) Handler de clique na linha
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
onRowClick={(params) => {
|
onRowClick={(params) => {
|
||||||
const orderId = params.row.orderId;
|
const orderId = params.row.orderId;
|
||||||
|
|
@ -188,13 +199,15 @@ 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' : ''
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### d) Estilo visual da linha selecionada
|
#### d) Estilo visual da linha selecionada
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'& .MuiDataGrid-row.Mui-selected': {
|
'& .MuiDataGrid-row.Mui-selected': {
|
||||||
backgroundColor: 'primary.light',
|
backgroundColor: 'primary.light',
|
||||||
|
|
@ -205,6 +218,7 @@ getRowClassName={(params) =>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### e) Renderização condicional da tabela de itens
|
#### e) Renderização condicional da tabela de itens
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{selectedOrderId && <OrderItemsTable orderId={selectedOrderId} />}
|
{selectedOrderId && <OrderItemsTable orderId={selectedOrderId} />}
|
||||||
```
|
```
|
||||||
|
|
@ -226,11 +240,13 @@ getRowClassName={(params) =>
|
||||||
## Problemas Encontrados e Soluções
|
## Problemas Encontrados e Soluções
|
||||||
|
|
||||||
### Problema 1: "Nenhum item encontrado"
|
### Problema 1: "Nenhum item encontrado"
|
||||||
|
|
||||||
**Sintoma:** Mesmo com dados na API, a mensagem "Nenhum item encontrado" aparecia.
|
**Sintoma:** Mesmo com dados na API, a mensagem "Nenhum item encontrado" aparecia.
|
||||||
|
|
||||||
**Causa:** A API retorna campos numéricos como strings (`"123"` ao invés de `123`), mas o schema Zod esperava `number`.
|
**Causa:** A API retorna campos numéricos como strings (`"123"` ao invés de `123`), mas o schema Zod esperava `number`.
|
||||||
|
|
||||||
**Solução:** Usar `z.coerce.number()` em todos os campos numéricos:
|
**Solução:** Usar `z.coerce.number()` em todos os campos numéricos:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
- productId: z.number(),
|
- productId: z.number(),
|
||||||
+ productId: z.coerce.number(),
|
+ productId: z.coerce.number(),
|
||||||
|
|
@ -241,9 +257,11 @@ getRowClassName={(params) =>
|
||||||
---
|
---
|
||||||
|
|
||||||
### Problema 2: Nomes de colunas genéricos
|
### Problema 2: Nomes de colunas genéricos
|
||||||
|
|
||||||
**Sintoma:** Colunas com nomes pouco descritivos ("Código", "Lote").
|
**Sintoma:** Colunas com nomes pouco descritivos ("Código", "Lote").
|
||||||
|
|
||||||
**Solução:** Renomear baseado nos dados reais:
|
**Solução:** Renomear baseado nos dados reais:
|
||||||
|
|
||||||
- `Código` → `Cód. Produto`
|
- `Código` → `Cód. Produto`
|
||||||
- `Lote` → `Unidade` (campo contém unidade de medida)
|
- `Lote` → `Unidade` (campo contém unidade de medida)
|
||||||
- `Estoque` → `Cód. Estoque`
|
- `Estoque` → `Cód. Estoque`
|
||||||
|
|
@ -254,6 +272,7 @@ getRowClassName={(params) =>
|
||||||
## Estilização
|
## Estilização
|
||||||
|
|
||||||
### Cabeçalho da Tabela
|
### Cabeçalho da Tabela
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'& .MuiDataGrid-columnHeaders': {
|
'& .MuiDataGrid-columnHeaders': {
|
||||||
backgroundColor: 'grey.50',
|
backgroundColor: 'grey.50',
|
||||||
|
|
@ -265,6 +284,7 @@ getRowClassName={(params) =>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linhas
|
### Linhas
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'& .MuiDataGrid-row': {
|
'& .MuiDataGrid-row': {
|
||||||
minHeight: '36px !important',
|
minHeight: '36px !important',
|
||||||
|
|
@ -278,6 +298,7 @@ getRowClassName={(params) =>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Células
|
### Células
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'& .MuiDataGrid-cell': {
|
'& .MuiDataGrid-cell': {
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
|
|
@ -310,9 +331,9 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook para buscar movimentação de carga de um pedido específico.
|
||||||
|
*/
|
||||||
|
export function useCargoMovement(orderId: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['orderCargoMovement', orderId],
|
||||||
|
enabled: !!orderId,
|
||||||
|
queryFn: () => orderService.findCargoMovement(orderId),
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -33,16 +33,16 @@ export function useCustomers(searchTerm: string) {
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = query.data?.map((customer, index) => ({
|
const options =
|
||||||
value: customer.id.toString(),
|
query.data?.map((customer, index) => ({
|
||||||
label: customer.name,
|
value: customer.id.toString(),
|
||||||
id: `customer-${customer.id}-${index}`,
|
label: customer.name,
|
||||||
customer: customer,
|
id: `customer-${customer.id}-${index}`,
|
||||||
})) ?? [];
|
customer: customer,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...query,
|
...query,
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook para buscar itens de corte de um pedido específico.
|
||||||
|
*/
|
||||||
|
export function useCuttingItems(orderId: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['orderCuttingItems', orderId],
|
||||||
|
enabled: !!orderId,
|
||||||
|
queryFn: () => orderService.findCuttingItems(orderId),
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook para buscar entregas de um pedido específico.
|
||||||
|
*/
|
||||||
|
export function useDelivery(orderId: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['orderDelivery', orderId],
|
||||||
|
enabled: !!orderId,
|
||||||
|
queryFn: () => orderService.findDelivery(orderId, true),
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -6,14 +6,14 @@ import { orderService } from '../api/order.service';
|
||||||
* Uses the general search endpoint filtering by ID as requested.
|
* Uses the general search endpoint filtering by ID as requested.
|
||||||
*/
|
*/
|
||||||
export function useOrderDetails(orderId: number) {
|
export function useOrderDetails(orderId: number) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['orderDetails', orderId],
|
queryKey: ['orderDetails', orderId],
|
||||||
enabled: !!orderId,
|
enabled: !!orderId,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// The findOrders method returns an array. We search by orderId and take the first result.
|
// The findOrders method returns an array. We search by orderId and take the first result.
|
||||||
const orders = await orderService.findOrders({ orderId: orderId });
|
const orders = await orderService.findOrders({ orderId: orderId });
|
||||||
return orders.length > 0 ? orders[0] : null;
|
return orders.length > 0 ? orders[0] : null;
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,36 @@ 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,
|
{
|
||||||
sellerName: parseAsString,
|
status: parseAsString,
|
||||||
sellerId: parseAsString,
|
sellerName: parseAsString,
|
||||||
customerName: parseAsString,
|
sellerId: parseAsString,
|
||||||
customerId: parseAsInteger,
|
customerName: parseAsString,
|
||||||
|
customerId: parseAsInteger,
|
||||||
|
|
||||||
codfilial: parseAsArrayOf(parseAsString, ','),
|
codfilial: parseAsArrayOf(parseAsString, ','),
|
||||||
codusur2: parseAsArrayOf(parseAsString, ','),
|
codusur2: parseAsArrayOf(parseAsString, ','),
|
||||||
store: parseAsArrayOf(parseAsString, ','),
|
store: parseAsArrayOf(parseAsString, ','),
|
||||||
orderId: parseAsInteger,
|
orderId: parseAsInteger,
|
||||||
productId: parseAsInteger,
|
productId: parseAsInteger,
|
||||||
stockId: parseAsArrayOf(parseAsString, ','),
|
stockId: parseAsArrayOf(parseAsString, ','),
|
||||||
|
|
||||||
hasPreBox: parseAsBoolean.withDefault(false),
|
hasPreBox: parseAsBoolean.withDefault(false),
|
||||||
includeCheckout: parseAsBoolean.withDefault(false),
|
includeCheckout: parseAsBoolean.withDefault(false),
|
||||||
|
|
||||||
createDateIni: parseAsString,
|
createDateIni: parseAsString,
|
||||||
createDateEnd: parseAsString,
|
createDateEnd: parseAsString,
|
||||||
|
|
||||||
searchTriggered: parseAsBoolean.withDefault(false),
|
searchTriggered: parseAsBoolean.withDefault(false),
|
||||||
}, {
|
},
|
||||||
shallow: true,
|
{
|
||||||
history: 'replace',
|
shallow: true,
|
||||||
});
|
history: 'replace',
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ import { orderService } from '../api/order.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to fetch order items for a specific order.
|
* Hook to fetch order items for a specific order.
|
||||||
*
|
*
|
||||||
* @param orderId - The ID of the order to fetch items for
|
* @param orderId - The ID of the order to fetch items for
|
||||||
* @returns Query result with order items data, loading state, and error
|
* @returns Query result with order items data, loading state, and error
|
||||||
*/
|
*/
|
||||||
export function useOrderItems(orderId: number | null | undefined) {
|
export function useOrderItems(orderId: number | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['orderItems', orderId],
|
queryKey: ['orderItems', orderId],
|
||||||
enabled: orderId != null && orderId > 0,
|
enabled: orderId != null && orderId > 0,
|
||||||
queryFn: () => orderService.findOrderItems(orderId!),
|
queryFn: () => orderService.findOrderItems(orderId!),
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
retry: 1,
|
retry: 1,
|
||||||
retryOnMount: false,
|
retryOnMount: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue