feat: Initialize Next.js project with Shadcn UI components, Tailwind CSS, and Oracle DB integration.
This commit is contained in:
parent
18c9301ae5
commit
7c613d5249
|
|
@ -0,0 +1,37 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
.env
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {
|
||||||
|
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||||
|
...nextVitals,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'react/no-unescaped-entities': 'off',
|
||||||
|
'@next/next/no-page-custom-font': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'node_modules/**',
|
||||||
|
'.next/**',
|
||||||
|
'out/**',
|
||||||
|
'build/**',
|
||||||
|
'next-env.d.ts',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
// Next 16+: use `serverExternalPackages` instead of experimental.serverComponentsExternalPackages
|
||||||
|
serverExternalPackages: ['oracledb'],
|
||||||
|
|
||||||
|
// Add an explicit (empty) Turbopack config so Next 16 doesn't error
|
||||||
|
// when using Turbopack together with a webpack config.
|
||||||
|
turbopack: {},
|
||||||
|
|
||||||
|
webpack: (config, { isServer }) => {
|
||||||
|
if (isServer) {
|
||||||
|
config.externals = config.externals || [];
|
||||||
|
config.externals.push('oracledb');
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "portal-calendario-dias-rota",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start --port 3003",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@faker-js/faker": "^10.1.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@types/oracledb": "^6.9.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"jotai": "^2.16.0",
|
||||||
|
"lucide-react": "^0.560.0",
|
||||||
|
"next": "16.1.1",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"oracledb": "^6.9.0",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "^16.1.1",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { executeOracleUpdate } from '@/db/oracle';
|
||||||
|
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||||
|
|
||||||
|
async function updateBladinho(delivery: boolean, date: Date) {
|
||||||
|
// Converte boolean para 'S' ou 'N' conforme esperado pelo Oracle
|
||||||
|
const deliveryValue = delivery ? 'N' : 'S';
|
||||||
|
|
||||||
|
// Formata a data para DD/MM/YYYY conforme exigido pelo Oracle
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const formattedDate = `${day}/${month}/${year}`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🔄 Tentando atualizar: data=${formattedDate}, delivery=${deliveryValue}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Usa objeto de binds para evitar problemas com palavras reservadas
|
||||||
|
// TO_DATE converte a string DD/MM/YYYY para DATE do Oracle
|
||||||
|
const sql = `
|
||||||
|
UPDATE PCDIASUTEIS
|
||||||
|
SET DIAROTA = :deliveryVal
|
||||||
|
WHERE DATA = TO_DATE(:dataVal, 'DD/MM/YYYY') AND CODFILIAL = 12
|
||||||
|
`;
|
||||||
|
const binds = {
|
||||||
|
deliveryVal: deliveryValue,
|
||||||
|
dataVal: formattedDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📝 SQL:', sql);
|
||||||
|
console.log('📦 Binds:', JSON.stringify(binds, null, 2));
|
||||||
|
|
||||||
|
const rowsAffected = await executeOracleUpdate(sql, binds);
|
||||||
|
console.log(`✅ UPDATE concluído: ${rowsAffected} linha(s) atualizada(s)`);
|
||||||
|
|
||||||
|
if (rowsAffected === 0) {
|
||||||
|
console.warn(
|
||||||
|
'⚠️ Nenhuma linha foi atualizada. Verifique se a data existe na tabela.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowsAffected;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBaldinhoDaysSale(daysSale: number): Promise<number> {
|
||||||
|
const sql = `UPDATE PCPARAMFILIAL SET PCPARAMFILIAL.VALOR=:daysSale WHERE NOME = 'DIAS_CORTE_LOGISTICA' AND CODFILIAL=12`;
|
||||||
|
const binds = {
|
||||||
|
daysSale: daysSale,
|
||||||
|
};
|
||||||
|
const result = await executeOracleUpdate(sql, binds);
|
||||||
|
|
||||||
|
console.log(`✅ UPDATE concluído: ${result} linha(s) atualizada(s)`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBaldinhoDeliverySize(
|
||||||
|
deliverySize: number
|
||||||
|
): Promise<number> {
|
||||||
|
const sql = `UPDATE PCPARAMFILIAL SET PCPARAMFILIAL.VALOR=:deliverySize WHERE NOME = 'CAPACIDADE_LOGISTICA' AND CODFILIAL=12`;
|
||||||
|
const binds = {
|
||||||
|
deliverySize: deliverySize,
|
||||||
|
};
|
||||||
|
const result = await executeOracleUpdate(sql, binds);
|
||||||
|
console.log(`✅ UPDATE concluído: ${result} linha(s) atualizada(s)`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeNoDelivery(date: Date, delivery: boolean) {
|
||||||
|
await updateBladinho(delivery, date);
|
||||||
|
console.log(
|
||||||
|
`clicado: ${date.toLocaleDateString('pt-BR')}, delivery: ${delivery}`
|
||||||
|
);
|
||||||
|
// Next 16 requires cache profile; empty config preserves current defaults
|
||||||
|
revalidateTag('baldinho', {});
|
||||||
|
revalidatePath('/dias-rota');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeBaldinhoDaysSale(daysSale: number) {
|
||||||
|
await updateBaldinhoDaysSale(daysSale);
|
||||||
|
console.log(`clicado: ${daysSale}`);
|
||||||
|
revalidateTag('baldinho');
|
||||||
|
revalidatePath('/dias-rota');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeBaldinhoDeliverySize(deliverySize: number) {
|
||||||
|
await updateBaldinhoDeliverySize(deliverySize);
|
||||||
|
console.log(`clicado: ${deliverySize}`);
|
||||||
|
revalidateTag('baldinho');
|
||||||
|
revalidatePath('/dias-rota');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { executeOracleQuery } from '@/db/oracle';
|
||||||
|
import { unstable_cache } from 'next/cache';
|
||||||
|
|
||||||
|
export interface GetBladinhoParams {
|
||||||
|
dateDelivery: Date;
|
||||||
|
delivery: string;
|
||||||
|
deliverySize: number;
|
||||||
|
saleWeight: number;
|
||||||
|
avaliableDelivery: number;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetBladinhoResponse {
|
||||||
|
dateDelivery: Date;
|
||||||
|
delivery: boolean;
|
||||||
|
deliverySize: number;
|
||||||
|
saleWeight: number;
|
||||||
|
avaliableDelivery: number;
|
||||||
|
id: number;
|
||||||
|
daysSale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBladinho(): Promise<GetBladinhoResponse[]> {
|
||||||
|
const sql = `
|
||||||
|
SELECT rownum as "id",
|
||||||
|
PCDIASUTEIS.DATA as "dateDelivery",
|
||||||
|
NVL (PCDIASUTEIS.DIAROTA, 'N') as "delivery",
|
||||||
|
CASE WHEN NVL (PCDIASUTEIS.DIAROTA, 'N') = 'N' THEN 0
|
||||||
|
ELSE (PARAMFILIAL.OBTERCOMONUMBER ('CAPACIDADE_LOGISTICA', 12)) END as "deliverySize",
|
||||||
|
ROUND ( (NVL (VENDAS.TOTPESO, 0) / 1000), 3) as "saleWeight",
|
||||||
|
CASE WHEN NVL (PCDIASUTEIS.DIAROTA, 'N') = 'N' THEN 0
|
||||||
|
ELSE ROUND (
|
||||||
|
GREATEST (
|
||||||
|
( ( PARAMFILIAL.OBTERCOMONUMBER ('CAPACIDADE_LOGISTICA',
|
||||||
|
12)
|
||||||
|
* 1000)
|
||||||
|
- NVL (VENDAS.TOTPESO, 0))
|
||||||
|
/ 1000,
|
||||||
|
0),
|
||||||
|
3) END as "avaliableDelivery",
|
||||||
|
PARAMFILIAL.OBTERCOMONUMBER ('DIAS_CORTE_LOGISTICA',12) "daysSale"
|
||||||
|
FROM PCDIASUTEIS,
|
||||||
|
( SELECT PCPEDC.DTENTREGA, SUM (PCPEDC.TOTPESO) TOTPESO
|
||||||
|
FROM PCPEDC
|
||||||
|
WHERE PCPEDC.POSICAO IN ('L', 'M')
|
||||||
|
AND PCPEDC.CONDVENDA = 8
|
||||||
|
AND PCPEDC.CODFILIAL IN (12, 13, 4, 6)
|
||||||
|
AND EXISTS
|
||||||
|
(SELECT TV7.NUMPED
|
||||||
|
FROM PCPEDC TV7
|
||||||
|
WHERE TV7.NUMPED = PCPEDC.NUMPEDENTFUT
|
||||||
|
AND TV7.POSICAO = 'F')
|
||||||
|
GROUP BY PCPEDC.DTENTREGA) VENDAS
|
||||||
|
WHERE PCDIASUTEIS.CODFILIAL = 12 --AND NVL(PCDIASUTEIS.DIAROTA,'N') = 'S'
|
||||||
|
AND PCDIASUTEIS.DATA = VENDAS.DTENTREGA(+)
|
||||||
|
ORDER BY PCDIASUTEIS.DATA
|
||||||
|
`;
|
||||||
|
const result = await executeOracleQuery(sql);
|
||||||
|
return result.map((item): GetBladinhoResponse => {
|
||||||
|
let deliveryBoolean: boolean;
|
||||||
|
if (item.delivery === 'N') {
|
||||||
|
deliveryBoolean = false;
|
||||||
|
} else {
|
||||||
|
deliveryBoolean = true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dateDelivery: item.dateDelivery,
|
||||||
|
delivery: deliveryBoolean,
|
||||||
|
deliverySize: item.deliverySize,
|
||||||
|
saleWeight: item.saleWeight || 0,
|
||||||
|
avaliableDelivery: item.avaliableDelivery,
|
||||||
|
id: item.id,
|
||||||
|
daysSale: item.daysSale || 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cachedGetBladinho = unstable_cache(getBladinho, ['bladinho'], {
|
||||||
|
revalidate: 60, // 1 minute
|
||||||
|
tags: ['baldinho'],
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { cachedGetBladinho } from '../data-access/calendario/get-bladinho';
|
||||||
|
import Pagina from './pagina';
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const bladinho = await cachedGetBladinho();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Pagina bladinhoData={bladinho} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
CalendarBody,
|
||||||
|
CalendarDate,
|
||||||
|
CalendarDatePagination,
|
||||||
|
CalendarDatePicker,
|
||||||
|
CalendarHeader,
|
||||||
|
CalendarMonthPicker,
|
||||||
|
CalendarProvider,
|
||||||
|
CalendarYearPicker,
|
||||||
|
type Feature,
|
||||||
|
} from '@/components/kibo-ui/calendar';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
changeBaldinhoDaysSale,
|
||||||
|
changeBaldinhoDeliverySize,
|
||||||
|
changeNoDelivery,
|
||||||
|
} from '../action/update-baldinho';
|
||||||
|
import { type GetBladinhoResponse } from '../data-access/calendario/get-bladinho';
|
||||||
|
|
||||||
|
interface PaginaProps {
|
||||||
|
bladinhoData: GetBladinhoResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Componente customizado para exibir informações em múltiplas linhas
|
||||||
|
const CustomCalendarItem = ({ feature }: { feature: Feature }) => {
|
||||||
|
const lines = feature.name.split('\n');
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-full mt-1"
|
||||||
|
style={{
|
||||||
|
backgroundColor: feature.status.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
{lines.map((line, index) => (
|
||||||
|
<span key={index} className="text-xs leading-tight break-words">
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pagina = ({ bladinhoData }: PaginaProps) => {
|
||||||
|
// Estado para controlar a abertura do AlertDialog
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
// Estado para armazenar a data e o novo valor de delivery quando o usuário clicar
|
||||||
|
const [pendingUpdate, setPendingUpdate] = useState<{
|
||||||
|
date: Date;
|
||||||
|
delivery: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Transforma os dados do bladinho em features do calendário
|
||||||
|
|
||||||
|
const [valorBaldinho, setValorBaldinho] = useState<number | undefined>(
|
||||||
|
bladinhoData[0].deliverySize ?? 0
|
||||||
|
);
|
||||||
|
const [diasBaldinho, setDiasBaldinho] = useState<number | undefined>(
|
||||||
|
bladinhoData[0].daysSale ?? 0
|
||||||
|
);
|
||||||
|
const [isLoadingDaysSale, setIsLoadingDaysSale] = useState(false);
|
||||||
|
const [isLoadingDeliverySize, setIsLoadingDeliverySize] = useState(false);
|
||||||
|
// Estado para controlar a abertura do AlertDialog
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
// Estado para armazenar a data e o novo valor de delivery quando o usuário clicar
|
||||||
|
const [pendingUpdate, setPendingUpdate] = useState<{
|
||||||
|
date: Date;
|
||||||
|
delivery: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const features = useMemo<Feature[]>(() => {
|
||||||
|
return bladinhoData.map((item) => {
|
||||||
|
const dateDelivery = new Date(item.dateDelivery);
|
||||||
|
// Cria um texto com os dados adicionais em formato de array para exibir em linhas separadas
|
||||||
|
const additionalInfo = [
|
||||||
|
`Capacidade: ${item.deliverySize}`,
|
||||||
|
`Peso Venda: ${item.saleWeight}`,
|
||||||
|
`Disponível: ${item.avaliableDelivery}`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id.toString(),
|
||||||
|
name: additionalInfo,
|
||||||
|
startAt: dateDelivery,
|
||||||
|
endAt: dateDelivery,
|
||||||
|
status: {
|
||||||
|
id: item.id.toString(),
|
||||||
|
name: item.delivery ? 'Delivery' : 'No Delivery',
|
||||||
|
color: item.delivery ? '#6B7280' : '#EF4444', // Cinza se delivery, vermelho se não
|
||||||
|
},
|
||||||
|
isSelected: !item.delivery, // Invertido: fundo vermelho quando delivery é false
|
||||||
|
delivery: item.delivery, // Adiciona a propriedade delivery para uso no calendário
|
||||||
|
} as Feature & { delivery: boolean };
|
||||||
|
});
|
||||||
|
}, [bladinhoData]);
|
||||||
|
|
||||||
|
// Atualiza os features quando os dados mudarem
|
||||||
|
const selectedFeatures = useMemo(() => features, [features]);
|
||||||
|
|
||||||
|
const earliestYear = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedFeatures
|
||||||
|
.map((feature) => feature.startAt.getFullYear())
|
||||||
|
.sort()
|
||||||
|
.at(0) ?? new Date().getFullYear(),
|
||||||
|
[selectedFeatures]
|
||||||
|
);
|
||||||
|
|
||||||
|
const latestYear = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedFeatures
|
||||||
|
.map((feature) => feature.endAt.getFullYear())
|
||||||
|
.sort()
|
||||||
|
.at(-1) ?? new Date().getFullYear(),
|
||||||
|
[selectedFeatures]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDateClick = (date: Date, dayFeatures?: Feature[]) => {
|
||||||
|
// Encontra o feature correspondente à data clicada
|
||||||
|
const featureForDate = dayFeatures?.find((feature) => {
|
||||||
|
const featureDate = new Date(feature.startAt);
|
||||||
|
return (
|
||||||
|
featureDate.getDate() === date.getDate() &&
|
||||||
|
featureDate.getMonth() === date.getMonth() &&
|
||||||
|
featureDate.getFullYear() === date.getFullYear()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let newDelivery: boolean;
|
||||||
|
if (featureForDate) {
|
||||||
|
// Extrai o isSelected atual do feature
|
||||||
|
const currentIsSelected = featureForDate.isSelected ?? false;
|
||||||
|
// Inverte o valor para obter o novo delivery (!isSelected)
|
||||||
|
newDelivery = !currentIsSelected;
|
||||||
|
} else {
|
||||||
|
// Se não há feature para esta data, cria um novo com delivery = true
|
||||||
|
newDelivery = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Armazena os dados e abre o dialog
|
||||||
|
setPendingUpdate({ date, delivery: newDelivery });
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmUpdate = async () => {
|
||||||
|
if (pendingUpdate) {
|
||||||
|
// Executa o update apenas quando o usuário confirmar
|
||||||
|
await changeNoDelivery(pendingUpdate.date, pendingUpdate.delivery);
|
||||||
|
// Fecha o dialog e limpa o estado
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setPendingUpdate(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelUpdate = () => {
|
||||||
|
// Apenas fecha o dialog e limpa o estado, sem fazer update
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setPendingUpdate(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBaldinhoDaysSaleChange = async (daysSale: number) => {
|
||||||
|
if ((daysSale ?? 0) <= 0) {
|
||||||
|
toast.error('O valor deve ser maior que zero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDiasBaldinho(daysSale);
|
||||||
|
setIsLoadingDaysSale(true);
|
||||||
|
|
||||||
|
const toastId = toast.loading('Atualizando dias baldinho...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await changeBaldinhoDaysSale(daysSale);
|
||||||
|
toast.success(`Dias baldinho atualizado para ${daysSale}`, {
|
||||||
|
id: toastId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Erro ao atualizar dias baldinho', { id: toastId });
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDaysSale(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Armazena os dados e abre o dialog
|
||||||
|
setPendingUpdate({ date, delivery: newDelivery });
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmUpdate = async () => {
|
||||||
|
if (pendingUpdate) {
|
||||||
|
// Executa o update apenas quando o usuário confirmar
|
||||||
|
await changeNoDelivery(pendingUpdate.date, pendingUpdate.delivery);
|
||||||
|
// Fecha o dialog e limpa o estado
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setPendingUpdate(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelUpdate = () => {
|
||||||
|
// Apenas fecha o dialog e limpa o estado, sem fazer update
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setPendingUpdate(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBaldinhoDeliverySizeChange = async (deliverySize: number) => {
|
||||||
|
if ((deliverySize ?? 0) <= 0) {
|
||||||
|
toast.error('O valor deve ser maior que zero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValorBaldinho(deliverySize);
|
||||||
|
setIsLoadingDeliverySize(true);
|
||||||
|
|
||||||
|
const toastId = toast.loading('Atualizando capacidade baldinho...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await changeBaldinhoDeliverySize(deliverySize);
|
||||||
|
toast.success(`Capacidade baldinho atualizada para ${deliverySize}`, {
|
||||||
|
id: toastId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Erro ao atualizar capacidade baldinho', { id: toastId });
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDeliverySize(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<CalendarProvider>
|
||||||
|
<CalendarDate>
|
||||||
|
<CalendarDatePicker>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<CalendarMonthPicker />
|
||||||
|
<CalendarYearPicker end={latestYear} start={earliestYear} />
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<Label className="text-sm min-w-32">Capacidade</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={valorBaldinho}
|
||||||
|
onChange={(e) => setValorBaldinho(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleBaldinhoDeliverySizeChange(valorBaldinho ?? 0)
|
||||||
|
}
|
||||||
|
disabled={isLoadingDeliverySize}
|
||||||
|
>
|
||||||
|
{isLoadingDeliverySize ? 'Salvando...' : 'Salvar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<Label className="text-sm min-w-32">Dias úteis</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={diasBaldinho}
|
||||||
|
onChange={(e) => setDiasBaldinho(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleBaldinhoDaysSaleChange(diasBaldinho ?? 0)
|
||||||
|
}
|
||||||
|
disabled={isLoadingDaysSale}
|
||||||
|
>
|
||||||
|
{isLoadingDaysSale ? 'Salvando...' : 'Salvar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CalendarDatePicker>
|
||||||
|
<CalendarDatePagination />
|
||||||
|
</CalendarDate>
|
||||||
|
<CalendarHeader />
|
||||||
|
<CalendarBody features={selectedFeatures} onDateClick={handleDateClick}>
|
||||||
|
{({ feature }) => (
|
||||||
|
<CustomCalendarItem feature={feature} key={feature.id} />
|
||||||
|
)}
|
||||||
|
</CalendarBody>
|
||||||
|
</CalendarProvider>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Confirmar alteração</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Tem certeza que deseja alterar o status de entrega para esta data?
|
||||||
|
Esta ação não pode ser desfeita.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={handleCancelUpdate}>
|
||||||
|
Cancelar
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleConfirmUpdate}>
|
||||||
|
Confirmar
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagina;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,75 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 0 0% 3.9%;
|
||||||
|
--primary: 0 72.2% 50.6%;
|
||||||
|
--primary-foreground: 0 85.7% 97.3%;
|
||||||
|
--secondary: 0 0% 96.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
--muted: 0 0% 96.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
--accent: 0 0% 96.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
--ring: 0 72.2% 50.6%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 72.2% 50.6%;
|
||||||
|
--primary-foreground: 0 85.7% 97.3%;
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
--ring: 0 72.2% 50.6%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import "./globals.css";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
|
const geistSans = localFont({
|
||||||
|
src: "./fonts/GeistVF.woff",
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
weight: "100 900",
|
||||||
|
});
|
||||||
|
const geistMono = localFont({
|
||||||
|
src: "./fonts/GeistMonoVF.woff",
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
weight: "100 900",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,522 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { getDay, getDaysInMonth, isSameDay } from 'date-fns';
|
||||||
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export type CalendarState = {
|
||||||
|
month: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
|
||||||
|
year: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthAtom = atom<CalendarState['month']>(
|
||||||
|
new Date().getMonth() as CalendarState['month']
|
||||||
|
);
|
||||||
|
const yearAtom = atom<CalendarState['year']>(new Date().getFullYear());
|
||||||
|
|
||||||
|
export const useCalendarMonth = () => useAtom(monthAtom);
|
||||||
|
export const useCalendarYear = () => useAtom(yearAtom);
|
||||||
|
|
||||||
|
type CalendarContextProps = {
|
||||||
|
locale: Intl.LocalesArgument;
|
||||||
|
startDay: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CalendarContext = createContext<CalendarContextProps>({
|
||||||
|
locale: 'pt-BR',
|
||||||
|
startDay: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Status = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Feature = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
startAt: Date;
|
||||||
|
endAt: Date;
|
||||||
|
status: Status;
|
||||||
|
isSelected?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComboboxProps = {
|
||||||
|
value: string;
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
data: {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
labels: {
|
||||||
|
button: string;
|
||||||
|
empty: string;
|
||||||
|
search: string;
|
||||||
|
};
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const monthsForLocale = (
|
||||||
|
localeName: Intl.LocalesArgument,
|
||||||
|
monthFormat: Intl.DateTimeFormatOptions['month'] = 'long'
|
||||||
|
) => {
|
||||||
|
const format = new Intl.DateTimeFormat(localeName, { month: monthFormat })
|
||||||
|
.format;
|
||||||
|
|
||||||
|
return Array.from({ length: 12 }, (_, m) =>
|
||||||
|
format(new Date(Date.UTC(2021, m, 2)))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const daysForLocale = (
|
||||||
|
locale: Intl.LocalesArgument,
|
||||||
|
startDay: number
|
||||||
|
) => {
|
||||||
|
const weekdays: string[] = [];
|
||||||
|
const baseDate = new Date(2024, 0, startDay);
|
||||||
|
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
weekdays.push(
|
||||||
|
new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(baseDate)
|
||||||
|
);
|
||||||
|
baseDate.setDate(baseDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return weekdays;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Combobox = ({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
data,
|
||||||
|
labels,
|
||||||
|
className,
|
||||||
|
}: ComboboxProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover onOpenChange={setOpen} open={open}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-expanded={open}
|
||||||
|
className={cn('w-40 justify-between capitalize', className)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{value
|
||||||
|
? data.find((item) => item.value === value)?.label
|
||||||
|
: labels.button}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-40 p-0">
|
||||||
|
<Command
|
||||||
|
filter={(value, search) => {
|
||||||
|
const label = data.find((item) => item.value === value)?.label;
|
||||||
|
|
||||||
|
return label?.toLowerCase().includes(search.toLowerCase()) ? 1 : 0;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommandInput placeholder={labels.search} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>{labels.empty}</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{data.map((item) => (
|
||||||
|
<CommandItem
|
||||||
|
className="capitalize"
|
||||||
|
key={item.value}
|
||||||
|
onSelect={(currentValue) => {
|
||||||
|
setValue(currentValue === value ? '' : currentValue);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
value={item.value}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
value === item.value ? 'opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{item.label}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type OutOfBoundsDayProps = {
|
||||||
|
day: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OutOfBoundsDay = ({ day }: OutOfBoundsDayProps) => (
|
||||||
|
<div className="relative h-full w-full bg-secondary p-1 text-muted-foreground text-xs">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CalendarBodyProps = {
|
||||||
|
features: Feature[];
|
||||||
|
children: (props: { feature: Feature }) => ReactNode;
|
||||||
|
onDateClick?: (date: Date, dayFeatures?: Feature[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarBody = ({
|
||||||
|
features,
|
||||||
|
children,
|
||||||
|
onDateClick,
|
||||||
|
}: CalendarBodyProps) => {
|
||||||
|
const [month] = useCalendarMonth();
|
||||||
|
const [year] = useCalendarYear();
|
||||||
|
const { startDay } = useContext(CalendarContext);
|
||||||
|
|
||||||
|
// Memoize expensive date calculations
|
||||||
|
const currentMonthDate = useMemo(
|
||||||
|
() => new Date(year, month, 1),
|
||||||
|
[year, month]
|
||||||
|
);
|
||||||
|
const daysInMonth = useMemo(
|
||||||
|
() => getDaysInMonth(currentMonthDate),
|
||||||
|
[currentMonthDate]
|
||||||
|
);
|
||||||
|
const firstDay = useMemo(
|
||||||
|
() => (getDay(currentMonthDate) - startDay + 7) % 7,
|
||||||
|
[currentMonthDate, startDay]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize previous month calculations
|
||||||
|
const prevMonthData = useMemo(() => {
|
||||||
|
const prevMonth = month === 0 ? 11 : month - 1;
|
||||||
|
const prevMonthYear = month === 0 ? year - 1 : year;
|
||||||
|
const prevMonthDays = getDaysInMonth(new Date(prevMonthYear, prevMonth, 1));
|
||||||
|
const prevMonthDaysArray = Array.from(
|
||||||
|
{ length: prevMonthDays },
|
||||||
|
(_, i) => i + 1
|
||||||
|
);
|
||||||
|
return { prevMonthDays, prevMonthDaysArray };
|
||||||
|
}, [month, year]);
|
||||||
|
|
||||||
|
// Memoize next month calculations
|
||||||
|
const nextMonthData = useMemo(() => {
|
||||||
|
const nextMonth = month === 11 ? 0 : month + 1;
|
||||||
|
const nextMonthYear = month === 11 ? year + 1 : year;
|
||||||
|
const nextMonthDays = getDaysInMonth(new Date(nextMonthYear, nextMonth, 1));
|
||||||
|
const nextMonthDaysArray = Array.from(
|
||||||
|
{ length: nextMonthDays },
|
||||||
|
(_, i) => i + 1
|
||||||
|
);
|
||||||
|
return { nextMonthDaysArray };
|
||||||
|
}, [month, year]);
|
||||||
|
|
||||||
|
// Memoize features filtering by day to avoid recalculating on every render
|
||||||
|
const featuresByDay = useMemo(() => {
|
||||||
|
const result: { [day: number]: Feature[] } = {};
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
result[day] = features.filter((feature) => {
|
||||||
|
return isSameDay(new Date(feature.endAt), new Date(year, month, day));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [features, daysInMonth, year, month]);
|
||||||
|
|
||||||
|
const days: ReactNode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < firstDay; i++) {
|
||||||
|
const day =
|
||||||
|
prevMonthData.prevMonthDaysArray[
|
||||||
|
prevMonthData.prevMonthDays - firstDay + i
|
||||||
|
];
|
||||||
|
|
||||||
|
if (day) {
|
||||||
|
days.push(<OutOfBoundsDay day={day} key={`prev-${i}`} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const featuresForDay = featuresByDay[day] || [];
|
||||||
|
const dayDate = new Date(year, month, day);
|
||||||
|
const isToday = isSameDay(dayDate, new Date());
|
||||||
|
const hasSelectedFeature = featuresForDay.some(
|
||||||
|
(feature) => feature.isSelected === true
|
||||||
|
);
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-full w-full flex-col gap-1 p-1 text-muted-foreground text-xs cursor-pointer transition-colors',
|
||||||
|
hasSelectedFeature && 'bg-red-500/20',
|
||||||
|
'hover:bg-primary/20 hover:text-primary'
|
||||||
|
)}
|
||||||
|
key={day}
|
||||||
|
onClick={() => {
|
||||||
|
onDateClick?.(dayDate, featuresForDay);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center w-6 h-6 rounded-full',
|
||||||
|
isToday &&
|
||||||
|
'ring-2 ring-blue-500 bg-blue-500/10 font-semibold text-blue-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{featuresForDay.slice(0, 3).map((feature) => children({ feature }))}
|
||||||
|
</div>
|
||||||
|
{featuresForDay.length > 3 && (
|
||||||
|
<span className="block text-muted-foreground text-xs">
|
||||||
|
+{featuresForDay.length - 3} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingDays = 7 - ((firstDay + daysInMonth) % 7);
|
||||||
|
if (remainingDays < 7) {
|
||||||
|
for (let i = 0; i < remainingDays; i++) {
|
||||||
|
const day = nextMonthData.nextMonthDaysArray[i];
|
||||||
|
|
||||||
|
if (day) {
|
||||||
|
days.push(<OutOfBoundsDay day={day} key={`next-${i}`} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid flex-grow grid-cols-7">
|
||||||
|
{days.map((day, index) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative aspect-square overflow-hidden border-t border-r',
|
||||||
|
index % 7 === 6 && 'border-r-0'
|
||||||
|
)}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarDatePickerProps = {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarDatePicker = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: CalendarDatePickerProps) => (
|
||||||
|
<div className={cn('flex items-center gap-1', className)}>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CalendarMonthPickerProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarMonthPicker = ({
|
||||||
|
className,
|
||||||
|
}: CalendarMonthPickerProps) => {
|
||||||
|
const [month, setMonth] = useCalendarMonth();
|
||||||
|
const { locale } = useContext(CalendarContext);
|
||||||
|
|
||||||
|
// Memoize month data to avoid recalculating date formatting
|
||||||
|
const monthData = useMemo(() => {
|
||||||
|
return monthsForLocale(locale).map((month, index) => ({
|
||||||
|
value: index.toString(),
|
||||||
|
label: month,
|
||||||
|
}));
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
className={className}
|
||||||
|
data={monthData}
|
||||||
|
labels={{
|
||||||
|
button: 'Select month',
|
||||||
|
empty: 'No month found',
|
||||||
|
search: 'Search month',
|
||||||
|
}}
|
||||||
|
setValue={(value) =>
|
||||||
|
setMonth(Number.parseInt(value, 10) as CalendarState['month'])
|
||||||
|
}
|
||||||
|
value={month.toString()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarYearPickerProps = {
|
||||||
|
className?: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarYearPicker = ({
|
||||||
|
className,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
}: CalendarYearPickerProps) => {
|
||||||
|
const [year, setYear] = useCalendarYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
className={className}
|
||||||
|
data={Array.from({ length: end - start + 1 }, (_, i) => ({
|
||||||
|
value: (start + i).toString(),
|
||||||
|
label: (start + i).toString(),
|
||||||
|
}))}
|
||||||
|
labels={{
|
||||||
|
button: 'Select year',
|
||||||
|
empty: 'No year found',
|
||||||
|
search: 'Search year',
|
||||||
|
}}
|
||||||
|
setValue={(value) => setYear(Number.parseInt(value, 10))}
|
||||||
|
value={year.toString()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarDatePaginationProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarDatePagination = ({
|
||||||
|
className,
|
||||||
|
}: CalendarDatePaginationProps) => {
|
||||||
|
const [month, setMonth] = useCalendarMonth();
|
||||||
|
const [year, setYear] = useCalendarYear();
|
||||||
|
|
||||||
|
const handlePreviousMonth = useCallback(() => {
|
||||||
|
if (month === 0) {
|
||||||
|
setMonth(11);
|
||||||
|
setYear(year - 1);
|
||||||
|
} else {
|
||||||
|
setMonth((month - 1) as CalendarState['month']);
|
||||||
|
}
|
||||||
|
}, [month, year, setMonth, setYear]);
|
||||||
|
|
||||||
|
const handleNextMonth = useCallback(() => {
|
||||||
|
if (month === 11) {
|
||||||
|
setMonth(0);
|
||||||
|
setYear(year + 1);
|
||||||
|
} else {
|
||||||
|
setMonth((month + 1) as CalendarState['month']);
|
||||||
|
}
|
||||||
|
}, [month, year, setMonth, setYear]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-2', className)}>
|
||||||
|
<Button onClick={handlePreviousMonth} size="icon" variant="ghost">
|
||||||
|
<ChevronLeftIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleNextMonth} size="icon" variant="ghost">
|
||||||
|
<ChevronRightIcon size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarDateProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarDate = ({ children }: CalendarDateProps) => (
|
||||||
|
<div className="flex items-center justify-between p-3">{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CalendarHeaderProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarHeader = ({ className }: CalendarHeaderProps) => {
|
||||||
|
const { locale, startDay } = useContext(CalendarContext);
|
||||||
|
|
||||||
|
// Memoize days data to avoid recalculating date formatting
|
||||||
|
const daysData = useMemo(() => {
|
||||||
|
return daysForLocale(locale, startDay);
|
||||||
|
}, [locale, startDay]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('grid flex-grow grid-cols-7', className)}>
|
||||||
|
{daysData.map((day) => (
|
||||||
|
<div className="p-3 text-right text-muted-foreground text-xs" key={day}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CalendarItemProps = {
|
||||||
|
feature: Feature;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarItem = memo(
|
||||||
|
({ feature, className }: CalendarItemProps) => (
|
||||||
|
<div className={cn('flex items-center gap-2', className)}>
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: feature.status.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="truncate">{feature.name}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CalendarItem.displayName = 'CalendarItem';
|
||||||
|
|
||||||
|
export type CalendarProviderProps = {
|
||||||
|
locale?: Intl.LocalesArgument;
|
||||||
|
startDay?: number;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarProvider = ({
|
||||||
|
locale = 'pt-BR',
|
||||||
|
startDay = 0,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: CalendarProviderProps) => (
|
||||||
|
<CalendarContext.Provider value={{ locale, startDay }}>
|
||||||
|
<div className={cn('relative flex flex-col', className)}>{children}</div>
|
||||||
|
</CalendarContext.Provider>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { Search } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
const Command = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
|
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||||
|
|
||||||
|
const CommandList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName
|
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
ref={ref}
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||||
|
|
||||||
|
const CommandSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const CommandItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const CommandShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CommandShortcut.displayName = "CommandShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent }
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
CircleCheck,
|
||||||
|
Info,
|
||||||
|
LoaderCircle,
|
||||||
|
OctagonX,
|
||||||
|
TriangleAlert,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner } from "sonner"
|
||||||
|
|
||||||
|
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
position="top-center"
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: <CircleCheck className="h-4 w-4" />,
|
||||||
|
info: <Info className="h-4 w-4" />,
|
||||||
|
warning: <TriangleAlert className="h-4 w-4" />,
|
||||||
|
error: <OctagonX className="h-4 w-4" />,
|
||||||
|
loading: <LoaderCircle className="h-4 w-4 animate-spin" />,
|
||||||
|
}}
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton:
|
||||||
|
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton:
|
||||||
|
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import 'dotenv/config';
|
||||||
|
import oracledb from 'oracledb';
|
||||||
|
|
||||||
|
// Usar modo Thin (padrão no node-oracledb 6.x)
|
||||||
|
// O modo Thin não requer binários nativos e funciona melhor no Next.js
|
||||||
|
// Se precisar usar Thick mode, configure ORACLE_LIB_DIR e descomente abaixo:
|
||||||
|
if (process.env.ORACLE_LIB_DIR) {
|
||||||
|
oracledb.initOracleClient({ libDir: process.env.ORACLE_LIB_DIR });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuração do pool de conexões Oracle
|
||||||
|
const oracleConfig = {
|
||||||
|
user: process.env.ORACLE_USER,
|
||||||
|
password: process.env.ORACLE_PASSWORD,
|
||||||
|
connectString: process.env.ORACLE_CONNECTION_STRING,
|
||||||
|
poolMin: Number(process.env.ORACLE_POOL_MIN) || 1,
|
||||||
|
poolMax: Number(process.env.ORACLE_POOL_MAX) || 30,
|
||||||
|
poolIncrement: Number(process.env.ORACLE_POOL_INCREMENT) || 1,
|
||||||
|
queueTimeout: Number(process.env.ORACLE_QUEUE_TIMEOUT) || 60000,
|
||||||
|
poolTimeout: Number(process.env.ORACLE_INACTIVITY_TIMEOUT) || 20000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Criar pool de conexões
|
||||||
|
let pool: oracledb.Pool | null = null;
|
||||||
|
|
||||||
|
export async function getOraclePool(): Promise<oracledb.Pool> {
|
||||||
|
if (!pool) {
|
||||||
|
try {
|
||||||
|
pool = await oracledb.createPool(oracleConfig);
|
||||||
|
console.log('✅ Pool Oracle criado com sucesso');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao criar pool Oracle:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeOracleQuery(
|
||||||
|
sql: string,
|
||||||
|
binds: any[] | Record<string, any> = []
|
||||||
|
): Promise<any[]> {
|
||||||
|
const pool = await getOraclePool();
|
||||||
|
let connection: oracledb.Connection | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
const result = await connection.execute(sql, binds, {
|
||||||
|
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
||||||
|
autoCommit: true, // Commit automático
|
||||||
|
});
|
||||||
|
return result.rows || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao executar query Oracle:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeOracleUpdate(
|
||||||
|
sql: string,
|
||||||
|
binds: any[] | Record<string, any> = []
|
||||||
|
): Promise<number> {
|
||||||
|
const pool = await getOraclePool();
|
||||||
|
let connection: oracledb.Connection | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
console.log(
|
||||||
|
'🔍 Executando UPDATE com binds:',
|
||||||
|
JSON.stringify(binds, null, 2)
|
||||||
|
);
|
||||||
|
const result = await connection.execute(sql, binds, {
|
||||||
|
autoCommit: true, // Commit automático para UPDATE
|
||||||
|
});
|
||||||
|
const rowsAffected = result.rowsAffected || 0;
|
||||||
|
console.log(`✅ UPDATE executado: ${rowsAffected} linha(s) afetada(s)`);
|
||||||
|
if (rowsAffected === 0) {
|
||||||
|
console.warn(
|
||||||
|
'⚠️ ATENÇÃO: Nenhuma linha foi afetada pelo UPDATE. Verifique:'
|
||||||
|
);
|
||||||
|
console.warn(' - Se a data existe na tabela PCDIASUTEIS');
|
||||||
|
console.warn(' - Se CODFILIAL = 12 corresponde ao registro');
|
||||||
|
console.warn(' - Se o formato da data está correto');
|
||||||
|
}
|
||||||
|
return rowsAffected;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao executar UPDATE Oracle:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeOraclePool(): Promise<void> {
|
||||||
|
if (pool) {
|
||||||
|
await pool.close();
|
||||||
|
pool = null;
|
||||||
|
console.log('✅ Pool Oracle fechado');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getOraclePool,
|
||||||
|
executeOracleQuery,
|
||||||
|
executeOracleUpdate,
|
||||||
|
closeOraclePool,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
chart: {
|
||||||
|
'1': 'hsl(var(--chart-1))',
|
||||||
|
'2': 'hsl(var(--chart-2))',
|
||||||
|
'3': 'hsl(var(--chart-3))',
|
||||||
|
'4': 'hsl(var(--chart-4))',
|
||||||
|
'5': 'hsl(var(--chart-5))'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target": "ES2017"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue