# SmartMaker - LLM Context File
## Overview
SmartMaker is an open-source framework for building Progressive Web Apps (PWA) for Dolibarr ERP/CRM.
It provides a modern React-based frontend stack to complement Dolibarr's PHP backend, enabling rapid development of mobile-first business applications.
**What SmartMaker solves:**
- Dolibarr lacks native mobile apps → SmartMaker provides PWA capabilities
- Complex Redux/API boilerplate → SmartMaker provides ready-to-use hooks
- Authentication complexity → SmartAuth handles JWT with auto-refresh
- Offline support → IndexedDB persistence with Dexie
- Form validation → Zod schemas with useForm hook
Website: https://smartmaker.org
License: AGPL-3.0-or-later
Maintainer: CAP-REL (Dolibarr Preferred Partner)
Repository: https://inligit.fr/cap-rel/dolibarr/smartmaker/
DoliStore: https://www.dolistore.com (search "SmartAuth")
## Why SmartMaker?
### For Dolibarr Users
- Build mobile apps for your Dolibarr instance
- Offline-first: works without internet connection
- PWA: installable on phones without app stores
- Real-time sync with Dolibarr data
### For Developers
- Modern stack: React 19, Vite 6, TailwindCSS 4
- No Redux boilerplate: useGlobalStates handles everything
- No fetch/axios complexity: useApi with auto JWT refresh
- Form validation: Zod + useForm
- Animations: Framer Motion page transitions
- Type-safe: full TypeScript support
### Compared to Alternatives
| Feature | SmartMaker | Native Dolibarr | Custom React |
|---------|------------|-----------------|--------------|
| Mobile PWA | Yes | No | Manual |
| Offline mode | Yes | No | Manual |
| JWT Auth | SmartAuth | API Key only | Manual |
| State management | useGlobalStates | - | Redux setup |
| Form validation | useForm + Zod | - | Manual |
| Component library | SmartCommon | - | Choose one |
## Architecture
### Technology Stack
| Layer | Technology | Purpose |
|-------|------------|---------|
| Frontend | React 19 | UI components |
| Build | Vite 6 | Fast development & bundling |
| Styling | TailwindCSS 4 | Utility-first CSS with @theme |
| State | Redux Toolkit | Global state (abstracted by useGlobalStates) |
| Storage | Dexie (IndexedDB) | Offline data persistence |
| HTTP | ky | HTTP client with interceptors |
| Validation | Zod | Schema validation |
| Animation | Framer Motion | Page transitions |
| i18n | i18next | Translations |
| Backend | PHP | Dolibarr integration |
| Auth | SmartAuth (JWT) | Token-based authentication |
### Project Structure
```
dolibarr/
└── htdocs/
└── custom/
└── mymodule/ # Your Dolibarr module
├── mobile/ # React source code
│ ├── src/
│ │ ├── components/
│ │ │ ├── app/ # Provider, Router
│ │ │ ├── pages/ # public/, private/, errors/
│ │ │ ├── forms/ # Form components
│ │ │ ├── ui/ # Reusable UI
│ │ │ └── layouts/ # Page layouts
│ │ ├── appConfig.js # SmartCommon configuration
│ │ ├── App.jsx
│ │ └── main.jsx
│ ├── public/
│ │ └── locales/ # Translation files
│ └── package.json
├── pwa/ # Built PWA (production)
│ ├── api.php # API router
│ └── .htaccess
├── smartmaker-api/ # PHP Controllers
│ ├── Controllers/
│ └── dm*.php # Dolibarr mapping classes
└── smartmaker-api-prepend.php
```
## Main Packages
### @cap-rel/smartcommon
Shared React component library with hooks, UI components, and utilities.
- NPM Registry: https://inligit.fr/api/v4/projects/197/packages/npm/
- Install: `npm install @cap-rel/smartcommon`
- Import: `import { Provider, useApi, useGlobalStates } from '@cap-rel/smartcommon'`
### SmartAuth
JWT authentication module for Dolibarr with multi-device support.
- DoliStore: https://www.dolistore.com/product.php?id=2509&l=fr
- Features: JWT tokens, refresh tokens, device management, connection logs
- Repository: https://inligit.fr/cap-rel/dolibarr/plugin-smartauth/
### SmartBoot
Bootstrap script to initialize SmartMaker in any Dolibarr module.
- Repository: https://inligit.fr/cap-rel/dolibarr/smartmaker/smartboot/
- Usage: `git clone https://inligit.fr/cap-rel/dolibarr/smartmaker/smartboot.git && ./smartboot/setup.sh`
## SmartCommon Components
### Provider (App Setup)
```jsx
import { Provider } from '@cap-rel/smartcommon';
import { config } from './appConfig';
export const App = () => (
);
```
The Provider wraps:
- LibConfigProvider: App configuration context
- ReduxProvider: Redux store with persistence
- I18nextProvider: Translations (i18next)
- ApiProvider: API context with JWT handling
- GlobalStatesProvider: Global state management
- NavigationProvider: Router context
- Toaster: Toast notifications (react-hot-toast)
### Form Components
| Component | Description | Example |
|-----------|-------------|---------|
| Form | Form wrapper with validation | `
);
};
```
### useDb - IndexedDB with Dexie
```jsx
import { useDb } from '@cap-rel/smartcommon';
const MyComponent = () => {
const db = useDb({
name: 'myApp',
version: 1,
stores: {
items: 'id++, name, category, createdAt',
settings: 'key',
},
});
// Create
const id = await db.items.add({ name: 'Item 1', category: 'A' });
// Read
const item = await db.items.get(id);
const all = await db.items.toArray();
const filtered = await db.items.where('category').equals('A').toArray();
// Update
await db.items.update(id, { name: 'Updated' });
// Delete
await db.items.delete(id);
await db.items.clear(); // Delete all
};
```
### useIntl - Internationalization
```jsx
import { useIntl } from '@cap-rel/smartcommon';
const MyComponent = () => {
const { t, lng, setLng, formatDate, formatNumber, formatCurrency } = useIntl();
return (
{/* Translations */}
{t('home.title')}
{t('home.welcome', { name: 'John' })}
{t('items.count', { count: 5 })}
{/* Date formatting */}
{formatDate(new Date())}
{formatDate(date, { dateStyle: 'full' })}
{formatDate(date, { dateStyle: 'long', timeStyle: 'short' })}
{/* Number formatting */}
{formatNumber(1234567.89)}
{/* fr: 1 234 567,89 | en: 1,234,567.89 */}
{/* Currency formatting */}
{formatCurrency(99.99, 'EUR')}
{/* fr: 99,99 € | en: €99.99 */}
{/* Language switcher */}
);
};
```
Translation files (public/locales/fr.json):
```json
{
"home": {
"title": "Accueil",
"welcome": "Bienvenue {{name}} !"
},
"items": {
"count": "{{count}} élément",
"count_plural": "{{count}} éléments"
}
}
```
### useNavigation - Router Navigation
```jsx
import { useNavigation } from '@cap-rel/smartcommon';
const MyComponent = () => {
const navigate = useNavigation();
// Navigate to path
navigate('/home');
navigate('/items/123');
// Navigate back
navigate(-1);
// Replace history (no back)
navigate('/login', { replace: true });
// With state
navigate('/details', { state: { from: 'home' } });
};
```
### useAnimation - Page Transitions
```jsx
import { useAnimation } from '@cap-rel/smartcommon';
const MyPage = () => {
const { variants, getAnimation } = useAnimation();
// Get animation for current route
const animation = getAnimation('/home', '/settings'); // 'slideLeft'
return (
{/* Page content */}
);
};
```
### useFile - File Utilities
```jsx
import { useFile } from '@cap-rel/smartcommon';
const MyComponent = () => {
const { resizeImage, toBase64 } = useFile();
const handleImage = async (file) => {
// Resize image
const resized = await resizeImage(file, { maxWidth: 800, maxHeight: 600 });
// Convert to base64
const base64 = await toBase64(resized);
};
};
```
### useWindow - Window & Device Detection
```jsx
import { useWindow } from '@cap-rel/smartcommon';
const MyComponent = () => {
const { width, height, isMobile, isTablet, isDesktop } = useWindow();
return (
{isMobile && }
{isDesktop && }
);
};
```
### useVariantMerger - Component Variants
```jsx
import { useVariantMerger } from '@cap-rel/smartcommon';
const Button = ({ variant = 'primary', className, ...props }) => {
const { merge } = useVariantMerger();
const baseClasses = 'px-4 py-2 rounded';
const variants = {
primary: 'bg-primary text-white',
secondary: 'bg-secondary text-white',
outline: 'border-2 border-primary text-primary',
};
return (
);
};
```
### useListDnD - Drag and Drop
```jsx
import { useListDnD } from '@cap-rel/smartcommon';
const SortableList = ({ items, onReorder }) => {
const { dragHandleProps, getDragItemProps } = useListDnD({
items,
onReorder,
});
return (
{items.map((item, index) => (
-
⋮⋮
{item.name}
))}
);
};
```
## Provider Configuration
Full configuration options for appConfig.js:
```js
export const config = {
// Debug mode (colored console logs)
debug: import.meta.env.DEV,
// API configuration
api: {
prefixUrl: import.meta.env.VITE_API_URL,
timeout: 30000,
debug: true,
paths: {
login: 'login',
logout: 'logout',
refresh: 'refresh',
},
errors: {
network: 'Network error',
timeout: 'Request timeout',
unauthorized: 'Session expired',
},
},
// Component variants
components: {
variants: {
Button: {
default: 'bg-primary text-white rounded-md px-4 py-2',
secondary: 'bg-secondary text-white rounded-md px-4 py-2',
outline: 'border-2 border-primary text-primary rounded-md px-4 py-2',
ghost: 'text-primary hover:bg-primary/10 rounded-md px-4 py-2',
},
Input: {
default: 'bg-surface border border-gray-200 rounded-md p-3',
filled: 'bg-muted border-0 rounded-md p-3',
},
},
},
// Storage persistence
storage: {
db: {
name: 'myApp',
version: 1,
stores: { items: 'id++, name' },
},
local: ['session', 'settings'], // localStorage keys
session: ['tempData'], // sessionStorage keys
},
// Global state initial values
globalState: {
reducers: {
session: null,
settings: { theme: 'light', lng: 'fr' },
items: [],
loading: false,
},
},
// Page transition animations
pages: {
'/': {
'/settings': 'slideLeft',
'/items/*': 'slideLeft',
'*': 'fade',
},
'/settings': {
'/': 'slideRight',
},
'*': 'fade',
},
};
```
## Page Animations
Available animations:
| Animation | Description | Use case |
|-----------|-------------|----------|
| fade | Opacity fade | Default, modal open |
| slideLeft | Slide from right to left | Navigate deeper |
| slideRight | Slide from left to right | Navigate back |
| zoom | Scale in/out | Modal, focus |
Configuration in appConfig.js:
```js
pages: {
'/home': {
'/details': 'slideLeft', // home → details: slide left
'/settings': 'slideLeft', // home → settings: slide left
'*': 'fade', // home → other: fade
},
'/details': {
'/home': 'slideRight', // details → home: slide right
},
'*': 'fade', // default: fade
}
```
## Debug Logging
Import `log` for colored console output:
```jsx
import { log } from '@cap-rel/smartcommon';
// State changes
log.state('count', 5);
log.globalState('session', { user: 'John' });
// Effects
log.effect('useEffect triggered');
// API calls
log.apiLoading('GET', '/items');
log.apiSuccess('GET', '/items', data);
log.apiError('GET', '/items', error);
// General
log.success('Operation completed');
log.error('Something went wrong');
log.warning('Deprecated method');
log.info('FYI');
// Navigation
log.page('/home');
log.location({ pathname: '/home', search: '' });
// Database
log.db('items', 'add', { name: 'Item 1' });
```
## Backend PHP
### Router (pwa/api.php)
```php
entity;
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
$item = new \MyObject($db);
$item->fetch($obj->rowid);
$mapping = new dmMyObject();
$items[] = $mapping->exportMappedData($item);
}
return [$items, 200];
}
public function show($payload = null)
{
global $db;
$id = $payload['id'] ?? null;
if (!$id) return ['ID required', 400];
$item = new \MyObject($db);
if ($item->fetch($id) <= 0) {
return ['Not found', 404];
}
$item->fetch_optionals();
$item->fetch_lines();
$mapping = new dmMyObject();
return [$mapping->exportMappedData($item), 200];
}
public function create($payload = null)
{
global $db, $user;
$item = new \MyObject($db);
$item->label = $payload['label'] ?? '';
$item->description = $payload['description'] ?? '';
$res = $item->create($user);
if ($res < 0) return [$item->error, 500];
return [['id' => $res], 201];
}
public function update($payload = null)
{
global $db, $user;
$item = new \MyObject($db);
if ($item->fetch($payload['id']) <= 0) {
return ['Not found', 404];
}
if (isset($payload['label'])) $item->label = $payload['label'];
if (isset($payload['description'])) $item->description = $payload['description'];
$res = $item->update($user);
if ($res < 0) return [$item->error, 500];
return ['Updated', 200];
}
public function delete($payload = null)
{
global $db, $user;
$item = new \MyObject($db);
if ($item->fetch($payload['id']) <= 0) {
return ['Not found', 404];
}
$res = $item->delete($user);
if ($res < 0) return [$item->error, 500];
return ['Deleted', 200];
}
}
```
### Dolibarr Mapping (dm* Classes)
```php
React field
protected $listOfPublishedFields = [
'rowid' => 'id',
'ref' => 'ref',
'label' => 'label',
'description' => 'description',
'fk_soc' => 'thirdparty', // Auto-resolved
'fk_statut' => 'status',
'date_creation' => 'createdAt',
'options_myfield' => 'myField', // Extrafield
];
// For objects with lines (invoices, orders, etc.)
protected $listOfPublishedFieldsForLines = [
'rowid' => 'id',
'fk_product' => 'product',
'qty' => 'quantity',
'subprice' => 'unitPrice',
'total_ht' => 'totalHT',
];
public function __construct()
{
global $langs;
$langs->load("mymodule@mymodule");
$this->boot();
}
// Transform field value: fieldFilterValue + FieldName
public function fieldFilterValueCreatedAt($object)
{
return dol_print_date($object->date_creation, 'dayhour');
}
public function fieldFilterValueStatus($object)
{
$statuses = [0 => 'draft', 1 => 'active', 2 => 'closed'];
return $statuses[$object->fk_statut] ?? 'unknown';
}
// Transform linked object (fk_soc → thirdparty object)
public function fieldFilterValueThirdparty($object)
{
if (empty($object->fk_soc)) return null;
$thirdparty = new \Societe($this->db);
$thirdparty->fetch($object->fk_soc);
return [
'id' => $thirdparty->id,
'name' => $thirdparty->name,
'email' => $thirdparty->email,
];
}
}
```
## SmartAuth - JWT Authentication
### How it works
1. User sends login/password to POST /login
2. Server validates credentials against Dolibarr
3. Server returns accessToken (15min) + refreshToken (7 days)
4. Client stores tokens and sends accessToken in Authorization header
5. When accessToken expires, client calls GET /refresh with refreshToken
6. Server issues new tokens
### Token structure
| Token | Duration | Purpose |
|-------|----------|---------|
| accessToken | 15 minutes | API authentication |
| refreshToken | 7 days | Get new accessToken |
### Device management
SmartAuth supports multiple devices per user:
- Each login creates a new device entry
- Devices can be revoked from Dolibarr interface
- Push notification tokens stored per device
### Security features
- HTTPS required (tokens in clear)
- Refresh tokens are single-use
- Connection logs for audit
- Per-device revocation
## TailwindCSS 4 Theming
### Configuration with @theme
```css
/* src/assets/styles/theme.css */
@theme {
--color-primary: #5fbabf;
--color-primary-light: #8cd4d8;
--color-primary-dark: #4a9599;
--color-secondary: #fc8c8c;
--color-background: #ffffff;
--color-surface: #f8fafc;
--color-foreground: #0f172a;
--color-foreground-muted: #64748b;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--radius-md: 0.5rem;
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
```
### Usage in components
```jsx
Primary button
Muted text
```
### Dark mode
```css
@media (prefers-color-scheme: dark) {
@theme {
--color-background: #0f172a;
--color-foreground: #f8fafc;
}
}
```
## Quick Start Guide
### 1. Create Dolibarr Module
Use Dolibarr Module Builder to create a new module with at least one object.
### 2. Install SmartBoot
```bash
cd htdocs/custom/mymodule
git clone https://inligit.fr/cap-rel/dolibarr/smartmaker/smartboot.git
./smartboot/setup.sh # or setup.ps1 on Windows
rm -rf smartboot
```
### 3. Configure Environment
```bash
cd mobile
cp .env.example .env
```
Edit .env:
```
VITE_API_URL=https://your-dolibarr.com/custom/mymodule/pwa/api.php
VITE_APP_VERSION=1.0.0
VITE_LOCALES=fr,en
```
### 4. Install Dependencies
```bash
npm install
```
### 5. Start Development
```bash
npm run dev
```
### 6. Build for Production
```bash
npm run build
# Copy dist/ to pwa/
```
## Common Patterns
### Protected Routes
```jsx
import { Outlet, Navigate } from 'react-router-dom';
import { useGlobalStates } from '@cap-rel/smartcommon';
export const PrivateRoutes = () => {
const [session] = useGlobalStates('session');
return session ? : ;
};
export const PublicRoutes = () => {
const [session] = useGlobalStates('session');
return session ? : ;
};
```
### Login Page
```jsx
import { useApi, useGlobalStates, useForm, useNavigation } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';
const schema = z.object({
login: z.string().min(1),
password: z.string().min(1),
});
export const LoginPage = () => {
const api = useApi();
const navigate = useNavigation();
const [, setSession] = useGlobalStates('session');
const form = useForm({ schema });
const handleSubmit = async (data) => {
const response = await api.public.post('login', { json: data });
if (response.success) {
setSession(response.data);
navigate('/');
}
};
return (
);
};
```
### List Page with API
```jsx
import { useApi, useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
import { useEffect, useState } from 'react';
export const ItemsPage = () => {
const api = useApi();
const navigate = useNavigation();
const [items, setItems] = useGlobalStates('items');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetch = async () => {
const response = await api.private.get('items');
if (response.success) setItems(response.data);
setLoading(false);
};
fetch();
}, []);
if (loading) return ;
return (
{items.map(item => (
navigate(`/items/${item.id}`)}>
{item.label}
))}
);
};
```
### Form with CRUD
```jsx
import { useApi, useNavigation, useForm } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';
const schema = z.object({
label: z.string().min(3),
description: z.string().optional(),
});
export const ItemForm = ({ item, onSuccess }) => {
const api = useApi();
const form = useForm({ schema, defaultValues: item });
const handleSubmit = async (data) => {
const response = item?.id
? await api.private.put(`items/${item.id}`, { json: data })
: await api.private.post('items', { json: data });
if (response.success) onSuccess(response.data);
};
return (
);
};
```
## Related Links
### Documentation
- Main site: https://smartmaker.org
- Getting started: https://smartmaker.org/howto/start
- SmartAuth: https://smartmaker.org/smartauth/start
- SmartCommon: https://smartmaker.org/front/smartcommon
- Hooks: https://smartmaker.org/front/hooks
- Configuration: https://smartmaker.org/front/configuration
- Animations: https://smartmaker.org/front/animations
- Debug: https://smartmaker.org/front/debug
- PWA: https://smartmaker.org/front/pwa
- Themes: https://smartmaker.org/front/themes
- Translations: https://smartmaker.org/front/traductions
- Routing: https://smartmaker.org/front/routage
- Backend: https://smartmaker.org/back/start
- Mapping: https://smartmaker.org/back/mapping_dolibarr_-_react
### External
- Dolibarr: https://www.dolibarr.org
- Dolibarr GitHub: https://github.com/Dolibarr/dolibarr
- DoliStore: https://www.dolistore.com
- React: https://react.dev
- Vite: https://vitejs.dev
- TailwindCSS: https://tailwindcss.com
- Zod: https://zod.dev
- Framer Motion: https://www.framer.com/motion/
- Dexie: https://dexie.org
- ky: https://github.com/sindresorhus/ky
- i18next: https://www.i18next.com
## Keywords
Dolibarr, Dolibarr mobile, Dolibarr PWA, Dolibarr app, Dolibarr React, Progressive Web App, PWA framework, React 19, Vite 6, TailwindCSS 4, ERP mobile, CRM mobile, open source ERP, open source CRM, PHP React, JWT authentication, IndexedDB, offline-first, mobile-first, Dexie, Framer Motion, ky HTTP, Zod validation, i18next, Redux Toolkit, SmartMaker, SmartAuth, SmartCommon, SmartBoot, CAP-REL, Dolibarr Preferred Partner, business app, enterprise app, field service app, intervention management