Compare commits
10 Commits
55ba2e16e5
...
0bb29c6a01
Author | SHA1 | Date | |
---|---|---|---|
0bb29c6a01 | |||
273ad95fa0 | |||
599ed993d3 | |||
02d4bae997 | |||
7d680e469f | |||
8f3c8ba80e | |||
85ebe7b8d5 | |||
2402070ef1 | |||
54ffdc37f7 | |||
91fb01bebf |
253
DOCKER.md
Normal file
253
DOCKER.md
Normal file
@ -0,0 +1,253 @@
|
||||
# Docker Setup for Task Receipts
|
||||
|
||||
This document explains how to build and run the Task Receipts application using Docker.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- Docker Compose (usually included with Docker Desktop)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Using Docker Compose (Recommended)
|
||||
|
||||
1. **Build and run both services:**
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
2. **Run in background:**
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
3. **Stop services:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Option 2: Using the Build Script
|
||||
|
||||
1. **Build both images:**
|
||||
```bash
|
||||
./build-docker.sh
|
||||
```
|
||||
|
||||
2. **Build with specific version:**
|
||||
```bash
|
||||
./build-docker.sh v1.0.0
|
||||
```
|
||||
|
||||
3. **Run containers individually:**
|
||||
```bash
|
||||
# Run server
|
||||
docker run -p 4000:4000 task-receipts-server:latest
|
||||
|
||||
# Run client (in another terminal)
|
||||
docker run -p 80:80 task-receipts-client:latest
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
### Server
|
||||
- **Port:** 4000
|
||||
- **Health Check:** GraphQL endpoint at `/graphql`
|
||||
- **Database:** SQLite (persisted in `./server/data`)
|
||||
- **Features:**
|
||||
- GraphQL API
|
||||
- YAML import/export
|
||||
- Receipt printing
|
||||
- Database management
|
||||
|
||||
### Client
|
||||
- **Port:** 80
|
||||
- **Web Server:** Nginx
|
||||
- **Features:**
|
||||
- React application
|
||||
- Material-UI components
|
||||
- Apollo Client for GraphQL
|
||||
|
||||
## Docker Images
|
||||
|
||||
### Client Image (`task-receipts-client`)
|
||||
- **Base:** `nginx:stable` (Ubuntu-based)
|
||||
- **Build:** Multi-stage build with Node.js 18 (Ubuntu-based)
|
||||
- **Size:** Optimized for production
|
||||
- **Features:**
|
||||
- Static file serving
|
||||
- Gzip compression
|
||||
- Security headers
|
||||
|
||||
### Server Image (`task-receipts-server`)
|
||||
- **Base:** `node:18-slim` (Ubuntu-based)
|
||||
- **Build:** Multi-stage build with Node.js 18 (Ubuntu-based)
|
||||
- **Size:** Optimized for production
|
||||
- **Features:**
|
||||
- Non-root user execution
|
||||
- Health checks
|
||||
- Graceful shutdown handling
|
||||
- Signal handling with dumb-init
|
||||
|
||||
## Development
|
||||
|
||||
### Building Individual Images
|
||||
|
||||
```bash
|
||||
# Build client only
|
||||
cd client
|
||||
docker build -t task-receipts-client:dev .
|
||||
|
||||
# Build server only
|
||||
cd server
|
||||
docker build -t task-receipts-server:dev .
|
||||
```
|
||||
|
||||
### Development with Docker Compose
|
||||
|
||||
Create a `docker-compose.dev.yml` for development:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
volumes:
|
||||
- ./server/src:/app/src
|
||||
- ./shared:/app/shared
|
||||
command: npm run dev
|
||||
|
||||
client:
|
||||
build:
|
||||
context: ./client
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5173:80"
|
||||
volumes:
|
||||
- ./client/src:/app/src
|
||||
- ./shared:/app/shared
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The server supports the following environment variables:
|
||||
|
||||
- `NODE_ENV`: Environment (production/development)
|
||||
- `PORT`: Server port (default: 4000)
|
||||
|
||||
### Database Persistence
|
||||
|
||||
The server's SQLite database is persisted in the `./server/data` directory when using Docker Compose.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Port conflicts:**
|
||||
- Ensure ports 80 and 4000 are available
|
||||
- Modify ports in `docker-compose.yml` if needed
|
||||
|
||||
2. **Build failures:**
|
||||
- Check Docker is running
|
||||
- Ensure all source files are present
|
||||
- Check network connectivity for npm packages
|
||||
|
||||
3. **Client can't connect to server:**
|
||||
- Verify server is running and healthy
|
||||
- Check CORS configuration
|
||||
- Ensure proper network connectivity
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# View all logs
|
||||
docker-compose logs
|
||||
|
||||
# View specific service logs
|
||||
docker-compose logs server
|
||||
docker-compose logs client
|
||||
|
||||
# Follow logs in real-time
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check service health
|
||||
docker-compose ps
|
||||
|
||||
# Check individual container health
|
||||
docker inspect task-receipts-server | grep Health -A 10
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
1. **Build and deploy:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
2. **Update services:**
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Using Individual Containers
|
||||
|
||||
1. **Build images:**
|
||||
```bash
|
||||
./build-docker.sh v1.0.0
|
||||
```
|
||||
|
||||
2. **Deploy with orchestration:**
|
||||
```bash
|
||||
# Example with Docker Swarm or Kubernetes
|
||||
docker stack deploy -c docker-compose.yml task-receipts
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Images run as non-root users
|
||||
- Ubuntu-based images for better security and performance
|
||||
- No sensitive data in images
|
||||
- Health checks for monitoring
|
||||
- Graceful shutdown handling
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
- Multi-stage builds reduce image size
|
||||
- Nginx for static file serving
|
||||
- Ubuntu-based images for better performance
|
||||
- Production-only dependencies in final images
|
||||
- Layer caching optimization
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
- Server: GraphQL endpoint availability
|
||||
- Client: Nginx process status
|
||||
|
||||
### Metrics
|
||||
- Container resource usage
|
||||
- Application logs
|
||||
- Health check status
|
||||
|
||||
## Support
|
||||
|
||||
For issues with the Docker setup:
|
||||
1. Check the troubleshooting section
|
||||
2. Review container logs
|
||||
3. Verify Docker and Docker Compose versions
|
||||
4. Ensure all prerequisites are met
|
166
PRINTER_SETUP.md
Normal file
166
PRINTER_SETUP.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Printer Setup for Task Receipts
|
||||
|
||||
This document explains how to set up the Task Receipts application with USB printer support using Docker.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- USB thermal printer (ESC/POS compatible)
|
||||
- Docker and Docker Compose installed
|
||||
- Linux host system (for USB device access)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Run with Printer Support
|
||||
|
||||
```bash
|
||||
# Build and start with printer support
|
||||
docker-compose -f docker-compose.printer.yml up --build
|
||||
|
||||
# Or run in background
|
||||
docker-compose -f docker-compose.printer.yml up -d --build
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `PRINTER_DRIVER` | Type of printer connection | `serial` |
|
||||
|
||||
### USB Device Mounting
|
||||
|
||||
The printer configuration includes several USB mounting options:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
# Mount specific USB bus
|
||||
- /dev/bus/usb:/dev/bus/usb:rw
|
||||
|
||||
devices:
|
||||
# Mount USB devices specifically
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
|
||||
privileged: true # Required for USB access
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Printer Not Found
|
||||
|
||||
1. **Check USB device listing:**
|
||||
```bash
|
||||
lsusb
|
||||
```
|
||||
|
||||
2. **Verify device permissions:**
|
||||
```bash
|
||||
ls -la /dev/bus/usb/
|
||||
```
|
||||
|
||||
3. **Check container logs:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.printer.yml logs server
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
If you encounter permission issues:
|
||||
|
||||
1. **Add your user to the docker group:**
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
2. **Restart Docker service:**
|
||||
```bash
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
3. **Log out and back in** for group changes to take effect.
|
||||
|
||||
### Alternative: Run with sudo
|
||||
|
||||
If you still have issues, you can run with elevated privileges:
|
||||
|
||||
```bash
|
||||
sudo docker-compose -f docker-compose.printer.yml up --build
|
||||
```
|
||||
|
||||
## Common Printer Models
|
||||
|
||||
### ESC/POS Compatible Printers
|
||||
|
||||
Most thermal printers support ESC/POS commands. Common models include:
|
||||
|
||||
- **Star TSP100** series
|
||||
- **Epson TM-T88** series
|
||||
- **Citizen CT-S310** series
|
||||
- **Generic thermal printers**
|
||||
|
||||
### Finding Your Printer's IDs
|
||||
|
||||
```bash
|
||||
# Method 1: lsusb
|
||||
lsusb
|
||||
|
||||
# Method 2: dmesg (after plugging in printer)
|
||||
dmesg | tail -20
|
||||
|
||||
# Method 3: udevadm
|
||||
udevadm info -a -n /dev/usb/lp0
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
⚠️ **Warning**: The printer configuration uses `privileged: true` which gives the container elevated privileges. This is necessary for USB device access but should be used carefully in production environments.
|
||||
|
||||
### Alternative Security Approaches
|
||||
|
||||
1. **Use specific device capabilities:**
|
||||
```yaml
|
||||
cap_add:
|
||||
- SYS_RAWIO
|
||||
```
|
||||
|
||||
2. **Mount only specific devices:**
|
||||
```yaml
|
||||
devices:
|
||||
- /dev/usb/lp0:/dev/usb/lp0
|
||||
```
|
||||
|
||||
3. **Use udev rules** to set proper permissions on the host.
|
||||
|
||||
## Testing Printer Connection
|
||||
|
||||
Once the container is running, you can test the printer connection:
|
||||
|
||||
1. **Check if printer is detected:**
|
||||
```bash
|
||||
docker exec -it task-receipts-server lsusb
|
||||
```
|
||||
|
||||
2. **Test printer from the application:**
|
||||
- Use the web interface to send a test print
|
||||
- Check the server logs for printer-related messages
|
||||
|
||||
## Stopping the Services
|
||||
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose -f docker-compose.printer.yml down
|
||||
|
||||
# Stop and remove volumes
|
||||
docker-compose -f docker-compose.printer.yml down -v
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For printer-specific issues:
|
||||
|
||||
1. Check the server logs for detailed error messages
|
||||
2. Verify your printer is ESC/POS compatible
|
||||
3. Ensure the USB IDs are correctly configured
|
||||
4. Test the printer on the host system first
|
||||
|
||||
For Docker-related issues, refer to the main `DOCKER.md` documentation.
|
124
build-docker.sh
Executable file
124
build-docker.sh
Executable file
@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for Task Receipts Docker images
|
||||
# This script builds both the client and server Docker images
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Configuration
|
||||
PROJECT_NAME="task-receipts"
|
||||
CLIENT_IMAGE_NAME="${PROJECT_NAME}-client"
|
||||
SERVER_IMAGE_NAME="${PROJECT_NAME}-server"
|
||||
VERSION=${1:-latest} # Use first argument as version, default to 'latest'
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to check if Docker is running
|
||||
check_docker() {
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
print_error "Docker is not running or not accessible"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build client image
|
||||
build_client() {
|
||||
print_status "Building client image..."
|
||||
|
||||
# Build the image from root context with client Dockerfile
|
||||
docker build -t "${CLIENT_IMAGE_NAME}:${VERSION}" -f client/Dockerfile .
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Client image built successfully: ${CLIENT_IMAGE_NAME}:${VERSION}"
|
||||
else
|
||||
print_error "Failed to build client image"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build server image
|
||||
build_server() {
|
||||
print_status "Building server image..."
|
||||
|
||||
# Build the image from root context with server Dockerfile
|
||||
docker build -t "${SERVER_IMAGE_NAME}:${VERSION}" -f server/Dockerfile .
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
print_success "Server image built successfully: ${SERVER_IMAGE_NAME}:${VERSION}"
|
||||
else
|
||||
print_error "Failed to build server image"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "Usage: $0 [VERSION]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " VERSION Version tag for the Docker images (default: latest)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Build with 'latest' tag"
|
||||
echo " $0 v1.0.0 # Build with 'v1.0.0' tag"
|
||||
echo " $0 dev # Build with 'dev' tag"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "Starting Docker build process for Task Receipts"
|
||||
print_status "Version: ${VERSION}"
|
||||
print_status "Client image: ${CLIENT_IMAGE_NAME}:${VERSION}"
|
||||
print_status "Server image: ${SERVER_IMAGE_NAME}:${VERSION}"
|
||||
echo ""
|
||||
|
||||
# Check if Docker is available
|
||||
check_docker
|
||||
|
||||
# Build client image
|
||||
build_client
|
||||
|
||||
# Build server image
|
||||
build_server
|
||||
|
||||
echo ""
|
||||
print_success "All images built successfully!"
|
||||
echo ""
|
||||
print_status "Built images:"
|
||||
docker images | grep -E "(${CLIENT_IMAGE_NAME}|${SERVER_IMAGE_NAME})" || true
|
||||
echo ""
|
||||
print_status "To run the containers:"
|
||||
echo " docker run -p 80:80 ${CLIENT_IMAGE_NAME}:${VERSION}"
|
||||
echo " docker run -p 4000:4000 ${SERVER_IMAGE_NAME}:${VERSION}"
|
||||
}
|
||||
|
||||
# Handle help argument
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
show_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run main function
|
||||
main
|
44
client/.dockerignore
Normal file
44
client/.dockerignore
Normal file
@ -0,0 +1,44 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Test outputs
|
||||
test-output/
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
33
client/Dockerfile
Normal file
33
client/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# Build stage
|
||||
FROM node:18 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root package files for workspaces
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
|
||||
# Copy client and shared code
|
||||
COPY client/ ./client/
|
||||
COPY shared/ ./shared/
|
||||
|
||||
# Install dependencies for client workspace
|
||||
RUN npm ci --workspace=client
|
||||
|
||||
# Build the client
|
||||
WORKDIR /app/client
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:stable
|
||||
|
||||
COPY --from=builder /app/client/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration (optional - using default for now)
|
||||
# COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
4286
client/package-lock.json
generated
4286
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,9 +11,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.5",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.17.1",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@task-receipts/shared": "file:../shared",
|
||||
"graphql": "^16.8.1",
|
||||
"react": "^18.2.0",
|
||||
@ -23,8 +24,8 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { client } from './graphql/client';
|
||||
import { DesktopLayout } from './layouts/DesktopLayout';
|
||||
import { MobileLayout } from './layouts/MobileLayout';
|
||||
import { UserSelection } from './components/UserSelection';
|
||||
@ -64,37 +62,35 @@ export function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<div className="app">
|
||||
{deviceType === 'desktop' ? (
|
||||
<DesktopLayout
|
||||
groups={groups}
|
||||
selectedGroup={selectedGroup}
|
||||
selectedTask={selectedTask}
|
||||
selectedStep={selectedStep}
|
||||
onGroupSelect={setSelectedGroup}
|
||||
onTaskSelect={setSelectedTask}
|
||||
onStepSelect={setSelectedStep}
|
||||
onPrintTask={handlePrintTask}
|
||||
onPrintStep={handlePrintStep}
|
||||
onAddNote={handleAddNote}
|
||||
/>
|
||||
) : (
|
||||
<MobileLayout
|
||||
groups={groups}
|
||||
selectedGroup={selectedGroup}
|
||||
selectedTask={selectedTask}
|
||||
selectedStep={selectedStep}
|
||||
onGroupSelect={setSelectedGroup}
|
||||
onTaskSelect={setSelectedTask}
|
||||
onStepSelect={setSelectedStep}
|
||||
onBack={handleBack}
|
||||
onPrintTask={handlePrintTask}
|
||||
onPrintStep={handlePrintStep}
|
||||
onAddNote={handleAddNote}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ApolloProvider>
|
||||
<div className="app">
|
||||
{deviceType === 'desktop' ? (
|
||||
<DesktopLayout
|
||||
groups={groups}
|
||||
selectedGroup={selectedGroup}
|
||||
selectedTask={selectedTask}
|
||||
selectedStep={selectedStep}
|
||||
onGroupSelect={setSelectedGroup}
|
||||
onTaskSelect={setSelectedTask}
|
||||
onStepSelect={setSelectedStep}
|
||||
onPrintTask={handlePrintTask}
|
||||
onPrintStep={handlePrintStep}
|
||||
onAddNote={handleAddNote}
|
||||
/>
|
||||
) : (
|
||||
<MobileLayout
|
||||
groups={groups}
|
||||
selectedGroup={selectedGroup}
|
||||
selectedTask={selectedTask}
|
||||
selectedStep={selectedStep}
|
||||
onGroupSelect={setSelectedGroup}
|
||||
onTaskSelect={setSelectedTask}
|
||||
onStepSelect={setSelectedStep}
|
||||
onBack={handleBack}
|
||||
onPrintTask={handlePrintTask}
|
||||
onPrintStep={handlePrintStep}
|
||||
onAddNote={handleAddNote}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -33,6 +33,89 @@ function getComplementaryColors(primaryColor: string): string[] {
|
||||
return complementaryIndices.map(index => colorPalette[index]);
|
||||
}
|
||||
|
||||
// Check if current time is in night hours (midnight to 6am)
|
||||
function isNightTime(): boolean {
|
||||
const hour = new Date().getHours();
|
||||
return hour >= 0 && hour < 6;
|
||||
}
|
||||
|
||||
// Get fade opacity based on time (0 = completely off, 1 = full brightness)
|
||||
function getNightFadeOpacity(): number {
|
||||
if (!isNightTime()) return 1;
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const minute = new Date().getMinutes();
|
||||
|
||||
// Calculate fade based on how far into the night we are
|
||||
// 15-minute fade out starting at midnight, 15-minute fade in starting at 5:45am
|
||||
const totalMinutes = hour * 60 + minute;
|
||||
const fadeOutEnd = 15; // 12:15am
|
||||
const fadeInStart = 5 * 60 + 45; // 5:45am
|
||||
const fadeInEnd = 6 * 60; // 6:00am
|
||||
|
||||
if (totalMinutes <= fadeOutEnd) {
|
||||
// Fading out: midnight to 12:15am (15 minutes)
|
||||
const fadeProgress = totalMinutes / fadeOutEnd;
|
||||
return 1 - fadeProgress; // 1 to 0
|
||||
} else if (totalMinutes >= fadeInStart) {
|
||||
// Fading in: 5:45am to 6:00am (15 minutes)
|
||||
const fadeProgress = (totalMinutes - fadeInStart) / (fadeInEnd - fadeInStart);
|
||||
return fadeProgress; // 0 to 1
|
||||
} else {
|
||||
// Completely off: 12:15am to 5:45am
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw 24-hour clock
|
||||
function drawClock(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
const timeString = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
// Position clock in center
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Draw clock background
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(centerX - 120, centerY - 60, 240, 120);
|
||||
ctx.restore();
|
||||
|
||||
// Draw time text
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 48px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(timeString, centerX, centerY);
|
||||
ctx.restore();
|
||||
|
||||
// Draw date
|
||||
const dateString = now.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.font = '16px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(dateString, centerX, centerY + 50);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Create gradient pattern
|
||||
function createGradientPattern(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
@ -155,18 +238,29 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
const animationRef = useRef<number>();
|
||||
const { primaryColor } = useThemeStore();
|
||||
const [lastClearTime, setLastClearTime] = useState(Date.now());
|
||||
const [interactionEnabled, setInteractionEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Screensaver: useEffect triggered');
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
if (!canvas) {
|
||||
console.error('Screensaver: Canvas ref is null');
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
if (!ctx) {
|
||||
console.error('Screensaver: Could not get 2D context');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Screensaver: Canvas and context initialized');
|
||||
|
||||
// Set canvas size
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
console.log('Screensaver: Canvas resized to', canvas.width, 'x', canvas.height);
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
@ -175,9 +269,9 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
// Get complementary colors
|
||||
const complementaryColors = getComplementaryColors(primaryColor);
|
||||
const colors = [primaryColor, ...complementaryColors];
|
||||
console.log('Screensaver: Using colors', colors);
|
||||
|
||||
let startTime = Date.now();
|
||||
let clearInterval = 0;
|
||||
|
||||
const animate = () => {
|
||||
const currentTime = Date.now();
|
||||
@ -187,13 +281,6 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
if (currentTime - lastClearTime > 180000) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
setLastClearTime(currentTime);
|
||||
clearInterval = 0;
|
||||
}
|
||||
|
||||
// Clear screen every few seconds for variety
|
||||
if (clearInterval > 5000) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
clearInterval = 0;
|
||||
}
|
||||
|
||||
// Create gradient background
|
||||
@ -205,13 +292,28 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
// Draw line art
|
||||
drawLineArt(ctx, canvas.width, canvas.height, colors, elapsed);
|
||||
|
||||
clearInterval += 16; // ~60fps
|
||||
// Apply night fade if needed
|
||||
const nightFadeOpacity = getNightFadeOpacity();
|
||||
if (nightFadeOpacity < 1) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 1 - nightFadeOpacity;
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Draw clock
|
||||
drawClock(ctx, canvas.width, canvas.height);
|
||||
|
||||
// Schedule next frame before any potential clearing
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
console.log('Screensaver: Starting animation');
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
console.log('Screensaver: Cleaning up');
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
@ -219,9 +321,24 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
};
|
||||
}, [primaryColor, lastClearTime]);
|
||||
|
||||
// Close on any key press or mouse movement
|
||||
// Enable interaction detection after a delay to prevent immediate closing
|
||||
useEffect(() => {
|
||||
const handleInteraction = () => onClose();
|
||||
const timer = setTimeout(() => {
|
||||
console.log('Screensaver: Enabling interaction detection');
|
||||
setInteractionEnabled(true);
|
||||
}, 1000); // 1 second delay
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Close on any key press or mouse movement (only when interaction is enabled)
|
||||
useEffect(() => {
|
||||
if (!interactionEnabled) return;
|
||||
|
||||
const handleInteraction = () => {
|
||||
console.log('Screensaver: User interaction detected, closing');
|
||||
onClose();
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleInteraction);
|
||||
window.addEventListener('mousemove', handleInteraction);
|
||||
@ -234,7 +351,9 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
window.removeEventListener('click', handleInteraction);
|
||||
window.removeEventListener('touchstart', handleInteraction);
|
||||
};
|
||||
}, [onClose]);
|
||||
}, [onClose, interactionEnabled]);
|
||||
|
||||
console.log('Screensaver: Rendering component');
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -244,8 +363,9 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
left: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: 9999,
|
||||
zIndex: 99999,
|
||||
bgcolor: 'black',
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
@ -254,6 +374,7 @@ export function Screensaver({ onClose }: ScreensaverProps) {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'black',
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
|
@ -2,18 +2,27 @@ import { useState } from 'react';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
import { Slideshow } from '@mui/icons-material';
|
||||
import { Screensaver } from './Screensaver';
|
||||
import { useAutoScreensaver } from '../hooks/useAutoScreensaver';
|
||||
|
||||
export function ScreensaverButton() {
|
||||
const [screensaverOpen, setScreensaverOpen] = useState(false);
|
||||
const [manualScreensaverOpen, setManualScreensaverOpen] = useState(false);
|
||||
const { isActive: autoScreensaverActive, resetTimer } = useAutoScreensaver(5); // 5 minutes
|
||||
|
||||
const handleOpenScreensaver = () => {
|
||||
setScreensaverOpen(true);
|
||||
console.log('ScreensaverButton: Opening manual screensaver');
|
||||
setManualScreensaverOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseScreensaver = () => {
|
||||
setScreensaverOpen(false);
|
||||
console.log('ScreensaverButton: Closing screensaver');
|
||||
setManualScreensaverOpen(false);
|
||||
resetTimer(); // Reset the auto-screensaver timer when manually closed
|
||||
};
|
||||
|
||||
const isScreensaverOpen = manualScreensaverOpen || autoScreensaverActive;
|
||||
|
||||
console.log('ScreensaverButton: Rendering, screensaverOpen =', isScreensaverOpen, 'autoActive =', autoScreensaverActive);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Launch screensaver">
|
||||
@ -21,7 +30,7 @@ export function ScreensaverButton() {
|
||||
<Slideshow />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{screensaverOpen && (
|
||||
{isScreensaverOpen && (
|
||||
<Screensaver onClose={handleCloseScreensaver} />
|
||||
)}
|
||||
</>
|
||||
|
64
client/src/hooks/useAutoScreensaver.ts
Normal file
64
client/src/hooks/useAutoScreensaver.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
export function useAutoScreensaver(timeoutMinutes: number = 5) {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [lastActivity, setLastActivity] = useState(Date.now());
|
||||
|
||||
const resetTimer = useCallback(() => {
|
||||
setLastActivity(Date.now());
|
||||
if (isActive) {
|
||||
setIsActive(false);
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
const activateScreensaver = useCallback(() => {
|
||||
setIsActive(true);
|
||||
}, []);
|
||||
|
||||
// Track user activity
|
||||
useEffect(() => {
|
||||
const handleActivity = () => {
|
||||
resetTimer();
|
||||
};
|
||||
|
||||
// Listen for various user activities
|
||||
window.addEventListener('mousemove', handleActivity);
|
||||
window.addEventListener('mousedown', handleActivity);
|
||||
window.addEventListener('keydown', handleActivity);
|
||||
window.addEventListener('touchstart', handleActivity);
|
||||
window.addEventListener('scroll', handleActivity);
|
||||
window.addEventListener('click', handleActivity);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleActivity);
|
||||
window.removeEventListener('mousedown', handleActivity);
|
||||
window.removeEventListener('keydown', handleActivity);
|
||||
window.removeEventListener('touchstart', handleActivity);
|
||||
window.removeEventListener('scroll', handleActivity);
|
||||
window.removeEventListener('click', handleActivity);
|
||||
};
|
||||
}, [resetTimer]);
|
||||
|
||||
// Check for timeout
|
||||
useEffect(() => {
|
||||
const timeoutMs = timeoutMinutes * 60 * 1000;
|
||||
|
||||
const checkTimeout = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastActivity > timeoutMs && !isActive) {
|
||||
console.log(`Auto-screensaver: Activating after ${timeoutMinutes} minutes of inactivity`);
|
||||
activateScreensaver();
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(checkTimeout, 1000); // Check every second
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [lastActivity, isActive, timeoutMinutes, activateScreensaver]);
|
||||
|
||||
return {
|
||||
isActive,
|
||||
activateScreensaver,
|
||||
resetTimer,
|
||||
};
|
||||
}
|
@ -20,7 +20,7 @@ function isStepView(selectedTask?: TaskWithSteps, selectedStep?: StepWithNotes)
|
||||
}
|
||||
|
||||
function isGroupWithTasks(group: any): group is GroupWithTasks {
|
||||
return group && typeof group.id === 'string' && Array.isArray(group.tasks);
|
||||
return group && (typeof group.id === 'string' || typeof group.id === 'number') && Array.isArray(group.tasks);
|
||||
}
|
||||
|
||||
export function MobileLayout({
|
||||
|
54
docker-compose.printer.yml
Normal file
54
docker-compose.printer.yml
Normal file
@ -0,0 +1,54 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: server/Dockerfile
|
||||
container_name: task-receipts-server
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
# Printer configuration
|
||||
- PRINTER_DRIVER=serial
|
||||
volumes:
|
||||
# Mount database directory for persistence
|
||||
- ./server/data/prod.sqlite3:/app/server/prod.sqlite3:rw
|
||||
# Mount USB devices for printer access
|
||||
# /dev/bus/usb:/dev/bus/usb:rw
|
||||
devices:
|
||||
# Mount USB devices specifically
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
privileged: true # Required for USB device access
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/graphql', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- task-receipts-network
|
||||
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: client/Dockerfile
|
||||
container_name: task-receipts-client
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- task-receipts-network
|
||||
|
||||
networks:
|
||||
task-receipts-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
server-data:
|
||||
driver: local
|
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@ -0,0 +1,46 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
container_name: task-receipts-server
|
||||
ports:
|
||||
- "4000:4000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
# Mount database directory for persistence
|
||||
- ./server/data:/app/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/graphql', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- task-receipts-network
|
||||
|
||||
client:
|
||||
build:
|
||||
context: ./client
|
||||
dockerfile: Dockerfile
|
||||
container_name: task-receipts-client
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- task-receipts-network
|
||||
|
||||
networks:
|
||||
task-receipts-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
server-data:
|
||||
driver: local
|
4272
package-lock.json
generated
4272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,5 @@
|
||||
"test": "npm run test --workspaces",
|
||||
"lint": "npm run lint --workspaces",
|
||||
"lint:fix": "npm run lint:fix --workspaces"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/material": "^7.1.1"
|
||||
}
|
||||
}
|
||||
|
48
server/.dockerignore
Normal file
48
server/.dockerignore
Normal file
@ -0,0 +1,48 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Test outputs
|
||||
test-output/
|
||||
|
||||
# Database files
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
40
server/Dockerfile
Normal file
40
server/Dockerfile
Normal file
@ -0,0 +1,40 @@
|
||||
# Build stage
|
||||
FROM node:18 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy root package files for workspaces
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
|
||||
# Copy server and shared code
|
||||
COPY server/ ./server/
|
||||
COPY shared/ ./shared/
|
||||
|
||||
# Install dependencies for server workspace
|
||||
RUN npm ci --workspace=server
|
||||
|
||||
# Build the server
|
||||
WORKDIR /app/server
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:18-slim
|
||||
RUN apt-get update && apt-get install -y dumb-init && rm -rf /var/lib/apt/lists/*
|
||||
RUN groupadd -r nodejs && useradd -r -g nodejs nodejs
|
||||
WORKDIR /app/server
|
||||
|
||||
# Copy only server package.json and built code
|
||||
COPY server/package.json ./
|
||||
COPY --from=builder /app/server/dist ./dist
|
||||
COPY --from=builder /app/shared ../shared
|
||||
|
||||
# Install only production dependencies
|
||||
RUN npm install --omit=dev && npm cache clean --force
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
USER nodejs
|
||||
EXPOSE 4000
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:4000/graphql', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
CMD ["node", "dist/server/src/index.js"]
|
8980
server/package-lock.json
generated
8980
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -40,8 +40,8 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/pg": "^8.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
|
35
server/scripts/print-image.ts
Normal file
35
server/scripts/print-image.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Image, Printer } from "@node-escpos/core";
|
||||
import USB from "@node-escpos/usb-adapter";
|
||||
import { join } from "path";
|
||||
|
||||
async function printImage(path: string) {
|
||||
const device = new USB();
|
||||
await new Promise<void>((resolve,reject) => {
|
||||
device.open(async function(err){
|
||||
if(err){
|
||||
reject(err);
|
||||
return
|
||||
}
|
||||
|
||||
let printer = new Printer(device, {});
|
||||
|
||||
const tux = path; // join(__dirname, '../assets/tux.png');
|
||||
const image = await Image.load(tux);
|
||||
|
||||
// inject image to printer
|
||||
printer = await printer.image(image, "d24")
|
||||
|
||||
printer
|
||||
.cut()
|
||||
.close()
|
||||
.finally(resolve)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log(process.argv);
|
||||
|
||||
printImage(process.argv[2] || join(__dirname, 'tux.png'))
|
||||
.then(() => console.log('done'))
|
||||
.catch((err) => console.error(err));
|
||||
|
BIN
server/scripts/tux.png
Normal file
BIN
server/scripts/tux.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
@ -1,4 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import logger from '../../logger';
|
||||
|
||||
export abstract class BaseRepository<T extends { created_at?: Date; updated_at?: Date }> {
|
||||
protected tableName: string;
|
||||
@ -16,6 +17,7 @@ export abstract class BaseRepository<T extends { created_at?: Date; updated_at?:
|
||||
|
||||
async findById(id: number): Promise<T | null> {
|
||||
const result = await this.db(this.tableName).where('id', id).first();
|
||||
logger.info(`findById(${id}) for table ${this.tableName}:`, result);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { BaseRepository } from './base-repository';
|
||||
import { Step, Task } from '@shared/index';
|
||||
import logger from '../../logger';
|
||||
|
||||
export class StepRepository extends BaseRepository<Step> {
|
||||
constructor(db: Knex) {
|
||||
@ -8,10 +9,18 @@ export class StepRepository extends BaseRepository<Step> {
|
||||
}
|
||||
|
||||
async findByTaskId(taskId: number): Promise<Step[]> {
|
||||
return await this.db(this.tableName)
|
||||
const steps = await this.db(this.tableName)
|
||||
.where('task_id', taskId)
|
||||
.orderBy('order')
|
||||
.select('*');
|
||||
|
||||
/*logger.info(`Retrieved ${steps.length} steps for task ${taskId}`);
|
||||
for (const step of steps) {
|
||||
logger.info(`Step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
|
||||
logger.info(`Full step data:`, JSON.stringify(step, null, 2));
|
||||
}*/
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
async incrementPrintCount(id: number): Promise<boolean> {
|
||||
@ -54,4 +63,4 @@ export class StepRepository extends BaseRepository<Step> {
|
||||
|
||||
return steps.findIndex(s => s.id === stepId) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,13 +60,14 @@ async function startServer() {
|
||||
// YAML Import endpoint
|
||||
app.post('/api/yaml/import', async (req, res) => {
|
||||
try {
|
||||
const { yamlContent } = req.body;
|
||||
const yamlContent = req.body;
|
||||
console.log('loading YAML:', yamlContent);
|
||||
|
||||
if (!yamlContent || typeof yamlContent !== 'string') {
|
||||
if (!yamlContent || typeof yamlContent !== 'object') {
|
||||
return res.status(400).json({ error: 'YAML content is required' });
|
||||
}
|
||||
|
||||
await yamlService.importFromYaml(yamlContent);
|
||||
await yamlService.importFromYaml(JSON.stringify(yamlContent));
|
||||
res.json({ message: 'Database imported successfully' });
|
||||
} catch (error) {
|
||||
logger.error('Error importing YAML:', error);
|
||||
@ -101,4 +102,4 @@ async function startServer() {
|
||||
startServer().catch((error) => {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
98
server/src/printer/__tests__/barcode-debug.test.ts
Normal file
98
server/src/printer/__tests__/barcode-debug.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { CommandBuilder } from '../printer-commands';
|
||||
import { TestCommandExecutor } from '../command-executor';
|
||||
import { BARCODE_CONFIG } from '../printer-constants';
|
||||
|
||||
describe('Barcode Debug', () => {
|
||||
let commandExecutor: TestCommandExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
commandExecutor = new TestCommandExecutor();
|
||||
});
|
||||
|
||||
it('should test different barcode formats', async () => {
|
||||
const testData = '123456789';
|
||||
|
||||
// Test basic barcode
|
||||
const commands = [
|
||||
CommandBuilder.text('Testing barcode:'),
|
||||
CommandBuilder.barcode(testData),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
console.log('Barcode test output:', output);
|
||||
expect(output).toContain('[BARCODE: 123456789]');
|
||||
});
|
||||
|
||||
it('should test task barcode', async () => {
|
||||
const taskId = 123;
|
||||
|
||||
const commands = [
|
||||
CommandBuilder.text('Testing task barcode:'),
|
||||
CommandBuilder.taskBarcode(taskId),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
console.log('Task barcode test output:', output);
|
||||
expect(output).toContain('[BARCODE: 100000123]');
|
||||
});
|
||||
|
||||
it('should test step barcode', async () => {
|
||||
const stepId = 456;
|
||||
|
||||
const commands = [
|
||||
CommandBuilder.text('Testing step barcode:'),
|
||||
CommandBuilder.stepBarcode(stepId),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
console.log('Step barcode test output:', output);
|
||||
expect(output).toContain('[BARCODE: 200000456]');
|
||||
});
|
||||
|
||||
it('should test barcode with alignment', async () => {
|
||||
const testData = '987654321';
|
||||
|
||||
const commands = [
|
||||
CommandBuilder.text('Testing barcode with alignment:'),
|
||||
...CommandBuilder.barcodeWithAlignment(testData),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
console.log('Barcode with alignment test output:', output);
|
||||
expect(output).toContain('[BARCODE: 987654321]');
|
||||
});
|
||||
|
||||
it('should test task barcode with alignment', async () => {
|
||||
const taskId = 789;
|
||||
|
||||
const commands = [
|
||||
CommandBuilder.text('Testing task barcode with alignment:'),
|
||||
...CommandBuilder.taskBarcodeWithAlignment(taskId),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
console.log('Task barcode with alignment test output:', output);
|
||||
expect(output).toContain('[BARCODE: 100000789]');
|
||||
});
|
||||
|
||||
it('should test barcode configuration', () => {
|
||||
console.log('Barcode config:', BARCODE_CONFIG);
|
||||
expect(BARCODE_CONFIG.TYPE).toBe('CODE39');
|
||||
expect(BARCODE_CONFIG.ALTERNATIVE_TYPES).toContain('CODE128');
|
||||
});
|
||||
});
|
167
server/src/printer/__tests__/barcode-utils.test.ts
Normal file
167
server/src/printer/__tests__/barcode-utils.test.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { BarcodeUtils, BarcodeData } from '../barcode-utils';
|
||||
|
||||
describe('BarcodeUtils', () => {
|
||||
describe('parseBarcode', () => {
|
||||
it('should parse valid task barcode', () => {
|
||||
const result = BarcodeUtils.parseBarcode('100000123');
|
||||
expect(result).toEqual({ type: 'TASK', id: 123 });
|
||||
});
|
||||
|
||||
it('should parse valid step barcode', () => {
|
||||
const result = BarcodeUtils.parseBarcode('200000456');
|
||||
expect(result).toEqual({ type: 'STEP', id: 456 });
|
||||
});
|
||||
|
||||
it('should parse barcode with non-numeric characters', () => {
|
||||
const result = BarcodeUtils.parseBarcode('1ABC000123DEF');
|
||||
expect(result).toEqual({ type: 'TASK', id: 123 });
|
||||
});
|
||||
|
||||
it('should return null for invalid entity type', () => {
|
||||
const result = BarcodeUtils.parseBarcode('300000123');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for too short barcode', () => {
|
||||
const result = BarcodeUtils.parseBarcode('1');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid ID', () => {
|
||||
const result = BarcodeUtils.parseBarcode('100000000');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty string', () => {
|
||||
const result = BarcodeUtils.parseBarcode('');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse large IDs correctly', () => {
|
||||
const result = BarcodeUtils.parseBarcode('199999999');
|
||||
expect(result).toEqual({ type: 'TASK', id: 99999999 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTaskBarcode', () => {
|
||||
it('should generate task barcode', () => {
|
||||
const result = BarcodeUtils.generateTaskBarcode(123);
|
||||
expect(result).toBe('100000123');
|
||||
});
|
||||
|
||||
it('should generate task barcode with padding', () => {
|
||||
const result = BarcodeUtils.generateTaskBarcode(1);
|
||||
expect(result).toBe('100000001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateStepBarcode', () => {
|
||||
it('should generate step barcode', () => {
|
||||
const result = BarcodeUtils.generateStepBarcode(456);
|
||||
expect(result).toBe('200000456');
|
||||
});
|
||||
|
||||
it('should generate step barcode with padding', () => {
|
||||
const result = BarcodeUtils.generateStepBarcode(1);
|
||||
expect(result).toBe('200000001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidBarcode', () => {
|
||||
it('should return true for valid task barcode', () => {
|
||||
const result = BarcodeUtils.isValidBarcode('100000123');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid step barcode', () => {
|
||||
const result = BarcodeUtils.isValidBarcode('200000456');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid barcode', () => {
|
||||
const result = BarcodeUtils.isValidBarcode('300000123');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityType', () => {
|
||||
it('should return TASK for task barcode', () => {
|
||||
const result = BarcodeUtils.getEntityType('100000123');
|
||||
expect(result).toBe('TASK');
|
||||
});
|
||||
|
||||
it('should return STEP for step barcode', () => {
|
||||
const result = BarcodeUtils.getEntityType('200000456');
|
||||
expect(result).toBe('STEP');
|
||||
});
|
||||
|
||||
it('should return null for invalid barcode', () => {
|
||||
const result = BarcodeUtils.getEntityType('300000123');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityId', () => {
|
||||
it('should return ID for task barcode', () => {
|
||||
const result = BarcodeUtils.getEntityId('100000123');
|
||||
expect(result).toBe(123);
|
||||
});
|
||||
|
||||
it('should return ID for step barcode', () => {
|
||||
const result = BarcodeUtils.getEntityId('200000456');
|
||||
expect(result).toBe(456);
|
||||
});
|
||||
|
||||
it('should return null for invalid barcode', () => {
|
||||
const result = BarcodeUtils.getEntityId('300000123');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxId', () => {
|
||||
it('should return maximum supported ID', () => {
|
||||
const result = BarcodeUtils.getMaxId();
|
||||
expect(result).toBe(99999999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidId', () => {
|
||||
it('should return true for valid ID', () => {
|
||||
const result = BarcodeUtils.isValidId(123);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for zero ID', () => {
|
||||
const result = BarcodeUtils.isValidId(0);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for negative ID', () => {
|
||||
const result = BarcodeUtils.isValidId(-1);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for ID too large', () => {
|
||||
const result = BarcodeUtils.isValidId(100000000);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('round-trip tests', () => {
|
||||
it('should generate and parse task barcode correctly', () => {
|
||||
const taskId = 789;
|
||||
const generated = BarcodeUtils.generateTaskBarcode(taskId);
|
||||
const parsed = BarcodeUtils.parseBarcode(generated);
|
||||
|
||||
expect(parsed).toEqual({ type: 'TASK', id: taskId });
|
||||
});
|
||||
|
||||
it('should generate and parse step barcode correctly', () => {
|
||||
const stepId = 101;
|
||||
const generated = BarcodeUtils.generateStepBarcode(stepId);
|
||||
const parsed = BarcodeUtils.parseBarcode(generated);
|
||||
|
||||
expect(parsed).toEqual({ type: 'STEP', id: stepId });
|
||||
});
|
||||
});
|
||||
});
|
133
server/src/printer/__tests__/command-system.test.ts
Normal file
133
server/src/printer/__tests__/command-system.test.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { CommandBuilder } from '../printer-commands';
|
||||
import { TestCommandExecutor } from '../command-executor';
|
||||
|
||||
describe('Command System (Standalone)', () => {
|
||||
let commandExecutor: TestCommandExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
commandExecutor = new TestCommandExecutor();
|
||||
});
|
||||
|
||||
it('should execute text commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.text('Hello World'),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.text('Test'),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('Hello World');
|
||||
expect(output).toContain('Test');
|
||||
});
|
||||
|
||||
it('should execute header commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.header('Test Header'),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Test Header');
|
||||
});
|
||||
|
||||
it('should execute banner commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.banner('=', 10),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('==========');
|
||||
});
|
||||
|
||||
it('should execute barcode commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.barcode('123456'),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[BARCODE: 123456]');
|
||||
});
|
||||
|
||||
it('should execute section commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.section('Test Section', 'Test Content', '-', true),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Test Section');
|
||||
expect(output).toContain('Test Content');
|
||||
});
|
||||
|
||||
it('should execute step header commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.stepHeader('Test Step', 1, 'Test Task', true),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Step 1: Test Step');
|
||||
});
|
||||
|
||||
it('should execute cut commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.cut(true, 4),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('--- CUT ---');
|
||||
});
|
||||
|
||||
it('should execute complex command sequences', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.header('Complex Test'),
|
||||
CommandBuilder.banner('=', 20),
|
||||
CommandBuilder.text('Some content'),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.barcode('123'),
|
||||
CommandBuilder.cut(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Complex Test');
|
||||
expect(output).toContain('====================');
|
||||
expect(output).toContain('Some content');
|
||||
expect(output).toContain('[BARCODE: 123]');
|
||||
expect(output).toContain('--- CUT ---');
|
||||
});
|
||||
|
||||
it('should handle empty command arrays', async () => {
|
||||
const commands: any[] = [];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toBe('\n');
|
||||
});
|
||||
|
||||
it('should handle list commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.list(['Item 1', 'Item 2', 'Item 3'], 1, '- '),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('- [ ] 1: Item 1');
|
||||
expect(output).toContain('- [ ] 2: Item 2');
|
||||
expect(output).toContain('- [ ] 3: Item 3');
|
||||
});
|
||||
});
|
@ -1,144 +1,51 @@
|
||||
import { formatUtils } from '../format-utils';
|
||||
import { Command } from '../printer-commands';
|
||||
|
||||
describe('formatUtils', () => {
|
||||
describe('createBanner', () => {
|
||||
it('should create a banner with default length', () => {
|
||||
const banner = formatUtils.createBanner('=');
|
||||
expect(banner).toBe('='.repeat(40));
|
||||
});
|
||||
|
||||
it('should create a banner with custom length', () => {
|
||||
const banner = formatUtils.createBanner('-', 32);
|
||||
expect(banner).toBe('-'.repeat(32));
|
||||
});
|
||||
|
||||
it('should handle empty character', () => {
|
||||
const banner = formatUtils.createBanner('', 10);
|
||||
expect(banner).toBe('');
|
||||
});
|
||||
|
||||
it('should handle zero length', () => {
|
||||
const banner = formatUtils.createBanner('*', 0);
|
||||
expect(banner).toBe('');
|
||||
});
|
||||
it('banner returns a BANNER command', () => {
|
||||
expect(formatUtils.banner('=', 10)).toEqual([Command.BANNER, '=', 10]);
|
||||
});
|
||||
|
||||
describe('formatCheckbox', () => {
|
||||
it('should format text with checkbox', () => {
|
||||
const formatted = formatUtils.formatCheckbox('Test Item');
|
||||
expect(formatted).toBe('[ ] Test Item');
|
||||
});
|
||||
|
||||
it('should handle empty text', () => {
|
||||
const formatted = formatUtils.formatCheckbox('');
|
||||
expect(formatted).toBe('[ ] ');
|
||||
});
|
||||
|
||||
it('should handle text with special characters', () => {
|
||||
const formatted = formatUtils.formatCheckbox('Test: Item (123)');
|
||||
expect(formatted).toBe('[ ] Test: Item (123)');
|
||||
});
|
||||
it('checkbox returns a CHECKBOX command', () => {
|
||||
expect(formatUtils.checkbox('Test')).toEqual([Command.CHECKBOX, 'Test']);
|
||||
});
|
||||
|
||||
describe('formatList', () => {
|
||||
it('should format list without numbering', () => {
|
||||
const items = ['Item 1', 'Item 2', 'Item 3'];
|
||||
const formatted = formatUtils.formatList(items);
|
||||
expect(formatted).toEqual([
|
||||
'[ ] Item 1',
|
||||
'[ ] Item 2',
|
||||
'[ ] Item 3'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should format list with numbering', () => {
|
||||
const items = ['Item 1', 'Item 2', 'Item 3'];
|
||||
const formatted = formatUtils.formatList(items, 1);
|
||||
expect(formatted).toEqual([
|
||||
'[ ] 1: Item 1',
|
||||
'[ ] 2: Item 2',
|
||||
'[ ] 3: Item 3'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty list', () => {
|
||||
const formatted = formatUtils.formatList([]);
|
||||
expect(formatted).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle list with empty items', () => {
|
||||
const items = ['', 'Item 2', ''];
|
||||
const formatted = formatUtils.formatList(items);
|
||||
expect(formatted).toEqual([
|
||||
'[ ] ',
|
||||
'[ ] Item 2',
|
||||
'[ ] '
|
||||
]);
|
||||
});
|
||||
it('list returns a LIST command', () => {
|
||||
expect(formatUtils.list(['A', 'B'], 1, '- ')).toEqual([[Command.LIST, ['A', 'B'], 1, '- ']]);
|
||||
});
|
||||
|
||||
describe('formatSection', () => {
|
||||
it('should format section with default banner', () => {
|
||||
const section = formatUtils.formatSection('Header', 'Content');
|
||||
expect(section).toEqual([
|
||||
'[ ] Header',
|
||||
'='.repeat(40),
|
||||
'Content',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should format section with custom banner', () => {
|
||||
const section = formatUtils.formatSection('Header', 'Content', '-');
|
||||
expect(section).toEqual([
|
||||
'[ ] Header',
|
||||
'-'.repeat(40),
|
||||
'Content',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty content', () => {
|
||||
const section = formatUtils.formatSection('Header', '');
|
||||
expect(section).toEqual([
|
||||
'[ ] Header',
|
||||
'='.repeat(40),
|
||||
'',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty header', () => {
|
||||
const section = formatUtils.formatSection('', 'Content');
|
||||
expect(section).toEqual([
|
||||
'[ ] ',
|
||||
'='.repeat(40),
|
||||
'Content',
|
||||
]);
|
||||
});
|
||||
it('stepHeader returns a CHECKBOX command with formatted text', () => {
|
||||
expect(formatUtils.stepHeader('StepName', 2, 'TaskName', true)).toEqual([Command.CHECKBOX, 'Step 2: StepName']);
|
||||
expect(formatUtils.stepHeader('StepName', 2, 'TaskName', false)).toEqual([Command.CHECKBOX, 'Step 2 of TaskName: StepName']);
|
||||
});
|
||||
|
||||
describe('formatStepHeader', () => {
|
||||
it('should format step header with just step name and number', () => {
|
||||
const header = formatUtils.formatStepHeader('Test Step', 1);
|
||||
expect(header).toBe('Step 1: Test Step');
|
||||
});
|
||||
it('section returns a list of commands', () => {
|
||||
expect(formatUtils.section('Header', 'Content')).toEqual([
|
||||
[Command.CHECKBOX, 'Header'],
|
||||
[Command.BANNER, '='],
|
||||
[Command.TEXT, 'Content'],
|
||||
]);
|
||||
expect(formatUtils.section('Header', 'Content', '-', true)).toEqual([
|
||||
[Command.CHECKBOX, 'Header'],
|
||||
[Command.BANNER, '-'],
|
||||
[Command.TEXT, 'Content'],
|
||||
[Command.NEWLINE],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should format step header with step number and task name in single step view', () => {
|
||||
const header = formatUtils.formatStepHeader('Test Step', 1, 'Test Task');
|
||||
expect(header).toBe('Step 1 of Test Task: Test Step');
|
||||
});
|
||||
it('getCheckboxText extracts text from a CHECKBOX command', () => {
|
||||
expect(formatUtils.getCheckboxText([Command.CHECKBOX, 'Hello'])).toBe('Hello');
|
||||
expect(formatUtils.getCheckboxText([Command.BANNER, '='])).toBe('');
|
||||
});
|
||||
|
||||
it('should format step header with step number but no task name in task view', () => {
|
||||
const header = formatUtils.formatStepHeader('Test Step', 1, 'Test Task', true);
|
||||
expect(header).toBe('Step 1: Test Step');
|
||||
});
|
||||
it('taskHeader returns a CHECKBOX command for the task', () => {
|
||||
expect(formatUtils.taskHeader('My Task')).toEqual([Command.CHECKBOX, 'Task: My Task']);
|
||||
});
|
||||
|
||||
it('should handle empty step name', () => {
|
||||
const header = formatUtils.formatStepHeader('', 1);
|
||||
expect(header).toBe('Step 1: ');
|
||||
});
|
||||
|
||||
it('should handle zero step number', () => {
|
||||
const header = formatUtils.formatStepHeader('Test Step', 0);
|
||||
expect(header).toBe('Step 0: Test Step');
|
||||
});
|
||||
it('taskSection returns a list of commands for the task section', () => {
|
||||
expect(formatUtils.taskSection('My Task')).toEqual([
|
||||
[Command.CHECKBOX, 'Task: My Task'],
|
||||
[Command.BANNER, '='],
|
||||
]);
|
||||
});
|
||||
});
|
@ -3,6 +3,8 @@ import { TestPrinter } from '../index';
|
||||
import { Task, Step } from '@shared/index';
|
||||
import { InMemoryPrintHistoryRepository, InMemoryStepRepository } from '../../db/repositories/in-memory-repository';
|
||||
import { Knex } from 'knex';
|
||||
import { CommandBuilder } from '../printer-commands';
|
||||
import { TestCommandExecutor } from '../command-executor';
|
||||
|
||||
describe('Printer', () => {
|
||||
let printHistoryRepo: InMemoryPrintHistoryRepository;
|
||||
@ -40,4 +42,113 @@ describe('Printer', () => {
|
||||
printed_at: expect.any(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command System', () => {
|
||||
let commandExecutor: TestCommandExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
commandExecutor = new TestCommandExecutor();
|
||||
});
|
||||
|
||||
it('should execute text commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.text('Hello World'),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.text('Test'),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('Hello World');
|
||||
expect(output).toContain('Test');
|
||||
});
|
||||
|
||||
it('should execute header commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.header('Test Header'),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Test Header');
|
||||
});
|
||||
|
||||
it('should execute banner commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.banner('=', 10),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('==========');
|
||||
});
|
||||
|
||||
it('should execute barcode commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.barcode('123456'),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[BARCODE: 123456]');
|
||||
});
|
||||
|
||||
it('should execute section commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.section('Test Section', 'Test Content', '-', true),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Test Section');
|
||||
expect(output).toContain('Test Content');
|
||||
});
|
||||
|
||||
it('should execute step header commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.stepHeader('Test Step', 1, 'Test Task', true),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Step 1: Test Step');
|
||||
});
|
||||
|
||||
it('should execute cut commands', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.cut(true, 4),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('--- CUT ---');
|
||||
});
|
||||
|
||||
it('should execute complex command sequences', async () => {
|
||||
const commands = [
|
||||
CommandBuilder.header('Complex Test'),
|
||||
CommandBuilder.banner('=', 20),
|
||||
CommandBuilder.text('Some content'),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.barcode('123'),
|
||||
CommandBuilder.cut(),
|
||||
];
|
||||
|
||||
await commandExecutor.executeCommands(commands);
|
||||
const output = commandExecutor.getOutput();
|
||||
|
||||
expect(output).toContain('[ ] Complex Test');
|
||||
expect(output).toContain('====================');
|
||||
expect(output).toContain('Some content');
|
||||
expect(output).toContain('[BARCODE: 123]');
|
||||
expect(output).toContain('--- CUT ---');
|
||||
});
|
||||
});
|
51
server/src/printer/__tests__/step-data-debug.test.ts
Normal file
51
server/src/printer/__tests__/step-data-debug.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Step } from '@shared/index';
|
||||
|
||||
describe('Step Data Debug', () => {
|
||||
it('should test step data structure', () => {
|
||||
// Test with sample step data that matches the database
|
||||
const step: Step = {
|
||||
id: 10,
|
||||
name: 'Counter',
|
||||
instructions: 'Was hit',
|
||||
task_id: 20,
|
||||
order: 1,
|
||||
print_count: 9,
|
||||
created_at: new Date('2025-06-15T01:55:41.121Z'),
|
||||
updated_at: new Date('2025-06-15T01:55:41.121Z'),
|
||||
};
|
||||
|
||||
console.log(`Step ${step.id}:`);
|
||||
console.log(` name: "${step.name}"`);
|
||||
console.log(` instructions: "${step.instructions}"`);
|
||||
console.log(` task_id: ${step.task_id}`);
|
||||
console.log(` order: ${step.order}`);
|
||||
console.log(` print_count: ${step.print_count}`);
|
||||
console.log(` created_at: ${step.created_at}`);
|
||||
console.log(` updated_at: ${step.updated_at}`);
|
||||
|
||||
expect(step.instructions).toBe('Was hit');
|
||||
expect(typeof step.instructions).toBe('string');
|
||||
});
|
||||
|
||||
it('should test step data with different instructions', () => {
|
||||
const step: Step = {
|
||||
id: 11,
|
||||
name: 'Sink',
|
||||
instructions: 'asdf',
|
||||
task_id: 20,
|
||||
order: 2,
|
||||
print_count: 2,
|
||||
created_at: new Date('2025-06-15T01:55:46.466Z'),
|
||||
updated_at: new Date('2025-06-15T01:55:46.466Z'),
|
||||
};
|
||||
|
||||
console.log(`Step ${step.id}:`);
|
||||
console.log(` name: "${step.name}"`);
|
||||
console.log(` instructions: "${step.instructions}"`);
|
||||
console.log(` task_id: ${step.task_id}`);
|
||||
console.log(` order: ${step.order}`);
|
||||
|
||||
expect(step.instructions).toBe('asdf');
|
||||
expect(typeof step.instructions).toBe('string');
|
||||
});
|
||||
});
|
106
server/src/printer/barcode-utils.ts
Normal file
106
server/src/printer/barcode-utils.ts
Normal file
@ -0,0 +1,106 @@
|
||||
export interface BarcodeData {
|
||||
type: 'TASK' | 'STEP';
|
||||
id: number;
|
||||
}
|
||||
|
||||
export class BarcodeUtils {
|
||||
// Entity type codes - using numeric prefixes
|
||||
private static readonly TASK_PREFIX = 1;
|
||||
private static readonly STEP_PREFIX = 2;
|
||||
|
||||
// Format: [entity_type][padding][id]
|
||||
// Example: 100000123 for task 123, 200000456 for step 456
|
||||
private static readonly ID_PADDING = 4; // 4 digits for ID
|
||||
|
||||
/**
|
||||
* Parse barcode data to extract entity type and ID
|
||||
* @param barcodeData The raw barcode data (e.g., "100000123" or "200000456")
|
||||
* @returns Parsed barcode data with type and ID, or null if invalid
|
||||
*/
|
||||
static parseBarcode(barcodeData: string): BarcodeData | null {
|
||||
const numericData = barcodeData.replace(/\D/g, ''); // Remove non-digits
|
||||
|
||||
if (numericData.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entityType = parseInt(numericData.charAt(0), 10);
|
||||
const id = parseInt(numericData.substring(1), 10);
|
||||
|
||||
if (isNaN(id) || id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entityType === this.TASK_PREFIX) {
|
||||
return { type: 'TASK', id };
|
||||
} else if (entityType === this.STEP_PREFIX) {
|
||||
return { type: 'STEP', id };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate barcode data for a task
|
||||
* @param taskId The task ID
|
||||
* @returns Formatted barcode data (numeric only)
|
||||
*/
|
||||
static generateTaskBarcode(taskId: number): string {
|
||||
return `${this.TASK_PREFIX}${taskId.toString().padStart(this.ID_PADDING, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate barcode data for a step
|
||||
* @param stepId The step ID
|
||||
* @returns Formatted barcode data (numeric only)
|
||||
*/
|
||||
static generateStepBarcode(stepId: number): string {
|
||||
return `${this.STEP_PREFIX}${stepId.toString().padStart(this.ID_PADDING, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a barcode data string is valid
|
||||
* @param barcodeData The barcode data to validate
|
||||
* @returns True if valid, false otherwise
|
||||
*/
|
||||
static isValidBarcode(barcodeData: string): boolean {
|
||||
return this.parseBarcode(barcodeData) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity type from barcode data
|
||||
* @param barcodeData The barcode data
|
||||
* @returns The entity type or null if invalid
|
||||
*/
|
||||
static getEntityType(barcodeData: string): 'TASK' | 'STEP' | null {
|
||||
const parsed = this.parseBarcode(barcodeData);
|
||||
return parsed?.type || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity ID from barcode data
|
||||
* @param barcodeData The barcode data
|
||||
* @returns The entity ID or null if invalid
|
||||
*/
|
||||
static getEntityId(barcodeData: string): number | null {
|
||||
const parsed = this.parseBarcode(barcodeData);
|
||||
return parsed?.id || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum supported ID for the current format
|
||||
* @returns Maximum ID value
|
||||
*/
|
||||
static getMaxId(): number {
|
||||
return Math.pow(10, this.ID_PADDING) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ID is within the supported range
|
||||
* @param id The ID to check
|
||||
* @returns True if valid, false otherwise
|
||||
*/
|
||||
static isValidId(id: number): boolean {
|
||||
return id > 0 && id <= this.getMaxId();
|
||||
}
|
||||
}
|
268
server/src/printer/command-executor.ts
Normal file
268
server/src/printer/command-executor.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import { Printer as EscposPrinter } from '@node-escpos/core';
|
||||
import { formatUtils } from './format-utils';
|
||||
import { Command, CommandTuple, CommandArray } from './printer-commands';
|
||||
import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG } from './printer-constants';
|
||||
import logger from '../logger';
|
||||
|
||||
export interface CommandExecutor {
|
||||
executeCommands(commands: CommandArray): Promise<void>;
|
||||
}
|
||||
|
||||
export class SerialCommandExecutor implements CommandExecutor {
|
||||
constructor(private printer: EscposPrinter<[]>) {}
|
||||
|
||||
async executeCommands(commands: CommandArray): Promise<void> {
|
||||
for (const command of commands) {
|
||||
await this.executeCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeCommand(commandTuple: CommandTuple): Promise<void> {
|
||||
const [command, ...params] = commandTuple;
|
||||
|
||||
switch (command) {
|
||||
case Command.TEXT:
|
||||
await this.printer.text(params[0] as string);
|
||||
break;
|
||||
|
||||
case Command.HEADER:
|
||||
await this.printer
|
||||
.font(FONT.DEFAULT)
|
||||
.align(ALIGNMENT.CENTER)
|
||||
.style(STYLE.BOLD)
|
||||
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
|
||||
.text(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
|
||||
break;
|
||||
|
||||
case Command.BANNER:
|
||||
const char = params[0] as string;
|
||||
const length = params[1] as number;
|
||||
await this.printer.text(formatUtils.bannerString(char, length));
|
||||
break;
|
||||
|
||||
case Command.NEWLINE:
|
||||
await this.printer.text('');
|
||||
break;
|
||||
|
||||
case Command.BARCODE:
|
||||
const barcodeData = params[0] as string;
|
||||
logger.info(`Printing barcode: ${barcodeData} with type: ${BARCODE_CONFIG.TYPE}`);
|
||||
|
||||
// Try multiple barcode formats until one works
|
||||
const barcodeTypes = [BARCODE_CONFIG.TYPE, ...BARCODE_CONFIG.ALTERNATIVE_TYPES];
|
||||
let barcodePrinted = false;
|
||||
|
||||
for (const barcodeType of barcodeTypes) {
|
||||
try {
|
||||
logger.info(`Trying barcode type: ${barcodeType}`);
|
||||
await this.printer.barcode(
|
||||
barcodeData,
|
||||
barcodeType,
|
||||
BARCODE_CONFIG.DIMENSIONS
|
||||
);
|
||||
logger.info(`Successfully printed barcode with type: ${barcodeType}`);
|
||||
barcodePrinted = true;
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(`Barcode type ${barcodeType} failed: ${error}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!barcodePrinted) {
|
||||
logger.error(`All barcode types failed for data: ${barcodeData}`);
|
||||
// As a last resort, just print the data as text
|
||||
await this.printer.text(`[BARCODE: ${barcodeData}]`);
|
||||
}
|
||||
break;
|
||||
|
||||
case Command.FONT_SIZE:
|
||||
const size = params[0] as typeof FONT_SIZES[keyof typeof FONT_SIZES];
|
||||
await this.printer.size(size.width, size.height);
|
||||
break;
|
||||
|
||||
case Command.ALIGN:
|
||||
await this.printer.align(params[0] as any);
|
||||
break;
|
||||
|
||||
case Command.FONT_FAMILY:
|
||||
await this.printer.font(params[0] as any);
|
||||
break;
|
||||
|
||||
case Command.STYLE:
|
||||
await this.printer.style(params[0] as any);
|
||||
break;
|
||||
|
||||
case Command.CUT:
|
||||
const partial = params[0] as boolean;
|
||||
const lines = params[1] as number;
|
||||
await this.printer.cut(partial, lines);
|
||||
break;
|
||||
|
||||
case Command.SECTION:
|
||||
const header = params[0] as string;
|
||||
const content = params[1] as string;
|
||||
const bannerChar = params[2] as string;
|
||||
const trailingNewline = params[3] as boolean;
|
||||
const section = formatUtils.section(header, content, bannerChar, trailingNewline);
|
||||
for (const cmd of section) {
|
||||
if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) {
|
||||
await this.printer.text(cmd[1] as string);
|
||||
} else if (cmd[0] === Command.BANNER) {
|
||||
await this.printer.text(formatUtils.bannerString(cmd[1] as string, cmd[2] as number));
|
||||
} else if (cmd[0] === Command.NEWLINE) {
|
||||
await this.printer.text('');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Command.STEP_HEADER:
|
||||
const stepName = params[0] as string;
|
||||
const stepNumber = params[1] as number;
|
||||
const taskName = params[2] as string;
|
||||
const isTaskView = params[3] as boolean;
|
||||
const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView);
|
||||
await this.printer.text(formatUtils.getCheckboxText(stepHeader));
|
||||
break;
|
||||
|
||||
case Command.CHECKBOX:
|
||||
const text = params[0] as string;
|
||||
const checkbox = `[ ] ${text}`;
|
||||
await this.printer.text(checkbox);
|
||||
break;
|
||||
|
||||
case Command.LIST:
|
||||
const items = params[0] as string[];
|
||||
const startIndex = params[1] as number;
|
||||
const prefix = params[2] as string;
|
||||
const listCmds = formatUtils.list(items, startIndex, prefix);
|
||||
for (const cmd of listCmds) {
|
||||
if (cmd[0] === Command.LIST) {
|
||||
// Render as text for test printer, or as needed for real printer
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const num = startIndex > 0 ? `${i + startIndex}: ` : '';
|
||||
const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`));
|
||||
await this.printer.text(`${prefix}${body}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TestCommandExecutor implements CommandExecutor {
|
||||
private output: string[] = [];
|
||||
|
||||
async executeCommands(commands: CommandArray): Promise<void> {
|
||||
this.output = [];
|
||||
|
||||
for (const command of commands) {
|
||||
await this.executeCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeCommand(commandTuple: CommandTuple): Promise<void> {
|
||||
const [command, ...params] = commandTuple;
|
||||
|
||||
switch (command) {
|
||||
case Command.TEXT:
|
||||
this.output.push(params[0] as string);
|
||||
break;
|
||||
|
||||
case Command.HEADER:
|
||||
this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
|
||||
break;
|
||||
|
||||
case Command.BANNER:
|
||||
const char = params[0] as string;
|
||||
const length = params[1] as number;
|
||||
this.output.push(formatUtils.banner(char, length)[1] as string);
|
||||
break;
|
||||
|
||||
case Command.NEWLINE:
|
||||
this.output.push('');
|
||||
break;
|
||||
|
||||
case Command.BARCODE:
|
||||
this.output.push(`[BARCODE: ${params[0] as string}]`);
|
||||
break;
|
||||
|
||||
case Command.FONT_SIZE:
|
||||
// Test printer ignores font size changes
|
||||
break;
|
||||
|
||||
case Command.ALIGN:
|
||||
// Test printer ignores alignment changes
|
||||
break;
|
||||
|
||||
case Command.FONT_FAMILY:
|
||||
// Test printer ignores font changes
|
||||
break;
|
||||
|
||||
case Command.STYLE:
|
||||
// Test printer ignores style changes
|
||||
break;
|
||||
|
||||
case Command.CUT:
|
||||
this.output.push('--- CUT ---');
|
||||
break;
|
||||
|
||||
case Command.SECTION:
|
||||
const header = params[0] as string;
|
||||
const content = params[1] as string;
|
||||
const bannerChar = params[2] as string;
|
||||
const trailingNewline = params[3] as boolean;
|
||||
const section = formatUtils.section(header, content, bannerChar, trailingNewline);
|
||||
for (const cmd of section) {
|
||||
if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) {
|
||||
this.output.push(cmd[1] as string);
|
||||
} else if (cmd[0] === Command.BANNER) {
|
||||
this.output.push(formatUtils.bannerString(cmd[1] as string, cmd[2] as number));
|
||||
} else if (cmd[0] === Command.NEWLINE) {
|
||||
this.output.push('');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Command.STEP_HEADER:
|
||||
const stepName = params[0] as string;
|
||||
const stepNumber = params[1] as number;
|
||||
const taskName = params[2] as string;
|
||||
const isTaskView = params[3] as boolean;
|
||||
const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView);
|
||||
this.output.push(formatUtils.getCheckboxText(stepHeader));
|
||||
break;
|
||||
|
||||
case Command.CHECKBOX:
|
||||
this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string)));
|
||||
break;
|
||||
|
||||
case Command.LIST:
|
||||
const items = params[0] as string[];
|
||||
const startIndex = params[1] as number;
|
||||
const prefix = params[2] as string;
|
||||
const listCmds = formatUtils.list(items, startIndex, prefix);
|
||||
for (const cmd of listCmds) {
|
||||
if (cmd[0] === Command.LIST) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const num = startIndex > 0 ? `${i + startIndex}: ` : '';
|
||||
const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`));
|
||||
this.output.push(`${prefix}${body}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
getOutput(): string {
|
||||
return this.output.join('\n') + '\n';
|
||||
}
|
||||
}
|
@ -1,48 +1,39 @@
|
||||
import { PAPER_CONFIG } from "./printer-constants";
|
||||
import { Command, CommandTuple } from './printer-commands';
|
||||
|
||||
export const formatUtils = {
|
||||
/**
|
||||
* Creates a banner line with the specified character
|
||||
* @param char Character to repeat
|
||||
* @param length Length of the banner
|
||||
* @returns Formatted banner string
|
||||
* Creates a banner command
|
||||
*/
|
||||
createBanner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string {
|
||||
banner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): CommandTuple {
|
||||
return [Command.BANNER, char, length];
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a banner string (repeated character)
|
||||
*/
|
||||
bannerString(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string {
|
||||
return char.repeat(length);
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a checkbox item with the given text
|
||||
* @param text Text to display after the checkbox
|
||||
* @returns Formatted checkbox string
|
||||
* Checkbox command
|
||||
*/
|
||||
formatCheckbox(text: string): string {
|
||||
return `[ ] ${text}`;
|
||||
checkbox(text: string): CommandTuple {
|
||||
return [Command.CHECKBOX, text];
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a list of items into lines with optional numbering
|
||||
* @param items List of items to format
|
||||
* @param startIndex Starting index for numbering (0 for no numbers)
|
||||
* @returns Array of formatted lines
|
||||
* List of items as commands
|
||||
*/
|
||||
formatList(items: string[], startIndex: number = 0, prefix: string = ''): string[] {
|
||||
return items.map((item, index) => {
|
||||
const num = startIndex > 0 ? `${index + startIndex}: ` : '';
|
||||
const body = this.formatCheckbox(`${num}${item}`);
|
||||
return `${prefix}${body}`;
|
||||
});
|
||||
list(items: string[], startIndex: number = 0, prefix: string = ''): CommandTuple[] {
|
||||
return [[Command.LIST, items, startIndex, prefix]];
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a step header with task context
|
||||
* @param stepName Name of the step
|
||||
* @param stepNumber Step number (1-based)
|
||||
* @param taskName Name of the parent task (only used in single step view)
|
||||
* @param isTaskView Whether this is being used in a task view
|
||||
* @returns Formatted step header
|
||||
* Step header as a command
|
||||
*/
|
||||
formatStepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): string {
|
||||
stepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): CommandTuple {
|
||||
const parts = ['Step'];
|
||||
if (stepNumber !== undefined) {
|
||||
parts.push(' ');
|
||||
@ -53,25 +44,48 @@ export const formatUtils = {
|
||||
}
|
||||
parts.push(': ');
|
||||
parts.push(stepName);
|
||||
return parts.join('');
|
||||
return [Command.CHECKBOX, parts.join('')];
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats a section with a header and content
|
||||
* @param header Section header
|
||||
* @param content Section content
|
||||
* @param bannerChar Character to use for the banner
|
||||
* @returns Array of formatted lines
|
||||
* Section as a list of commands
|
||||
*/
|
||||
formatSection(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): string[] {
|
||||
const parts = [
|
||||
this.formatCheckbox(header),
|
||||
this.createBanner(bannerChar),
|
||||
content,
|
||||
section(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): CommandTuple[] {
|
||||
const commands: CommandTuple[] = [
|
||||
[Command.CHECKBOX, header],
|
||||
[Command.BANNER, bannerChar],
|
||||
[Command.TEXT, content],
|
||||
];
|
||||
if (trailingNewline) {
|
||||
parts.push('');
|
||||
commands.push([Command.NEWLINE]);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
return commands;
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts the text from a CHECKBOX CommandTuple
|
||||
*/
|
||||
getCheckboxText(cmd: CommandTuple): string {
|
||||
if (cmd[0] === Command.CHECKBOX && typeof cmd[1] === 'string') {
|
||||
return cmd[1];
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Task header as a command
|
||||
*/
|
||||
taskHeader(taskName: string): CommandTuple {
|
||||
return [Command.CHECKBOX, `Task: ${taskName}`];
|
||||
},
|
||||
|
||||
/**
|
||||
* Task section as a list of commands
|
||||
*/
|
||||
taskSection(taskName: string): CommandTuple[] {
|
||||
return [
|
||||
[Command.CHECKBOX, `Task: ${taskName}`],
|
||||
[Command.BANNER, '='],
|
||||
];
|
||||
},
|
||||
};
|
@ -16,3 +16,6 @@ export function createPrinter(printHistoryRepo: PrintHistoryRepository, stepRepo
|
||||
|
||||
export { SerialPrinter } from './serial-printer';
|
||||
export { TestPrinter } from './test-printer';
|
||||
export { CommandBuilder, Command, type CommandArray, type CommandTuple } from './printer-commands';
|
||||
export { SerialCommandExecutor, TestCommandExecutor, type CommandExecutor } from './command-executor';
|
||||
export { BarcodeUtils, type BarcodeData } from './barcode-utils';
|
||||
|
148
server/src/printer/printer-commands.ts
Normal file
148
server/src/printer/printer-commands.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG, PAPER_CONFIG } from './printer-constants';
|
||||
import { BarcodeUtils } from './barcode-utils';
|
||||
|
||||
// Command types for printer operations
|
||||
export enum Command {
|
||||
// Text commands
|
||||
TEXT = 'TEXT',
|
||||
HEADER = 'HEADER',
|
||||
BANNER = 'BANNER',
|
||||
NEWLINE = 'NEWLINE',
|
||||
|
||||
// Barcode commands
|
||||
BARCODE = 'BARCODE',
|
||||
|
||||
// Formatting commands
|
||||
FONT_SIZE = 'FONT_SIZE',
|
||||
ALIGN = 'ALIGN',
|
||||
FONT_FAMILY = 'FONT_FAMILY',
|
||||
STYLE = 'STYLE',
|
||||
|
||||
// Paper commands
|
||||
CUT = 'CUT',
|
||||
|
||||
// Section formatting
|
||||
SECTION = 'SECTION',
|
||||
STEP_HEADER = 'STEP_HEADER',
|
||||
CHECKBOX = 'CHECKBOX',
|
||||
|
||||
// List formatting
|
||||
LIST = 'LIST',
|
||||
}
|
||||
|
||||
// Command parameter types
|
||||
export type FontSize = typeof FONT_SIZES[keyof typeof FONT_SIZES];
|
||||
export type Alignment = typeof ALIGNMENT[keyof typeof ALIGNMENT];
|
||||
export type FontFamily = typeof FONT[keyof typeof FONT];
|
||||
export type TextStyle = typeof STYLE[keyof typeof STYLE];
|
||||
|
||||
// Command tuple type - [Command, ...parameters]
|
||||
export type CommandTuple =
|
||||
| [Command.TEXT, string]
|
||||
| [Command.HEADER, string]
|
||||
| [Command.BANNER, string, number?]
|
||||
| [Command.NEWLINE]
|
||||
| [Command.BARCODE, string]
|
||||
| [Command.FONT_SIZE, FontSize]
|
||||
| [Command.ALIGN, Alignment]
|
||||
| [Command.FONT_FAMILY, FontFamily]
|
||||
| [Command.STYLE, TextStyle]
|
||||
| [Command.CUT, boolean, number?]
|
||||
| [Command.SECTION, string, string, string?, boolean?]
|
||||
| [Command.STEP_HEADER, string, number, string?, boolean?]
|
||||
| [Command.CHECKBOX, string]
|
||||
| [Command.LIST, string[], number?, string?];
|
||||
|
||||
// Command array type
|
||||
export type CommandArray = CommandTuple[];
|
||||
|
||||
// Command builder utility
|
||||
export class CommandBuilder {
|
||||
static text(text: string): CommandTuple {
|
||||
return [Command.TEXT, text];
|
||||
}
|
||||
|
||||
static header(text: string): CommandTuple {
|
||||
return [Command.HEADER, text];
|
||||
}
|
||||
|
||||
static banner(char: string = '=', length?: number): CommandTuple {
|
||||
return [Command.BANNER, char, length];
|
||||
}
|
||||
|
||||
static newline(): CommandTuple {
|
||||
return [Command.NEWLINE];
|
||||
}
|
||||
|
||||
static barcode(data: string): CommandTuple {
|
||||
return [Command.BARCODE, data];
|
||||
}
|
||||
|
||||
static taskBarcode(taskId: number): CommandTuple {
|
||||
return [Command.BARCODE, BarcodeUtils.generateTaskBarcode(taskId)];
|
||||
}
|
||||
|
||||
static stepBarcode(stepId: number): CommandTuple {
|
||||
return [Command.BARCODE, BarcodeUtils.generateStepBarcode(stepId)];
|
||||
}
|
||||
|
||||
static barcodeWithAlignment(data: string, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] {
|
||||
return [
|
||||
[Command.ALIGN, alignment],
|
||||
[Command.BARCODE, data],
|
||||
[Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment
|
||||
];
|
||||
}
|
||||
|
||||
static taskBarcodeWithAlignment(taskId: number, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] {
|
||||
return [
|
||||
[Command.ALIGN, alignment],
|
||||
[Command.BARCODE, BarcodeUtils.generateTaskBarcode(taskId)],
|
||||
[Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment
|
||||
];
|
||||
}
|
||||
|
||||
static stepBarcodeWithAlignment(stepId: number, alignment: Alignment = ALIGNMENT.CENTER): CommandTuple[] {
|
||||
return [
|
||||
[Command.ALIGN, alignment],
|
||||
[Command.BARCODE, BarcodeUtils.generateStepBarcode(stepId)],
|
||||
[Command.ALIGN, ALIGNMENT.LEFT], // Reset to left alignment
|
||||
];
|
||||
}
|
||||
|
||||
static fontSize(size: FontSize): CommandTuple {
|
||||
return [Command.FONT_SIZE, size];
|
||||
}
|
||||
|
||||
static align(alignment: Alignment): CommandTuple {
|
||||
return [Command.ALIGN, alignment];
|
||||
}
|
||||
|
||||
static font(font: FontFamily): CommandTuple {
|
||||
return [Command.FONT_FAMILY, font];
|
||||
}
|
||||
|
||||
static style(style: TextStyle): CommandTuple {
|
||||
return [Command.STYLE, style];
|
||||
}
|
||||
|
||||
static cut(partial: boolean = true, lines: number = PAPER_CONFIG.CUT_LINES): CommandTuple {
|
||||
return [Command.CUT, partial, lines];
|
||||
}
|
||||
|
||||
static section(header: string, content: string, bannerChar: string = '=', trailingNewline: boolean = false): CommandTuple {
|
||||
return [Command.SECTION, header, content, bannerChar, trailingNewline];
|
||||
}
|
||||
|
||||
static stepHeader(stepName: string, stepNumber: number, taskName?: string, isTaskView: boolean = false): CommandTuple {
|
||||
return [Command.STEP_HEADER, stepName, stepNumber, taskName, isTaskView];
|
||||
}
|
||||
|
||||
static checkbox(text: string): CommandTuple {
|
||||
return [Command.CHECKBOX, text];
|
||||
}
|
||||
|
||||
static list(items: string[], startIndex: number = 0, prefix: string = ''): CommandTuple {
|
||||
return [Command.LIST, items, startIndex, prefix];
|
||||
}
|
||||
}
|
@ -5,13 +5,14 @@ export const PRINTER_CONFIG = {
|
||||
|
||||
export const FONT_SIZES = {
|
||||
NORMAL: { width: 1, height: 1 }, // 0.08 x 2.13 mm
|
||||
SMALL: { width: 1, height: 1 }, // Same as normal for now, but can be adjusted if needed
|
||||
SMALL: { width: 0.5, height: 0.5 }, // Half size
|
||||
LARGE: { width: 2, height: 2 }, // Double size
|
||||
} as const;
|
||||
|
||||
export const BARCODE_CONFIG = {
|
||||
TYPE: 'CODE128',
|
||||
TYPE: 'CODE39',
|
||||
DIMENSIONS: { width: 2, height: 50 },
|
||||
ALTERNATIVE_TYPES: ['CODE128', 'EAN13', 'UPC_A'] as const,
|
||||
} as const;
|
||||
|
||||
export const PAPER_CONFIG = {
|
||||
|
@ -5,6 +5,8 @@ import { StepRepository, PrintHistoryRepository } from '../db/repositories';
|
||||
import { Knex } from 'knex';
|
||||
import logger from '../logger';
|
||||
import { formatUtils } from './format-utils';
|
||||
import { Command, CommandBuilder } from './printer-commands';
|
||||
import { SerialCommandExecutor } from './command-executor';
|
||||
import {
|
||||
PRINTER_CONFIG,
|
||||
FONT_SIZES,
|
||||
@ -20,6 +22,7 @@ export class SerialPrinter implements PrinterInterface {
|
||||
private printer: Printer<[]> | null = null;
|
||||
private printHistoryRepo: PrintHistoryRepository;
|
||||
private stepRepository: StepRepository;
|
||||
private commandExecutor: SerialCommandExecutor | null = null;
|
||||
|
||||
constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) {
|
||||
this.printHistoryRepo = printHistoryRepo;
|
||||
@ -32,6 +35,7 @@ export class SerialPrinter implements PrinterInterface {
|
||||
this.device = new USB();
|
||||
const options = { encoding: PRINTER_CONFIG.ENCODING };
|
||||
this.printer = new Printer(this.device, options);
|
||||
this.commandExecutor = new SerialCommandExecutor(this.printer);
|
||||
logger.info('Printer device initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize printer device:', error);
|
||||
@ -80,58 +84,49 @@ export class SerialPrinter implements PrinterInterface {
|
||||
}
|
||||
|
||||
async printTask(task: Task, db: Knex): Promise<void> {
|
||||
if (!this.printer || !this.device) {
|
||||
if (!this.commandExecutor) {
|
||||
throw new Error('Printer not initialized');
|
||||
}
|
||||
|
||||
const taskSteps = await this.getTaskSteps(db, task);
|
||||
logger.info(`Printing task ${task.id} with ${taskSteps.length} steps`);
|
||||
|
||||
try {
|
||||
await this.openPrinter();
|
||||
|
||||
// Print header with task ID as barcode
|
||||
await this.printer
|
||||
.font(FONT.DEFAULT)
|
||||
.align(ALIGNMENT.CENTER)
|
||||
.style(STYLE.BOLD)
|
||||
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
|
||||
.text(formatUtils.formatCheckbox(`Task: ${task.name}`))
|
||||
.text(formatUtils.createBanner('=', PAPER_CONFIG.BANNER_LENGTH))
|
||||
// .text('')
|
||||
.align(ALIGNMENT.LEFT);
|
||||
const commands = [
|
||||
// Header with task name
|
||||
CommandBuilder.header(`Task: ${task.name}`),
|
||||
CommandBuilder.banner('=', PAPER_CONFIG.BANNER_LENGTH),
|
||||
CommandBuilder.align(ALIGNMENT.LEFT),
|
||||
|
||||
// Print task ID as barcode
|
||||
await this.printer
|
||||
.barcode(task.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
|
||||
// .text('')
|
||||
// .text('');
|
||||
// Task ID as barcode
|
||||
...CommandBuilder.taskBarcodeWithAlignment(task.id),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
// Print steps
|
||||
// Add steps
|
||||
for (let i = 0; i < taskSteps.length; i++) {
|
||||
const step = taskSteps[i];
|
||||
const stepSection = formatUtils.formatSection(
|
||||
formatUtils.formatStepHeader(step.name, i + 1, task.name, true),
|
||||
step.instructions || 'No instructions provided',
|
||||
'-'
|
||||
logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
|
||||
|
||||
commands.push(
|
||||
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
|
||||
formatUtils.stepHeader(step.name, i + 1, task.name, true),
|
||||
CommandBuilder.banner('-', PAPER_CONFIG.BANNER_LENGTH),
|
||||
CommandBuilder.text(
|
||||
step.instructions || 'No instructions provided',
|
||||
),
|
||||
CommandBuilder.newline(),
|
||||
// ...CommandBuilder.stepBarcodeWithAlignment(step.id),
|
||||
CommandBuilder.newline()
|
||||
);
|
||||
|
||||
await this.printer
|
||||
.size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height)
|
||||
.text(stepSection[0])
|
||||
.text(stepSection[1])
|
||||
.size(FONT_SIZES.SMALL.width, FONT_SIZES.SMALL.height)
|
||||
.text(stepSection[3]);
|
||||
// .text('');
|
||||
|
||||
// Print step ID as barcode
|
||||
await this.printer
|
||||
.barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS);
|
||||
// .text('');
|
||||
}
|
||||
|
||||
await this.printer
|
||||
.cut(true, PAPER_CONFIG.CUT_LINES);
|
||||
commands.push(CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES));
|
||||
|
||||
await this.commandExecutor.executeCommands(commands);
|
||||
await this.closePrinter();
|
||||
|
||||
logger.info(`Printed task ${task.id}`);
|
||||
@ -148,10 +143,12 @@ export class SerialPrinter implements PrinterInterface {
|
||||
}
|
||||
|
||||
async printStep(step: Step, db: Knex): Promise<void> {
|
||||
if (!this.printer || !this.device) {
|
||||
if (!this.commandExecutor) {
|
||||
throw new Error('Printer not initialized');
|
||||
}
|
||||
|
||||
logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
|
||||
|
||||
try {
|
||||
await this.openPrinter();
|
||||
|
||||
@ -159,30 +156,31 @@ export class SerialPrinter implements PrinterInterface {
|
||||
const task = await this.stepRepository.findTaskById(step.id);
|
||||
const stepNumber = await this.stepRepository.findStepNumber(step.id);
|
||||
|
||||
const stepSection = formatUtils.formatSection(
|
||||
formatUtils.formatStepHeader(step.name, stepNumber, task?.name),
|
||||
step.instructions || 'No instructions provided'
|
||||
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, stepNumber, task?.name));
|
||||
const stepSection = formatUtils.section(
|
||||
headerText,
|
||||
step.instructions || 'No instructions provided',
|
||||
'-'
|
||||
);
|
||||
|
||||
await this.printer
|
||||
.font(FONT.DEFAULT)
|
||||
.align(ALIGNMENT.CENTER)
|
||||
.style(STYLE.BOLD)
|
||||
.size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height)
|
||||
.text(stepSection[0])
|
||||
.text(stepSection[1])
|
||||
.text('')
|
||||
.align(ALIGNMENT.LEFT)
|
||||
.size(FONT_SIZES.NORMAL.width, FONT_SIZES.NORMAL.height)
|
||||
.text(stepSection[3])
|
||||
.text('')
|
||||
.text('');
|
||||
|
||||
// Print step ID as barcode
|
||||
await this.printer
|
||||
.barcode(step.id.toString(), BARCODE_CONFIG.TYPE, BARCODE_CONFIG.DIMENSIONS)
|
||||
.cut(true, PAPER_CONFIG.CUT_LINES);
|
||||
const commands = [
|
||||
CommandBuilder.font(FONT.DEFAULT),
|
||||
CommandBuilder.align(ALIGNMENT.CENTER),
|
||||
CommandBuilder.style(STYLE.BOLD),
|
||||
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
|
||||
formatUtils.stepHeader(step.name, stepNumber, task?.name),
|
||||
// CommandBuilder.text(headerText),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.align(ALIGNMENT.LEFT),
|
||||
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
|
||||
CommandBuilder.text(step.instructions || 'No instructions provided'),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.newline(),
|
||||
...CommandBuilder.stepBarcodeWithAlignment(step.id),
|
||||
CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES),
|
||||
];
|
||||
|
||||
await this.commandExecutor.executeCommands(commands);
|
||||
await this.closePrinter();
|
||||
|
||||
logger.info(`Printed step ${step.id}`);
|
||||
|
@ -5,16 +5,27 @@ import { StepRepository, PrintHistoryRepository } from '../db/repositories';
|
||||
import { Knex } from 'knex';
|
||||
import logger from '../logger';
|
||||
import { formatUtils } from './format-utils';
|
||||
import { CommandBuilder } from './printer-commands';
|
||||
import { TestCommandExecutor } from './command-executor';
|
||||
import {
|
||||
PAPER_CONFIG,
|
||||
FONT_SIZES,
|
||||
ALIGNMENT,
|
||||
FONT,
|
||||
STYLE,
|
||||
} from './printer-constants';
|
||||
|
||||
export class TestPrinter implements Printer {
|
||||
private readonly outputDir: string;
|
||||
private printHistoryRepo: PrintHistoryRepository;
|
||||
private stepRepository: StepRepository;
|
||||
private commandExecutor: TestCommandExecutor;
|
||||
|
||||
constructor(printHistoryRepo: PrintHistoryRepository, stepRepo: StepRepository) {
|
||||
this.outputDir = path.join(process.cwd(), 'test-output');
|
||||
this.printHistoryRepo = printHistoryRepo;
|
||||
this.stepRepository = stepRepo;
|
||||
this.commandExecutor = new TestCommandExecutor();
|
||||
this.ensureOutputDir();
|
||||
}
|
||||
|
||||
@ -35,12 +46,41 @@ export class TestPrinter implements Printer {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = path.join(this.outputDir, `task-${task.id}-${timestamp}.txt`);
|
||||
|
||||
const content = [
|
||||
...formatUtils.formatSection(`Task: ${task.name}`, ''),
|
||||
...formatUtils.formatList(taskSteps.map((step, index) =>
|
||||
formatUtils.formatStepHeader(step.name, index + 1, task.name, true)
|
||||
), 0, '- '),
|
||||
].join('\n') + '\n';
|
||||
const commands = [
|
||||
// Header with task name
|
||||
CommandBuilder.header(`Task: ${task.name}`),
|
||||
CommandBuilder.banner('=', PAPER_CONFIG.BANNER_LENGTH),
|
||||
CommandBuilder.align(ALIGNMENT.LEFT),
|
||||
|
||||
// Task ID as barcode
|
||||
...CommandBuilder.taskBarcodeWithAlignment(task.id),
|
||||
CommandBuilder.newline(),
|
||||
];
|
||||
|
||||
// Add steps
|
||||
for (let i = 0; i < taskSteps.length; i++) {
|
||||
const step = taskSteps[i];
|
||||
logger.info(`Printing step ${step.id}: name="${step.name}", instructions="${step.instructions}"`);
|
||||
|
||||
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, i + 1, task.name, true));
|
||||
commands.push(
|
||||
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
|
||||
CommandBuilder.text(headerText),
|
||||
CommandBuilder.newline(),
|
||||
...formatUtils.section(
|
||||
'',
|
||||
step.instructions || 'No instructions provided',
|
||||
'-'
|
||||
),
|
||||
...CommandBuilder.stepBarcodeWithAlignment(step.id),
|
||||
CommandBuilder.newline()
|
||||
);
|
||||
}
|
||||
|
||||
commands.push(CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES));
|
||||
|
||||
await this.commandExecutor.executeCommands(commands);
|
||||
const content = this.commandExecutor.getOutput();
|
||||
|
||||
await fs.writeFile(filename, content);
|
||||
logger.info(`Printed task ${task.id} to ${filename}`);
|
||||
@ -61,10 +101,26 @@ export class TestPrinter implements Printer {
|
||||
const task = await this.stepRepository.findTaskById(step.id);
|
||||
const stepNumber = await this.stepRepository.findStepNumber(step.id);
|
||||
|
||||
const content = formatUtils.formatSection(
|
||||
formatUtils.formatStepHeader(step.name, stepNumber, task?.name),
|
||||
step.instructions
|
||||
).join('\n') + '\n';
|
||||
const headerText = formatUtils.getCheckboxText(formatUtils.stepHeader(step.name, stepNumber, task?.name));
|
||||
|
||||
const commands = [
|
||||
CommandBuilder.font(FONT.DEFAULT),
|
||||
CommandBuilder.align(ALIGNMENT.CENTER),
|
||||
CommandBuilder.style(STYLE.BOLD),
|
||||
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
|
||||
CommandBuilder.text(headerText),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.align(ALIGNMENT.LEFT),
|
||||
CommandBuilder.fontSize(FONT_SIZES.NORMAL),
|
||||
CommandBuilder.text(step.instructions || 'No instructions provided'),
|
||||
CommandBuilder.newline(),
|
||||
CommandBuilder.newline(),
|
||||
...CommandBuilder.stepBarcodeWithAlignment(step.id),
|
||||
CommandBuilder.cut(true, PAPER_CONFIG.CUT_LINES),
|
||||
];
|
||||
|
||||
await this.commandExecutor.executeCommands(commands);
|
||||
const content = this.commandExecutor.getOutput();
|
||||
|
||||
await fs.writeFile(filename, content);
|
||||
logger.info(`Printed step ${step.id} to ${filename}`);
|
||||
|
@ -10,7 +10,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@shared/*": ["../shared/types/*"]
|
||||
}
|
||||
},
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*", "../shared/types/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
Loading…
Reference in New Issue
Block a user