Clientes y facturas de cliente

This commit is contained in:
David Arranz 2025-09-22 11:24:19 +02:00
parent 5f2afd0520
commit a2b5c96cd7
8 changed files with 142 additions and 127 deletions

View File

@ -45,14 +45,14 @@ export class CustomerInvoiceReportPDFPresenter extends Presenter<
right: "10mm", right: "10mm",
top: "10mm", top: "10mm",
}, },
landscape: false, // landscape: false,
preferCSSPageSize: true, // preferCSSPageSize: true,
omitBackground: false, // omitBackground: false,
printBackground: true, // printBackground: true,
displayHeaderFooter: true, // displayHeaderFooter: false,
headerTemplate: "<div />", // headerTemplate: "<div />",
footerTemplate: // footerTemplate:
'<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></div>', // '<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></div>',
}); });
await browser.close(); await browser.close();

View File

@ -175,7 +175,7 @@
<td>{{description}}</td> <td>{{description}}</td>
<td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if total_amount}}{{total_amount}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if subtotal_amount}}{{subtotal_amount}}{{else}}&nbsp;{{/if}}</td>
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

View File

@ -110,15 +110,17 @@
<body> <body>
<header> <header id="header">
<aside class="flex items-start mb-4 w-full"> <aside class="flex items-start mb-4 w-full bg-red-600">
<!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda --> <!-- Bloque IZQUIERDO: imagen arriba + texto abajo, alineado a la izquierda -->
<div class="w-[70%] flex flex-col items-start text-left"> <div class="flex flex-col 70% items-start text-left bg-green-700">
<img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" /> <img src="https://rodax-software.com/images/logo1.jpg" alt="Logo Rodax" class="block h-14 w-auto mb-1" />
<div class="flex w-full"> <div class="flex w-full">
<div class="p-1 "> <div class="p-1 ">
<p>Factura nº:<strong>&nbsp;{{invoice_number}}</strong></p> <h3 class="text-2xl font-normal">PROFORMA</h3>
<p>Nº:<strong>&nbsp;{{invoice_number}}</strong></p>
<p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p> <p><span>Fecha:<strong>&nbsp;{{invoice_date}}</strong></p>
<p>Página <span class="pageNumber"></span> de <span class="totalPages"></span></p>
</div> </div>
<div class="p-1 ml-9"> <div class="p-1 ml-9">
<h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2> <h2 class="font-semibold uppercase mb-1">{{recipient.name}}</h2>
@ -145,7 +147,6 @@
<main id="main"> <main id="main">
<section id="details" class="border-b border-black "> <section id="details" class="border-b border-black ">
<div class="relative pt-0 border-b border-black"> <div class="relative pt-0 border-b border-black">
<!-- Badge TOTAL decorado con imagen --> <!-- Badge TOTAL decorado con imagen -->
<div class="absolute -top-9 right-0"> <div class="absolute -top-9 right-0">
@ -175,7 +176,7 @@
<td>{{description}}</td> <td>{{description}}</td>
<td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if total_amount}}{{total_amount}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if subtotal_amount}}{{subtotal_amount}}{{else}}&nbsp;{{/if}}</td>
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

View File

