diff --git a/src/app/(protected)/(administrativo)/administrativo/financeiro/emolumentos/[emolumentoId]/itens/page.tsx b/src/app/(protected)/(administrativo)/administrativo/financeiro/emolumentos/[emolumentoId]/[emolumentoPeriodoId]/itens/page.tsx similarity index 83% rename from src/app/(protected)/(administrativo)/administrativo/financeiro/emolumentos/[emolumentoId]/itens/page.tsx rename to src/app/(protected)/(administrativo)/administrativo/financeiro/emolumentos/[emolumentoId]/[emolumentoPeriodoId]/itens/page.tsx index 163aefe..66406ff 100644 --- a/src/app/(protected)/(administrativo)/administrativo/financeiro/emolumentos/[emolumentoId]/itens/page.tsx +++ b/src/app/(protected)/(administrativo)/administrativo/financeiro/emolumentos/[emolumentoId]/[emolumentoPeriodoId]/itens/page.tsx @@ -11,6 +11,7 @@ export default function GGramaticaPage() { return ( ); diff --git a/src/packages/administrativo/components/GEmolumento/GEmolumentoColumns.tsx b/src/packages/administrativo/components/GEmolumento/GEmolumentoColumns.tsx index 827890a..e2d563f 100644 --- a/src/packages/administrativo/components/GEmolumento/GEmolumentoColumns.tsx +++ b/src/packages/administrativo/components/GEmolumento/GEmolumentoColumns.tsx @@ -19,8 +19,11 @@ import GEmolumentoInterface from '../../interfaces/GEmolumento/GEmolumentoInterf export default function GEmolumentoColumns( onEdit: (item: GEmolumentoInterface, isEditingFormStatus: boolean) => void, onDelete: (item: GEmolumentoInterface, isEditingFormStatus: boolean) => void, + emolumentoPeriodoId: (id: number) => void, ): ColumnDef[] { + return [ + // ID { accessorKey: 'emolumento_id', @@ -57,7 +60,7 @@ export default function GEmolumentoColumns( - + Itens diff --git a/src/packages/administrativo/components/GEmolumento/GEmolumentoIndex.tsx b/src/packages/administrativo/components/GEmolumento/GEmolumentoIndex.tsx index 6d5c709..e3994e3 100644 --- a/src/packages/administrativo/components/GEmolumento/GEmolumentoIndex.tsx +++ b/src/packages/administrativo/components/GEmolumento/GEmolumentoIndex.tsx @@ -31,11 +31,21 @@ export default function GEmolumentoIndex() { // Estado para saber qual item será deletado const [itemToDelete, setItemToDelete] = useState(null); + // Estado para controlar o período selecionado + const [getEmolumentoPeriodoId, setEmolumentoPeriodoId] = useState(0); + /** * Hook do modal de confirmação */ const { isOpen: isConfirmOpen, openDialog: openConfirmDialog, handleCancel } = useConfirmDialog(); + /** + * Executa consulta ao selecionar um período + */ + const handleSelectedEmolumentoPeriodo = (emolumentoPeriodoId: number) => { + setEmolumentoPeriodoId(emolumentoPeriodoId); + }; + /** * Abre o formulário no modo de edição ou criação */ @@ -131,7 +141,13 @@ export default function GEmolumentoIndex() { }} /> {/* Tabela de andamentos */} - + {/* Modal de confirmação */} {isConfirmOpen && ( + ); -} +} \ No newline at end of file diff --git a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemColumns.tsx b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemColumns.tsx index 04ca78a..6b91402 100644 --- a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemColumns.tsx +++ b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemColumns.tsx @@ -22,17 +22,31 @@ export default function GEmolumentoItemColumns( return [ // ID { - accessorKey: 'emolumento_id', + accessorKey: 'emolumento_item_id', header: ({ column }) => SortableHeader('ID', column), - cell: ({ row }) => Number(row.getValue('emolumento_id')), + cell: ({ row }) => Number(row.getValue('emolumento_item_id')), enableSorting: true, }, - // Descrição + // Valor Início { - accessorKey: 'descricao', - header: ({ column }) => SortableHeader('Descrição', column), - cell: ({ row }) => GetCapitalize(String(row.getValue('descricao') || '')), + accessorKey: 'valor_inicio', + header: ({ column }) => SortableHeader('Valor Inicial', column), + cell: ({ row }) => GetCapitalize(String(row.getValue('valor_inicio') || '')), + }, + + // Valor Fim + { + accessorKey: 'valor_fim', + header: ({ column }) => SortableHeader('Valor Final', column), + cell: ({ row }) => GetCapitalize(String(row.getValue('valor_fim') || '')), + }, + + // Taxa Judiciárioa + { + accessorKey: 'valor_taxa_judiciaria', + header: ({ column }) => SortableHeader('Taxa Judiciária', column), + cell: ({ row }) => GetCapitalize(String(row.getValue('valor_taxa_judiciaria') || '')), }, // Ações diff --git a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemForm.tsx b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemForm.tsx index 495d3bf..f6bcb83 100644 --- a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemForm.tsx +++ b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { HouseIcon, IdCardIcon } from 'lucide-react'; +import { HouseIcon } from 'lucide-react'; import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; @@ -23,6 +23,7 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { parseNumberInput } from '@/shared/actions/form/parseNumberInput'; import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData'; import LoadingButton from '@/shared/components/loadingButton/LoadingButton'; @@ -40,6 +41,8 @@ export default function GEmolumentoItemForm({ onSave, buttonIsLoading, }: GEmolumentoItemFormInterface) { + + // Inicializa o formulário const form = useGEmolumentoItemFormHook({}); // Atualiza o formulário quando recebe dados para edição @@ -62,7 +65,7 @@ export default function GEmolumentoItemForm({ Emolumento - Formulário de Emolumento + Formulário de Itens de Emolumento {/* Formulário principal */} @@ -72,48 +75,67 @@ export default function GEmolumentoItemForm({ - Geral - - - - Registro de Imóveis + Valores {/* GRID MOBILE FIRST */}
{/* Descrição */} -
+
( - Descrição + Valor Início - + field.onChange(parseNumberInput(e))} + /> + + + + )} + /> +
+
+ ( + + Valor Fim + + field.onChange(parseNumberInput(e))} + /> + + + + )} + /> +
+
+ ( + + Valor Taxa Judiciária + + field.onChange(parseNumberInput(e))} + /> )} />
-
- - - {/* Pré-Definido */} -
- ( - - Pré-Definido - - - - )} - />
diff --git a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemIndex.tsx b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemIndex.tsx index 082cb7f..fa2e06f 100644 --- a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemIndex.tsx +++ b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemIndex.tsx @@ -1,20 +1,27 @@ 'use client'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; -import { useGEmolumentoItemDeleteHook } from '@/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemDeleteHook'; -import { useGEmolumentoItemIndexHook } from '@/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemIndexHook'; -import { useGEmolumentoItemSaveHook } from '@/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemSaveHook'; import GEmolumentoItemIndexInterface from '@/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemIndexInterface'; -import GEmolumentoItemInterface from '@/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemInterface'; import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog'; import { useConfirmDialog } from '@/shared/components/confirmDialog/useConfirmDialog'; +import Loading from '@/shared/components/loading/loading'; import Header from '@/shared/components/structure/Header'; +import { useGEmolumentoItemDeleteHook } from '../../hooks/GEmolumentoItem/useGEmolumentoItemDeleteHook'; +import { useGEmolumentoItemIndexHook } from '../../hooks/GEmolumentoItem/useGEmolumentoItemIndexHook'; +import { useGEmolumentoItemSaveHook } from '../../hooks/GEmolumentoItem/useGEmolumentoItemSaveHook'; +import GEmolumentoItemInterface from '../../interfaces/GEmolumentoItem/GEmolumentoItemInterface'; import GEmolumentoItemForm from './GEmolumentoItemForm'; import GEmolumentoItemTable from './GEmolumentoItemTable'; -export default function GEmolumentoItemIndex({ emolumento_id }: GEmolumentoItemIndexInterface) { +export default function GEmolumentoItemIndex({ emolumento_id, emolumento_periodo_id }: GEmolumentoItemIndexInterface) { + + const gEmolumentoItemParams = { + emolumento_id: emolumento_id, + emolumento_periodo_id: emolumento_periodo_id + } + // Controle de estado do botão const [buttonIsLoading, setButtonIsLoading] = useState(false); @@ -23,25 +30,25 @@ export default function GEmolumentoItemIndex({ emolumento_id }: GEmolumentoItemI const { saveGEmolumentoItem } = useGEmolumentoItemSaveHook(); const { deleteGEmolumentoItem } = useGEmolumentoItemDeleteHook(); - // Estados do componente + // Estados const [selectedData, setSelectedData] = useState(null); const [isFormOpen, setIsFormOpen] = useState(false); + + // Estado para saber qual item será deletado const [itemToDelete, setItemToDelete] = useState(null); - // Estado para controlar o período selecionado - const [getEmolumentoPeriodoId, setEmolumentoPeriodoId] = useState(null); - - // Hook do modal de confirmação + /** + * Hook do modal de confirmação + */ const { isOpen: isConfirmOpen, openDialog: openConfirmDialog, handleCancel } = useConfirmDialog(); - // Flag para evitar duplas execuções em modo Strict - const isMounted = useRef(false); - /** * Abre o formulário no modo de edição ou criação */ const handleOpenForm = useCallback((data: GEmolumentoItemInterface | null) => { - setSelectedData(data); + // Se não houver dados (criação), cria um objeto inicial com imovel_id + const initialData: GEmolumentoItemInterface = data ?? ({ emolumento_id, emolumento_periodo_id } as GEmolumentoItemInterface); + setSelectedData(initialData); setIsFormOpen(true); }, []); @@ -53,51 +60,24 @@ export default function GEmolumentoItemIndex({ emolumento_id }: GEmolumentoItemI setIsFormOpen(false); }, []); - /** - * Sempre que o emolumento_id ou o período mudar, busca novamente os dados. - * Evita loop infinito e só dispara quando necessário. - */ - useEffect(() => { - if (!emolumento_id) return; - - // Evita execução duplicada no modo Strict - if (isMounted.current) { - indexGEmolumentoItem({ - emolumento_id, - emolumento_periodo_id: getEmolumentoPeriodoId ?? 0, - }); - } else { - isMounted.current = true; - indexGEmolumentoItem({ - emolumento_id, - emolumento_periodo_id: getEmolumentoPeriodoId ?? 0, - }); - } - }, [emolumento_id, getEmolumentoPeriodoId]); - - /** - * Executa consulta ao selecionar um período - */ - const handleSelectedEmolumentoPeriodo = (emolumentoPeriodoId: number) => { - setEmolumentoPeriodoId(emolumentoPeriodoId); // 🔥 Isso dispara o useEffect acima - }; - /** * Salva os dados do formulário */ const handleSave = useCallback( async (formData: GEmolumentoItemInterface) => { + // Coloca o botão em estado de loading setButtonIsLoading(true); + + // Aguarda salvar o registro await saveGEmolumentoItem(formData); + + // Remove o botão em estado de loading setButtonIsLoading(false); - // Recarrega a lista usando os valores atuais - await indexGEmolumentoItem({ - emolumento_id, - emolumento_periodo_id: getEmolumentoPeriodoId ?? 0, - }); + // Atualiza a lista de dados + indexGEmolumentoItem(gEmolumentoItemParams); }, - [saveGEmolumentoItem, indexGEmolumentoItem, emolumento_id, getEmolumentoPeriodoId] + [saveGEmolumentoItem, indexGEmolumentoItem, handleCloseForm], ); /** @@ -105,25 +85,47 @@ export default function GEmolumentoItemIndex({ emolumento_id }: GEmolumentoItemI */ const handleConfirmDelete = useCallback( (item: GEmolumentoItemInterface) => { + // Define o item atual para remoção setItemToDelete(item); + // Abre o modal de confirmação openConfirmDialog(); }, - [openConfirmDialog] + [openConfirmDialog], ); /** * Executa a exclusão de fato quando o usuário confirma */ const handleDelete = useCallback(async () => { + // Protege contra null if (!itemToDelete) return; + + // Executa o Hook de remoção await deleteGEmolumentoItem(itemToDelete); - await indexGEmolumentoItem({ - emolumento_id, - emolumento_periodo_id: getEmolumentoPeriodoId ?? 0, - }); + + // Atualiza a lista + await indexGEmolumentoItem(gEmolumentoItemParams); + + // Limpa o item selecionado setItemToDelete(null); + + // Fecha o modal handleCancel(); - }, [itemToDelete, deleteGEmolumentoItem, indexGEmolumentoItem, emolumento_id, getEmolumentoPeriodoId, handleCancel]); + }, [itemToDelete, indexGEmolumentoItem, handleCancel]); + + /** + * Busca inicial dos dados + */ + useEffect(() => { + indexGEmolumentoItem(gEmolumentoItemParams); + }, []); + + /** + * Tela de loading enquanto carrega os dados + */ + if (gEmolumentoItem?.length == 0) { + return ; + } return (
@@ -140,7 +142,6 @@ export default function GEmolumentoItemIndex({ emolumento_id }: GEmolumentoItemI data={gEmolumentoItem} onEdit={handleOpenForm} onDelete={handleConfirmDelete} - onSelectedEmolumentoPeriodo={handleSelectedEmolumentoPeriodo} /> {/* Modal de confirmação */} diff --git a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemTable.tsx b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemTable.tsx index 0a6a992..567e6c2 100644 --- a/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemTable.tsx +++ b/src/packages/administrativo/components/GEmolumentoItem/GEmolumentoItemTable.tsx @@ -2,24 +2,20 @@ import { DataTable } from '@/shared/components/dataTable/DataTable'; -import GEmolumentoItemColumns from './GEmolumentoItemColumns'; import GEmolumentoItemTableInterface from '../../interfaces/GEmolumentoItem/GEmolumentoItemTableInterface'; -import GEmolumentoPeriodoSelect from '../GEmolumentoPeriodo/GEmolumentoPeriodoSelect'; +import GEmolumentoItemColumns from './GEmolumentoItemColumns'; /** * Componente principal da tabela de Naturezas */ -export default function GEmolumentoItemTable({ data, onEdit, onDelete, onSelectedEmolumentoPeriodo }: GEmolumentoItemTableInterface) { +export default function GEmolumentoItemTable({ data, onEdit, onDelete }: GEmolumentoItemTableInterface) { const columns = GEmolumentoItemColumns(onEdit, onDelete); return (
-
diff --git a/src/packages/administrativo/data/GEmolumentoItem/GEmolumentoItemIndexData.ts b/src/packages/administrativo/data/GEmolumentoItem/GEmolumentoItemIndexData.ts index 36d2795..1ad0c87 100644 --- a/src/packages/administrativo/data/GEmolumentoItem/GEmolumentoItemIndexData.ts +++ b/src/packages/administrativo/data/GEmolumentoItem/GEmolumentoItemIndexData.ts @@ -9,7 +9,7 @@ async function executeGEmolumentoItemIndexData(data: GEmolumentoItemIndexInterfa const api = new API(); return api.send({ method: Methods.GET, - endpoint: `administrativo/g_emolumento_item/${data.emolumento_id}/1`, + endpoint: `administrativo/g_emolumento_item/${data.emolumento_id}/${data.emolumento_periodo_id}`, }); } diff --git a/src/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemFormHook.ts b/src/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemFormHook.ts index ed2cda7..63a6227 100644 --- a/src/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemFormHook.ts +++ b/src/packages/administrativo/hooks/GEmolumentoItem/useGEmolumentoItemFormHook.ts @@ -1,13 +1,18 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; -import { GEmolumentoItemFormValues, GEmolumentoItemSchema } from '../../schemas/GEmolumentoItem/GEmolumentoItemSchema'; +import { + GEmolumentoItemFormValues, + GEmolumentoItemSchema, +} from '../../schemas/GEmolumentoItem/GEmolumentoItemSchema'; export function useGEmolumentoItemFormHook(defaults?: Partial) { return useForm({ resolver: zodResolver(GEmolumentoItemSchema), defaultValues: { + emolumento_item_id: 0, emolumento_id: 0, + emolumento_periodo_id: 0, ...defaults, }, }); diff --git a/src/packages/administrativo/interfaces/GEmolumento/GEmolumentoTableInterface.ts b/src/packages/administrativo/interfaces/GEmolumento/GEmolumentoTableInterface.ts index aa9e7a6..e58e72e 100644 --- a/src/packages/administrativo/interfaces/GEmolumento/GEmolumentoTableInterface.ts +++ b/src/packages/administrativo/interfaces/GEmolumento/GEmolumentoTableInterface.ts @@ -4,4 +4,6 @@ export default interface GEmolumentoTableInterface { data?: GEmolumentoInterface[]; onEdit: (item: GEmolumentoInterface, isEditingFormStatus: boolean) => void; onDelete: (item: GEmolumentoInterface, isEditingFormStatus: boolean) => void; -} + onSelectedEmolumentoPeriodo: (emolumentoPeriodId: number) => void; + emolumentoPeriodoId: (id: number) => void; +} \ No newline at end of file diff --git a/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemInterface.ts b/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemInterface.ts index 3830973..253b178 100644 --- a/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemInterface.ts +++ b/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemInterface.ts @@ -1,13 +1,31 @@ export default interface GEmolumentoItemInterface { - emolumento_item_id?: number; - emolumento_id?: number; - descricao?: string; - tipo?: string; - tipo_lancamento?: string; - reg_averb?: string; - valor_fixo?: number; - percentual?: number; - situacao?: string; - emolumento_item_ref?: number; - observacao?: string; + valor_emolumento?: number, + emolumento_item_id?: number, + emolumento_id?: number, + valor_inicio?: number, + valor_fim?: number, + valor_taxa_judiciaria?: number, + emolumento_periodo_id?: number, + codigo?: number, + pagina_extra?: number, + valor_pagina_extra?: number, + valor_outra_taxa1?: number, + codigo_selo?: string, + valor_fundo_ri?: number, + codigo_tabela?: string, + selo_grupo_id?: number, + codigo_km?: string, + emolumento_acresce?: number, + taxa_acresce?: number, + funcivil_acresce?: number, + valor_fracao?: number, + valor_por_excedente_emol?: number, + valor_por_excedente_tj?: number, + valor_por_excedente_fundo?: number, + valor_limite_excedente_emol?: number, + valor_limite_excedente_tj?: number, + valor_limite_excedente_fundo?: number, + fundo_selo?: number, + distribuicao?: number, + vrcext?: number, } diff --git a/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemTableInterface.ts b/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemTableInterface.ts index bd90443..bb2d64e 100644 --- a/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemTableInterface.ts +++ b/src/packages/administrativo/interfaces/GEmolumentoItem/GEmolumentoItemTableInterface.ts @@ -4,5 +4,4 @@ export default interface GEmolumentoItemTableInterface { data?: GEmolumentoItemInterface[]; onEdit: (item: GEmolumentoItemInterface, isEditingFormStatus: boolean) => void; onDelete: (item: GEmolumentoItemInterface, isEditingFormStatus: boolean) => void; - onSelectedEmolumentoPeriodo: (emolumentoPeriodoId: number) => void; } diff --git a/src/packages/administrativo/schemas/GEmolumentoItem/GEmolumentoItemSchema.ts b/src/packages/administrativo/schemas/GEmolumentoItem/GEmolumentoItemSchema.ts index 50b14f6..5d87e56 100644 --- a/src/packages/administrativo/schemas/GEmolumentoItem/GEmolumentoItemSchema.ts +++ b/src/packages/administrativo/schemas/GEmolumentoItem/GEmolumentoItemSchema.ts @@ -1,17 +1,41 @@ import z from "zod"; export const GEmolumentoItemSchema = z.object({ - emolumento_item_id: z.number().optional(), - emolumento_id: z.number().optional(), - descricao: z.string().max(260).optional(), - tipo: z.string().max(1).optional(), - tipo_lancamento: z.string().max(1).optional(), - reg_averb: z.string().max(1).optional(), - valor_fixo: z.number().optional(), - percentual: z.number().optional(), - situacao: z.string().max(1).optional(), - emolumento_item_ref: z.number().optional(), - observacao: z.string().max(260).optional(), + emolumento_item_id: z.coerce.number().optional(), + emolumento_id: z.coerce.number().optional(), + emolumento_periodo_id: z.coerce.number().optional(), + selo_grupo_id: z.coerce.number().optional(), + + codigo: z.coerce.number().min(0).optional(), + codigo_tabela: z.string().max(30).optional(), + codigo_selo: z.string().max(30).optional(), + codigo_km: z.string().max(30).optional(), + + valor_emolumento: z.coerce.number().min(0).optional(), + valor_inicio: z.coerce.number().min(0).optional(), + valor_fim: z.coerce.number().min(0).optional(), + valor_taxa_judiciaria: z.coerce.number().min(0).optional(), + valor_fundo_ri: z.coerce.number().min(0).optional(), + valor_outra_taxa1: z.coerce.number().min(0).optional(), + valor_pagina_extra: z.coerce.number().min(0).optional(), + pagina_extra: z.coerce.number().min(0).optional(), + + emolumento_acresce: z.coerce.number().min(0).optional(), + taxa_acresce: z.coerce.number().min(0).optional(), + funcivil_acresce: z.coerce.number().min(0).optional(), + + valor_fracao: z.coerce.number().min(0).optional(), + valor_por_excedente_emol: z.coerce.number().min(0).optional(), + valor_por_excedente_tj: z.coerce.number().min(0).optional(), + valor_por_excedente_fundo: z.coerce.number().min(0).optional(), + + valor_limite_excedente_emol: z.coerce.number().min(0).optional(), + valor_limite_excedente_tj: z.coerce.number().min(0).optional(), + valor_limite_excedente_fundo: z.coerce.number().min(0).optional(), + + fundo_selo: z.coerce.number().min(0).optional(), + distribuicao: z.coerce.number().min(0).optional(), + vrcext: z.coerce.number().min(0).optional(), }); -export type GEmolumentoItemFormValues = z.infer; +export type GEmolumentoItemFormValues = z.infer; \ No newline at end of file