feat: implement US-2 - create print route with barcode data fetching

- Add Next.js print route at /[locale]/print/[year]/[month]
- Implement fetchBarcodeDataForPrint function with user authentication
- Create PrintPreview component with basic table layout
- Add proper 404 handling with not-found.tsx
- Fix barcode image display with flexible data URL handling
- Filter and sort bills with barcode data for consistent display

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-14 19:57:13 +02:00
parent ecea10e805
commit 493c2f62fa
4 changed files with 179 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
import { NotFoundPage } from '@/app/ui/NotFoundPage';
const PrintPageNotFound = () =>
<NotFoundPage title="404 Print Page Not Found" description="Could not find the requested print preview page." />;
export default PrintPageNotFound;

View File

@@ -0,0 +1,46 @@
import { fetchBarcodeDataForPrint } from '@/app/lib/actions/printActions';
import { notFound } from 'next/navigation';
import { PrintPreview } from '@/app/ui/PrintPreview';
interface PrintPageProps {
params: {
year: string;
month: string;
locale: string;
};
}
export default async function PrintPage({ params }: PrintPageProps) {
const year = parseInt(params.year);
const month = parseInt(params.month);
// Validate year and month parameters
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
notFound();
}
let printData;
try {
printData = await fetchBarcodeDataForPrint(year, month);
} catch (error) {
console.error('Error fetching print data:', error);
notFound();
}
// If no barcode data found, show empty state
if (!printData || printData.length === 0) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">No Barcode Data Found</h1>
<p className="text-gray-600">
No bills with 2D barcodes found for {year}-{month.toString().padStart(2, '0')}
</p>
</div>
</div>
);
}
return <PrintPreview data={printData} year={year} month={month} />;
}

View File

@@ -0,0 +1,64 @@
'use server';
import { getDbClient } from '../dbClient';
import { BillingLocation, Bill } from '../db-types';
import { AuthenticatedUser } from '../types/next-auth';
import { withUser } from '../auth';
import { unstable_noStore as noStore } from 'next/cache';
export interface PrintBarcodeData {
locationName: string;
billName: string;
barcodeImage: string;
yearMonth: string;
}
/**
* Fetches all bills with barcode images for a specific month for printing
* @param year - Year to fetch data for
* @param month - Month to fetch data for (1-12)
* @returns Array of barcode data for printing
*/
export const fetchBarcodeDataForPrint = withUser(async (user: AuthenticatedUser, year: number, month: number): Promise<PrintBarcodeData[]> => {
noStore();
const { id: userId } = user;
const dbClient = await getDbClient();
const yearMonth = `${year}-${month.toString().padStart(2, '0')}`;
// Fetch all locations for the specific month
const locations = await dbClient.collection<BillingLocation>("lokacije")
.find({
userId, // ensure data belongs to authenticated user
"yearMonth.year": year,
"yearMonth.month": month
})
.toArray();
// Extract and flatten barcode data
const printData: PrintBarcodeData[] = [];
for (const location of locations) {
for (const bill of location.bills) {
if (bill.barcodeImage && bill.barcodeImage.trim() !== "") {
printData.push({
locationName: location.name,
billName: bill.name,
barcodeImage: bill.barcodeImage,
yearMonth: yearMonth
});
}
}
}
// Sort by location name, then by bill name for consistent ordering
printData.sort((a, b) => {
if (a.locationName !== b.locationName) {
return a.locationName.localeCompare(b.locationName);
}
return a.billName.localeCompare(b.billName);
});
return printData;
});

63
app/ui/PrintPreview.tsx Normal file
View File

@@ -0,0 +1,63 @@
'use client';
import { PrintBarcodeData } from '../lib/actions/printActions';
export interface PrintPreviewProps {
data: PrintBarcodeData[];
year: number;
month: number;
}
export const PrintPreview: React.FC<PrintPreviewProps> = ({ data, year, month }) => {
return (
<div className="min-h-screen p-4">
<h1 className="text-2xl font-bold mb-4">
Print Preview - {year}-{month.toString().padStart(2, '0')}
</h1>
<div className="mb-4">
<p>Found {data.length} barcode(s) for printing</p>
</div>
{/* Basic table structure - will be enhanced in US-3 */}
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr>
<th className="border border-gray-300 p-2">Index</th>
<th className="border border-gray-300 p-2">Bill Info</th>
<th className="border border-gray-300 p-2">Barcode</th>
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<tr key={`${item.locationName}-${item.billName}`}>
<td className="border border-gray-300 p-2">{index + 1}</td>
<td className="border border-gray-300 p-2">
<div>
<div className="font-medium">{item.yearMonth}</div>
<div>{item.locationName}</div>
<div>{item.billName}</div>
</div>
</td>
<td className="border border-gray-300 p-2">
<img
src={item.barcodeImage.startsWith('data:') ? item.barcodeImage : `data:image/png;base64,${item.barcodeImage}`}
alt={`Barcode for ${item.billName}`}
className="max-w-full h-auto"
/>
</td>
</tr>
))}
</tbody>
</table>
{/* Print button - will be enhanced in US-4 */}
<button
onClick={() => window.print()}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Print
</button>
</div>
);
};