@ -1,14 +1,12 @@
import { import {
AudioWaveform, AudioWaveform,
BookOpen,
Bot,
Command, Command,
FileCheckIcon,
Frame, Frame,
GalleryVerticalEnd, GalleryVerticalEnd,
HomeIcon,
MapIcon, MapIcon,
PieChart, PieChart,
Settings2,
SquareTerminal,
} from "lucide-react"; } from "lucide-react";
import { import {
@ -23,18 +21,22 @@ import {
HelpCircleIcon, HelpCircleIcon,
LayoutDashboardIcon, LayoutDashboardIcon,
ListIcon, ListIcon,
SearchIcon,
SettingsIcon, SettingsIcon,
UsersIcon, UsersIcon,
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from "@repo/shadcn-ui/components"; import {
import { NavDocuments } from "./nav-documents.tsx"; Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@repo/shadcn-ui/components";
import { NavMain } from "./nav-main.tsx"; import { NavMain } from "./nav-main.tsx";
import { NavProjects } from "./nav-projects.tsx";
import { NavSecondary } from "./nav-secondary.tsx"; import { NavSecondary } from "./nav-secondary.tsx";
import { NavUser } from "./nav-user.tsx"; import { NavUser } from "./nav-user.tsx";
import { SearchForm } from "./search-form.tsx";
import { TeamSwitcher } from "./team-switcher.tsx"; import { TeamSwitcher } from "./team-switcher.tsx";
const data = { const data = {
@ -129,11 +131,6 @@ const data = {
url: "#", url: "#",
icon: HelpCircleIcon, icon: HelpCircleIcon,
}, },
{
title: "Buscar",
url: "#",
icon: SearchIcon,
},
], ],
documents: [ documents: [
{ {
@ -179,88 +176,48 @@ const data2 = {
}, },
], ],
navMain: [ navMain: [
{
title: "Inicio",
url: "/",
icon: HomeIcon,
isActive: true,
},
{ {
title: "Clientes", title: "Clientes",
url: "/customers", icon: UsersIcon,
icon: SquareTerminal,
isActive: true, isActive: true,
items: [ items: [
{ {
title: "History", title: "Listado de clientes",
url: "#", url: "/customers",
}, },
{ {
title: "Starred", title: "Añadir un cliente",
url: "#", url: "/customers/create",
},
{
title: "Settings",
url: "#",
}, },
], ],
}, },
{ {
title: "Proformas de cliente", title: "Facturas proforma",
url: "/customer-proforma", icon: FileTextIcon,
icon: Bot,
items: [ items: [
{ {
title: "Genesis", title: "Listado de proformas",
url: "#", url: "/customer-proforma",
}, },
{ {
title: "Explorer", title: "Enviar a Veri*Factu",
url: "#",
},
{
title: "Quantum",
url: "#", url: "#",
}, },
], ],
}, },
{ {
title: "Facturas de cliente", title: "Facturas de cliente",
url: "/customer-invoices", icon: FileCheckIcon,
icon: BookOpen,
items: [ items: [
{ {
title: "Introduction", title: "Listado de facturas",
url: "#", url: "/customer-invoices",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
},
{
title: "Settings",
url: "#",
icon: Settings2,
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
}, },
], ],
}, },
@ -287,18 +244,18 @@ const data2 = {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return ( return (
<Sidebar collapsible='icon' {...props}> <Sidebar collapsible='icon' {...props}>
<SidebarHeader> <SidebarHeader className='mb-3'>
<TeamSwitcher teams={data2.teams} /> <TeamSwitcher teams={data2.teams} />
<SearchForm />
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={data2.navMain} /> <NavMain items={data2.navMain} />
<NavProjects projects={data2.projects} />
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className='mt-auto' />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavSecondary items={data.navSecondary} className='mt-auto' />
<NavUser user={data.user} /> <NavUser user={data.user} />
</SidebarFooter> </SidebarFooter>
<SidebarRail />
</Sidebar> </Sidebar>
); );
} }

View File

@ -1,23 +1,35 @@
import { type LucideIcon, MailIcon, PlusCircleIcon } from "lucide-react"; import { ChevronRightIcon, type LucideIcon, PlusCircleIcon } from "lucide-react";
import { import {
Button, Collapsible,
CollapsibleContent,
CollapsibleTrigger,
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
type NavMainItem = {
title: string;
url?: string;
icon?: LucideIcon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
};
export function NavMain({ export function NavMain({
items, items,
}: { }: {
items: { items: NavMainItem[];
title: string;
url: string;
icon?: LucideIcon;
}[];
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -35,29 +47,34 @@ export function NavMain({
<PlusCircleIcon /> <PlusCircleIcon />
<span>Quick Create</span> <span>Quick Create</span>
</SidebarMenuButton> </SidebarMenuButton>
<Button
size='icon'
className='h-9 w-9 shrink-0 group-data-[collapsible=icon]:opacity-0'
variant='outline'
>
<MailIcon />
<span className='sr-only'>Inbox</span>
</Button>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.map((item) => (
<SidebarMenuItem key={item.title}> <Collapsible key={item.title} asChild defaultOpen={true} className='group/collapsible'>
<SidebarMenuButton <SidebarMenuItem className='mb-6'>
isActive={String(window.location.href).includes(item.url)} <CollapsibleTrigger asChild>
tooltip={item.title} <SidebarMenuButton tooltip={item.title}>
onClick={() => navigate(item.url)} {item.icon && <item.icon />}
className='data-[active=true]:bg-accent data-[active=true]:text-accent-foreground cursor-pointer' <span className='font-semibold'>{item.title}</span>
> <ChevronRightIcon className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
{item.icon && <item.icon />} </SidebarMenuButton>
<span>{item.title}</span> </CollapsibleTrigger>
</SidebarMenuButton> <CollapsibleContent>
</SidebarMenuItem> <SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))} ))}
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>

View File

@ -0,0 +1,38 @@
import {
Button,
Label,
SidebarGroup,
SidebarGroupContent,
SidebarInput,
SidebarMenu,
SidebarMenuItem,
} from "@repo/shadcn-ui/components";
import { BinocularsIcon, SearchIcon } from "lucide-react";
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
return (
<form {...props}>
<SidebarGroup className='py-0'>
<SidebarGroupContent className='relative'>
<SidebarMenu>
<SidebarMenuItem className='flex items-center gap-2'>
<Label htmlFor='search' className='sr-only'>
Search
</Label>
<SidebarInput id='search' placeholder='Search...' className='pl-8 h-9' />
<SearchIcon className='pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none ' />
<Button
size='icon'
className='h-9 w-9 shrink-0 group-data-[collapsible=icon]:opacity-0'
variant='outline'
>
<BinocularsIcon />
<span className='sr-only'>Advanced search</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</form>
);
}

View File

@ -1,13 +1,13 @@
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"; import { TrendingDownIcon, TrendingUpIcon } from "lucide-react";
import { Badge } from "@repo/shadcn-ui/components/badge";
import { import {
Card, Card,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@repo/shadcn-ui/components/card"; } from "@repo/shadcn-ui/components";
import { Badge } from "@repo/shadcn-ui/components/badge";
export function SectionCards() { export function SectionCards() {
return ( return (

View File

@ -1,3 +1,5 @@
"use client";
import { ChevronsUpDown, Plus } from "lucide-react"; import { ChevronsUpDown, Plus } from "lucide-react";
import * as React from "react"; import * as React from "react";
@ -40,31 +42,31 @@ export function TeamSwitcher({
size='lg' size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground' className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
> >
<div className='flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground'> <div className='bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg'>
<activeTeam.logo className='size-4' /> <activeTeam.logo className='size-4' />
</div> </div>
<div className='grid flex-1 text-left text-sm leading-tight'> <div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{activeTeam.name}</span> <span className='truncate font-medium'>{activeTeam.name}</span>
<span className='truncate text-xs'>{activeTeam.plan}</span> <span className='truncate text-xs'>{activeTeam.plan}</span>
</div> </div>
<ChevronsUpDown className='ml-auto' /> <ChevronsUpDown className='ml-auto' />
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className='w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg' className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
align='start' align='start'
side={isMobile ? "bottom" : "right"} side={isMobile ? "bottom" : "right"}
sideOffset={4} sideOffset={4}
> >
<DropdownMenuLabel className='text-xs text-muted-foreground'>Teams</DropdownMenuLabel> <DropdownMenuLabel className='text-muted-foreground text-xs'>Teams</DropdownMenuLabel>
{teams.map((team, index) => ( {teams.map((team, index) => (
<DropdownMenuItem <DropdownMenuItem
key={team.name} key={team.name}
onClick={() => setActiveTeam(team)} onClick={() => setActiveTeam(team)}
className='gap-2 p-2' className='gap-2 p-2'
> >
<div className='flex size-6 items-center justify-center rounded-sm border'> <div className='flex size-6 items-center justify-center rounded-md border'>
<team.logo className='size-4 shrink-0' /> <team.logo className='size-3.5 shrink-0' />
</div> </div>
{team.name} {team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut> <DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
@ -72,10 +74,10 @@ export function TeamSwitcher({
))} ))}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem className='gap-2 p-2'> <DropdownMenuItem className='gap-2 p-2'>
<div className='flex size-6 items-center justify-center rounded-md border bg-background'> <div className='flex size-6 items-center justify-center rounded-md border bg-transparent'>
<Plus className='size-4' /> <Plus className='size-4' />
</div> </div>
<div className='font-medium text-muted-foreground'>Add team</div> <div className='text-muted-foreground font-medium'>Add team</div>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>