560 lines
19 KiB
Markdown
560 lines
19 KiB
Markdown
# ByRachel — Clothing Business Management
|
|
|
|
A full-featured web application for managing a small clothing business. Built with Next.js, optimized for deployment on a single VPS with Docker.
|
|
|
|
## Features
|
|
|
|
- **Product Catalog** — Products with variants (size/color), images, traffic light inventory system
|
|
- **Sales Management** — Full sales workflow: create, confirm, deliver, cancel, returns
|
|
- **Purchase Orders** — Supplier purchases with receive/confirm/cancel workflow
|
|
- **Inventory** — Movement-based stock tracking, restock suggestions, adjustments
|
|
- **Customers** — Customer management with tags and sale history
|
|
- **Suppliers** — Supplier management with product associations
|
|
- **Categories** — Hierarchical categories with color coding
|
|
- **Analytics** — Sales trends, revenue/COGS/profit KPIs, best sellers, seasonality, cost evolution, inventory health
|
|
- **Reports** — PDF generation for sales, purchases, inventory, customers, brand, analytics
|
|
- **Public Catalog** — Customer-facing product catalog at `/catalogo`
|
|
- **Settings** — Brand configuration, pricing rules, password management, TOTP 2FA
|
|
- **Audit Logging** — All critical operations are logged with before/after data
|
|
|
|
## Tech Stack
|
|
|
|
| Layer | Technology |
|
|
|-------|-----------|
|
|
| Framework | Next.js 16 (App Router) |
|
|
| Language | TypeScript (strict mode) |
|
|
| Styling | Tailwind CSS 4 |
|
|
| UI Components | shadcn/ui (Radix UI primitives) |
|
|
| Database | SQLite (better-sqlite3) |
|
|
| ORM | Drizzle ORM |
|
|
| Authentication | JWT (jose) + TOTP (otplib) + Argon2 password hashing |
|
|
| Charts | Recharts |
|
|
| PDF Generation | pdf-lib (client-side) |
|
|
| Image Processing | Sharp |
|
|
| Testing | Vitest |
|
|
| Drag & Drop | @dnd-kit |
|
|
| Validation | Zod |
|
|
| Container | Docker (multi-stage build) |
|
|
|
|
## Prerequisites
|
|
|
|
- **Node.js 22+** and npm (for development)
|
|
- **Docker & Docker Compose** (for container deployment)
|
|
- **A domain name** (for production HTTPS, optional for local use)
|
|
|
|
## Quick Start (Docker)
|
|
|
|
The fastest way to get ByRachel running:
|
|
|
|
```bash
|
|
# 1. Clone the repository
|
|
git clone <your-repo-url> byrachel
|
|
cd byrachel
|
|
|
|
# 2. Create environment file
|
|
cp .env.example .env
|
|
# Edit .env and change JWT_SECRET to a random secure value:
|
|
# openssl rand -base64 32
|
|
|
|
# 3. Start the application
|
|
docker compose up -d
|
|
|
|
# 4. Open in browser
|
|
# http://localhost:3000
|
|
# You'll be redirected to /setup to create your admin account
|
|
```
|
|
|
|
## Development Setup
|
|
|
|
```bash
|
|
# 1. Clone and install
|
|
git clone <your-repo-url> byrachel
|
|
cd byrachel
|
|
npm install
|
|
|
|
# 2. Set up environment
|
|
cp .env.example .env
|
|
|
|
# 3. Initialize the database
|
|
npm run db:push
|
|
|
|
# 4. (Optional) Seed sample data
|
|
npm run seed
|
|
|
|
# 5. Start development server
|
|
npm run dev
|
|
# Open http://localhost:3000
|
|
```
|
|
|
|
### Development Commands
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `npm run dev` | Start development server with hot reload |
|
|
| `npm run build` | Create production build |
|
|
| `npm run start` | Start production server |
|
|
| `npm run test` | Run all tests |
|
|
| `npm run test:watch` | Run tests in watch mode |
|
|
| `npm run test:coverage` | Run tests with coverage report |
|
|
| `npm run db:push` | Push schema changes to database |
|
|
| `npm run db:generate` | Generate migration files |
|
|
| `npm run db:migrate` | Run pending migrations |
|
|
| `npm run db:studio` | Open Drizzle Studio (database GUI) |
|
|
| `npm run seed` | Seed database with sample data |
|
|
|
|
## Environment Variables
|
|
|
|
| Variable | Description | Default | Required |
|
|
|----------|-------------|---------|----------|
|
|
| `DATABASE_URL` | SQLite database file path. Dev: `./data/db.sqlite`, Docker: `/app/data/db.sqlite` | `./data/db.sqlite` | Yes |
|
|
| `JWT_SECRET` | Secret key for JWT signing. **MUST be changed in production.** Generate with: `openssl rand -base64 32` | `change-me-to-a-256-bit-secret` | Yes |
|
|
| `UPLOAD_DIR` | Directory for uploaded images. Dev: `./public/uploads`, Docker: `/app/public/uploads` | `./public/uploads` | Yes |
|
|
| `STORAGE_ADAPTER` | Storage backend. Currently only `local` (filesystem) is supported | `local` | Yes |
|
|
| `NODE_ENV` | Node environment: `development` or `production` | `production` | Yes |
|
|
| `NEXT_TELEMETRY_DISABLED` | Disable Next.js telemetry. Set to `1` to disable | `1` | No |
|
|
|
|
## API Routes Reference
|
|
|
|
All API routes are under `/api/`. Admin routes require a valid JWT cookie (except `/api/health` and `/api/auth/*`).
|
|
|
|
### Authentication
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| POST | `/api/auth/setup` | Initial admin account creation (first run only) |
|
|
| POST | `/api/auth/login` | Login with email/password |
|
|
| POST | `/api/auth/verify-totp` | Verify TOTP code |
|
|
| GET | `/api/auth/me` | Get current user info |
|
|
| POST | `/api/auth/logout` | Logout (clear session) |
|
|
| PUT | `/api/auth/change-password` | Change password |
|
|
| POST | `/api/auth/regenerate-recovery-codes` | Generate new recovery codes |
|
|
|
|
### Products
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/POST | `/api/products` | List / Create products |
|
|
| GET/PUT/DELETE | `/api/products/[id]` | Get / Update / Delete product |
|
|
| POST | `/api/products/[id]/duplicate` | Duplicate a product |
|
|
| GET/POST | `/api/products/[id]/images` | List / Upload product images |
|
|
| DELETE | `/api/products/[id]/images/[imageId]` | Delete product image |
|
|
| PUT | `/api/products/[id]/images/reorder` | Reorder product images |
|
|
| GET/POST | `/api/products/[id]/variants` | List / Create variants |
|
|
| PUT/DELETE | `/api/products/[id]/variants/[variantId]` | Update / Delete variant |
|
|
| GET | `/api/products/[id]/variants/[variantId]/movements` | Get variant movements |
|
|
|
|
### Categories
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/POST | `/api/categories` | List / Create categories |
|
|
| PUT/DELETE | `/api/categories/[id]` | Update / Delete category |
|
|
|
|
### Sales
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/POST | `/api/sales` | List / Create sales |
|
|
| GET/PUT/DELETE | `/api/sales/[id]` | Get / Update / Delete sale |
|
|
| POST | `/api/sales/[id]/confirm` | Confirm sale |
|
|
| POST | `/api/sales/[id]/deliver` | Mark sale as delivered |
|
|
| POST | `/api/sales/[id]/cancel` | Cancel sale |
|
|
| POST | `/api/sales/[id]/return` | Process sale return |
|
|
|
|
### Purchases
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/POST | `/api/purchases` | List / Create purchases |
|
|
| GET/PUT/DELETE | `/api/purchases/[id]` | Get / Update / Delete purchase |
|
|
| POST | `/api/purchases/[id]/receive` | Receive purchase items |
|
|
| POST | `/api/purchases/[id]/confirm` | Confirm purchase |
|
|
| POST | `/api/purchases/[id]/cancel` | Cancel purchase |
|
|
|
|
### Inventory
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET | `/api/inventory` | Get inventory status |
|
|
| POST | `/api/inventory/restock` | Restock variants |
|
|
| POST | `/api/inventory/adjust` | Manual stock adjustment |
|
|
| GET | `/api/inventory/movements` | List stock movements |
|
|
|
|
### Customers
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/POST | `/api/customers` | List / Create customers |
|
|
| GET/PUT/DELETE | `/api/customers/[id]` | Get / Update / Delete customer |
|
|
| GET/POST | `/api/customers/[id]/tags` | List / Add customer tags |
|
|
| DELETE | `/api/customers/[id]/tags/[tagId]` | Remove customer tag |
|
|
| GET | `/api/customer-tags` | List all customer tags |
|
|
|
|
### Suppliers
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/POST | `/api/suppliers` | List / Create suppliers |
|
|
| GET/PUT/DELETE | `/api/suppliers/[id]` | Get / Update / Delete supplier |
|
|
| GET | `/api/suppliers/[id]/products` | Get supplier's products |
|
|
|
|
### Analytics
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET | `/api/analytics/summary` | Sales summary KPIs |
|
|
| GET | `/api/analytics/sales-trend` | Revenue/units over time |
|
|
| GET | `/api/analytics/categories` | Sales by category |
|
|
| GET | `/api/analytics/products` | Product performance |
|
|
| GET | `/api/analytics/customers` | Customer analytics |
|
|
| GET | `/api/analytics/cost-evolution` | Cost price evolution |
|
|
| GET | `/api/analytics/inventory-health` | Inventory health metrics |
|
|
| GET | `/api/analytics/seasonality` | Seasonal sales patterns |
|
|
|
|
### Reports (PDF Generation)
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET | `/api/reports/sales` | Sales report data |
|
|
| GET | `/api/reports/sales/[id]` | Single sale report |
|
|
| GET | `/api/reports/sales/profits` | Profit report |
|
|
| GET | `/api/reports/purchases` | Purchases report data |
|
|
| GET | `/api/reports/purchases/[id]` | Single purchase report |
|
|
| GET | `/api/reports/inventory` | Inventory report |
|
|
| GET | `/api/reports/customers` | Customers report |
|
|
| GET | `/api/reports/customers/[id]` | Single customer report |
|
|
| GET | `/api/reports/brand` | Brand/catalog report |
|
|
| GET | `/api/reports/analytics/best-sellers` | Best sellers report |
|
|
| GET | `/api/reports/analytics/low-rotation` | Low rotation report |
|
|
|
|
### Settings
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET/PUT | `/api/settings/brand` | Get / Update brand settings |
|
|
| GET/PUT | `/api/settings/pricing` | Get / Update pricing settings |
|
|
|
|
### Admin
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET | `/api/admin/backup` | Download full backup (tar.gz) |
|
|
|
|
### Other
|
|
| Method | Route | Description |
|
|
|--------|-------|-------------|
|
|
| GET | `/api/health` | Health check (public, no auth) |
|
|
| POST | `/api/upload` | Upload image file |
|
|
|
|
## Backup & Restore
|
|
|
|
### Automated Backup (Admin Endpoint)
|
|
|
|
Admin users can download a full backup from the app:
|
|
|
|
```
|
|
GET /api/admin/backup
|
|
```
|
|
|
|
This returns a `tar.gz` archive containing:
|
|
- `data/` — SQLite database files (main DB, WAL, SHM)
|
|
- `public/uploads/` — All uploaded images
|
|
|
|
**Requirements**: Must be authenticated as admin. Download over HTTPS only.
|
|
|
|
### Manual SQLite Backup
|
|
|
|
SQLite with WAL mode requires a safe backup procedure to avoid corruption:
|
|
|
|
```bash
|
|
# Option 1: Using SQLite .backup command (safest)
|
|
docker compose exec app sh -c 'sqlite3 /app/data/db.sqlite ".backup /app/data/backup.db"'
|
|
# Then copy backup.db from the container
|
|
|
|
# Option 2: Copy WAL-safe (stop writes first)
|
|
# 1. Temporarily stop the application
|
|
docker compose stop app
|
|
|
|
# 2. Copy the database files
|
|
docker compose cp app:/app/data/db.sqlite ./backup/db.sqlite
|
|
docker compose cp app:/app/data/db.sqlite-wal ./backup/db.sqlite-wal
|
|
docker compose cp app:/app/data/db.sqlite-shm ./backup/db.sqlite-shm
|
|
|
|
# 3. Restart the application
|
|
docker compose start app
|
|
|
|
# Option 3: Using docker volume backup
|
|
docker run --rm -v byrachel_db-data:/data -v $(pwd)/backup:/backup alpine \
|
|
tar czf /backup/byrachel-db-$(date +%Y%m%d).tar.gz -C /data .
|
|
```
|
|
|
|
### Image Volume Backup
|
|
|
|
```bash
|
|
docker run --rm -v byrachel_uploads:/uploads -v $(pwd)/backup:/backup alpine \
|
|
tar czf /backup/byrachel-uploads-$(date +%Y%m%d).tar.gz -C /uploads .
|
|
```
|
|
|
|
### Restore Procedure
|
|
|
|
```bash
|
|
# 1. Stop the application
|
|
docker compose stop app
|
|
|
|
# 2. Restore database
|
|
docker run --rm -v byrachel_db-data:/data -v $(pwd)/backup:/backup alpine \
|
|
sh -c 'rm -rf /data/* && tar xzf /backup/byrachel-db-YYYYMMDD.tar.gz -C /data'
|
|
|
|
# 3. Restore uploads
|
|
docker run --rm -v byrachel_uploads:/uploads -v $(pwd)/backup:/backup alpine \
|
|
sh -c 'rm -rf /uploads/* && tar xzf /backup/byrachel-uploads-YYYYMMDD.tar.gz -C /uploads'
|
|
|
|
# 4. Start the application
|
|
docker compose start app
|
|
|
|
# 5. Verify
|
|
docker compose ps # Check health status
|
|
curl http://localhost:3000/api/health # Should return {"status":"ok"}
|
|
```
|
|
|
|
## Production Deployment
|
|
|
|
### Docker Deployment
|
|
|
|
```bash
|
|
# 1. Clone on your server
|
|
git clone <your-repo-url> byrachel
|
|
cd byrachel
|
|
|
|
# 2. Configure environment
|
|
cp .env.example .env
|
|
nano .env
|
|
# Set JWT_SECRET to a random value: openssl rand -base64 32
|
|
|
|
# 3. Build and start
|
|
docker compose up -d --build
|
|
|
|
# 4. Verify
|
|
docker compose ps
|
|
docker compose logs -f app
|
|
```
|
|
|
|
### Nginx Proxy Manager Setup
|
|
|
|
[Nginx Proxy Manager](https://nginxproxymanager.com/) is the recommended reverse proxy for HTTPS and domain routing.
|
|
|
|
#### 1. Install Nginx Proxy Manager
|
|
|
|
```bash
|
|
# Add to your docker-compose.yml or run separately
|
|
docker run -d \
|
|
--name nginx-proxy-manager \
|
|
-p 80:80 -p 81:81 -p 443:443 \
|
|
-v ./npm-data:/data \
|
|
-v ./npm-le:/etc/letsencrypt \
|
|
jc21/nginx-proxy-manager:latest
|
|
```
|
|
|
|
#### 2. Configure Proxy Host
|
|
|
|
1. Open NPM admin: `http://your-server-ip:81`
|
|
2. Default login: `admin@example.com` / `changeme`
|
|
3. Go to **Hosts → Proxy Hosts → Add Proxy Host**
|
|
4. Configure:
|
|
- **Domain Names**: `yourdomain.com`
|
|
- **Scheme**: `http`
|
|
- **Hostname**: `byrachel-app` (Docker container name) or your server's Docker IP
|
|
- **Port**: `3000`
|
|
- **Common Settings**:
|
|
- Enable: `Block Common Exploits`
|
|
- Enable: `Websockets Support`
|
|
- **SSL Tab**:
|
|
- SSL Certificate: Request a new Let's Encrypt certificate
|
|
- Enable: `Force SSL`
|
|
- Enable: `HTTP/2 Support`
|
|
|
|
#### 3. Docker Network (recommended)
|
|
|
|
If both NPM and ByRachel are in the same Docker network:
|
|
|
|
```yaml
|
|
# docker-compose.yml
|
|
services:
|
|
app:
|
|
# ... existing config ...
|
|
networks:
|
|
- proxy-net
|
|
|
|
networks:
|
|
proxy-net:
|
|
external: true
|
|
```
|
|
|
|
Then in NPM, use the container name `byrachel-app` as the hostname.
|
|
|
|
### HTTPS Configuration
|
|
|
|
HTTPS is configured through Nginx Proxy Manager:
|
|
|
|
1. **Request Certificate**: In the Proxy Host SSL tab, select "Request a new SSL Certificate"
|
|
2. **Let's Encrypt**: NPM handles Let's Encrypt automatically
|
|
3. **Force SSL**: Enable to redirect HTTP → HTTPS
|
|
4. **Auto-renewal**: Certificates renew automatically
|
|
|
|
### Domain Configuration
|
|
|
|
1. **DNS Record**: Create an A record pointing your domain to your server's IP
|
|
```
|
|
Type: A
|
|
Name: yourdomain.com (or @)
|
|
Value: YOUR_SERVER_IP
|
|
TTL: 300
|
|
```
|
|
|
|
2. **Wait for DNS propagation** (usually minutes, up to 48h)
|
|
|
|
3. **Configure in NPM**: Add the domain in the Proxy Host settings (see above)
|
|
|
|
### Auto-restart & Log Management
|
|
|
|
#### Auto-restart
|
|
|
|
The Docker Compose configuration includes `restart: unless-stopped`, which means:
|
|
- Container restarts automatically on crash
|
|
- Container starts on Docker daemon startup (server reboot)
|
|
- Container only stays stopped if you explicitly `docker compose stop`
|
|
|
|
For additional server-level auto-start, create a systemd service:
|
|
|
|
```ini
|
|
# /etc/systemd/system/byrachel.service
|
|
[Unit]
|
|
Description=ByRachel Docker Compose
|
|
Requires=docker.service
|
|
After=docker.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
WorkingDirectory=/path/to/byrachel
|
|
ExecStart=/usr/bin/docker compose up -d
|
|
ExecStop=/usr/bin/docker compose down
|
|
TimeoutStartSec=0
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
```bash
|
|
sudo systemctl enable byrachel.service
|
|
sudo systemctl start byrachel.service
|
|
```
|
|
|
|
#### Log Management
|
|
|
|
```bash
|
|
# View live logs
|
|
docker compose logs -f app
|
|
|
|
# View last 100 lines
|
|
docker compose logs --tail=100 app
|
|
|
|
# Configure log rotation (in docker-compose.yml, add to app service):
|
|
# logging:
|
|
# driver: json-file
|
|
# options:
|
|
# max-size: "10m"
|
|
# max-file: "3"
|
|
```
|
|
|
|
## SQLite → PostgreSQL Migration Plan
|
|
|
|
ByRachel uses Drizzle ORM, which supports multiple database dialects. Migration to PostgreSQL is possible when needed:
|
|
|
|
### Steps
|
|
|
|
1. **Install PostgreSQL driver**:
|
|
```bash
|
|
npm install pg
|
|
npm install @types/pg
|
|
```
|
|
|
|
2. **Update Drizzle config** (`drizzle.config.ts`):
|
|
```typescript
|
|
export default defineConfig({
|
|
schema: './src/lib/db/schema.ts',
|
|
out: './src/lib/db/migrations',
|
|
dialect: 'postgresql', // Change from 'sqlite'
|
|
dbCredentials: {
|
|
host: process.env.PGHOST,
|
|
port: Number(process.env.PGPORT),
|
|
user: process.env.PGUSER,
|
|
password: process.env.PGPASSWORD,
|
|
database: process.env.PGDATABASE,
|
|
},
|
|
});
|
|
```
|
|
|
|
3. **Update database connection** (`src/lib/db/index.ts`):
|
|
- Replace `drizzle(betterSqlite3(...))` with `drizzle(pgPool)` or `drizzle(nodePostgres(...))`
|
|
|
|
4. **Update environment variables**:
|
|
```env
|
|
DATABASE_URL=postgresql://user:password@localhost:5432/byrachel
|
|
```
|
|
|
|
5. **Generate and run migrations**:
|
|
```bash
|
|
npm run db:generate
|
|
npm run db:migrate
|
|
```
|
|
|
|
6. **Data migration**: Export data from SQLite and import into PostgreSQL using a migration script.
|
|
|
|
### Considerations
|
|
|
|
- SQLite uses `integer` primary keys with auto-increment — PostgreSQL uses `serial` or `identity`
|
|
- SQLite `boolean` (integer 0/1) maps directly to PostgreSQL `boolean`
|
|
- SQLite `text` timestamps work in both, but PostgreSQL has native `timestamp` types
|
|
- Test thoroughly in a staging environment before migrating production data
|
|
|
|
## First-Run Setup
|
|
|
|
### 1. Initial Admin Account
|
|
|
|
1. Open the app in your browser (e.g., `http://localhost:3000`)
|
|
2. You'll be redirected to `/setup` (only available when no users exist)
|
|
3. Fill in:
|
|
- **Email**: Admin email address
|
|
- **Password**: Strong password (min 8 characters)
|
|
- **Name**: Admin display name (optional)
|
|
4. Click "Create Account"
|
|
|
|
### 2. TOTP Configuration
|
|
|
|
After creating the account, TOTP (Time-based One-Time Password) setup begins automatically:
|
|
|
|
1. A **QR code** is displayed on screen
|
|
2. Open your authenticator app (Google Authenticator, Authy, 1Password, etc.)
|
|
3. Scan the QR code (or enter the secret key manually)
|
|
4. Enter the 6-digit code from your authenticator app to verify
|
|
5. **Save your recovery codes** in a secure location — these are your backup if you lose access to your authenticator
|
|
|
|
### 3. Brand Configuration
|
|
|
|
After logging in for the first time:
|
|
|
|
1. Navigate to **Settings** (gear icon in sidebar)
|
|
2. Configure your brand:
|
|
- **Store Name**: Your business name
|
|
- **Logo**: Upload your logo image
|
|
- **Tagline**: Short description
|
|
- **Colors**: Primary and secondary brand colors
|
|
- **Contact Info**: Phone, email, WhatsApp
|
|
- **Address**: Business address
|
|
- **Footer Text**: Custom footer for catalog pages
|
|
3. Click "Save Settings"
|
|
4. To publish your public catalog, check "Publish Catalog" in settings
|
|
|
|
## Known Limitations
|
|
|
|
- **SQLite write concurrency**: SQLite handles concurrent reads well but writes are serialized. For a single-admin small business, this is not an issue. If you need multi-writer support, migrate to PostgreSQL (see migration plan above).
|
|
- **No built-in email**: The app does not send emails. Password recovery uses local recovery codes instead of email-based reset.
|
|
- **Single admin**: The app supports admin and operator roles, but the initial setup creates only one admin user. Additional users must be created manually via the database.
|
|
- **No automated backups**: The backup endpoint provides on-demand backups. Set up cron jobs or external tools for scheduled backups.
|
|
- **Local file storage only**: Images are stored on the local filesystem. For multi-instance deployments, a shared storage solution (S3, etc.) would be needed.
|
|
- **No real-time updates**: The app uses standard HTTP requests. Changes made in one browser tab are not reflected in other tabs until refresh.
|
|
- **Catalog is read-only for customers**: The public catalog at `/catalogo` is for browsing only. Customers cannot place orders through the app.
|
|
|
|
## License
|
|
|
|
Private — All rights reserved.
|