el-contador 1.1.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker-compose.yml +1 -1
- package/frontend/README.md +73 -0
- package/package.json +2 -2
- package/server/Dockerfile +11 -2
- package/server/README.md +9 -0
- package/server/db/init.js +2 -0
- package/server/db/schema.sql +39 -2
- package/server/index.js +19 -12
- package/server/package-lock.json +685 -3
- package/server/package.json +6 -2
- package/server/routes/bank.js +59 -0
- package/server/routes/customers.js +14 -8
- package/server/routes/expenses.js +354 -7
- package/server/routes/integrations.js +44 -0
- package/server/routes/invoice-config.js +10 -0
- package/server/routes/payees.js +141 -0
- package/server/routes/public.js +45 -0
- package/server/routes/reconciliation.js +84 -5
- package/server/routes/sales.js +18 -1
- package/server/routes/suppliers.js +21 -14
- package/server/routes/webhooks.js +154 -0
- package/server/services/invoice-pdf.js +183 -0
- package/admin/app.html +0 -5369
- package/admin/index.html +0 -32
- package/admin/login.html +0 -102
package/docker-compose.yml
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "el-contador",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Bookkeeping and expense management – run with Docker",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bookkeeping",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"server/index.js",
|
|
27
27
|
"server/package.json",
|
|
28
28
|
"server/package-lock.json",
|
|
29
|
-
"
|
|
29
|
+
"frontend/dist",
|
|
30
30
|
"docker-compose.yml",
|
|
31
31
|
".env.example",
|
|
32
32
|
"README.md",
|
package/server/Dockerfile
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
# Build from project root: docker build -f server/Dockerfile .
|
|
1
|
+
# Build from project root: docker compose build (or docker build -f server/Dockerfile .)
|
|
2
|
+
# Stage 1: build frontend
|
|
3
|
+
FROM node:20-alpine AS frontend-builder
|
|
4
|
+
WORKDIR /app/frontend
|
|
5
|
+
COPY frontend/package.json frontend/package-lock.json ./
|
|
6
|
+
RUN npm ci
|
|
7
|
+
COPY frontend/ .
|
|
8
|
+
RUN npm run build
|
|
9
|
+
|
|
10
|
+
# Stage 2: backend + frontend dist
|
|
2
11
|
FROM node:20-alpine
|
|
3
12
|
|
|
4
13
|
WORKDIR /app
|
|
@@ -7,7 +16,7 @@ COPY server/package.json server/package-lock.json* ./
|
|
|
7
16
|
RUN npm ci --omit=dev
|
|
8
17
|
|
|
9
18
|
COPY server/ .
|
|
10
|
-
COPY
|
|
19
|
+
COPY --from=frontend-builder /app/frontend/dist/ /frontend/dist/
|
|
11
20
|
|
|
12
21
|
EXPOSE 3000
|
|
13
22
|
|
package/server/README.md
CHANGED
|
@@ -34,6 +34,15 @@ Then set in `.env`:
|
|
|
34
34
|
|
|
35
35
|
Point your subdomain at this app (reverse proxy to `http://127.0.0.1:3080` or your ADMIN_PORT). See the root README for an nginx example. First login with the email/password from step 2.
|
|
36
36
|
|
|
37
|
+
### 413 Payload Too Large on file upload
|
|
38
|
+
|
|
39
|
+
If users get **413** when uploading receipt images (e.g. on mobile), the request is being rejected by the **web server** (Apache/nginx) before it reaches Node, so the backend will not log the request.
|
|
40
|
+
|
|
41
|
+
- **Apache**: In the vhost or `.htaccess`, set `LimitRequestBody` high enough (e.g. `LimitRequestBody 10485760` for 10MB). Default is often 1MB or less.
|
|
42
|
+
- **nginx**: In server/location, set `client_max_body_size 10M;` (or higher). Default is 1M.
|
|
43
|
+
|
|
44
|
+
The app allows uploads up to 10MB (expenses/sales); ensure the proxy limit is at least that.
|
|
45
|
+
|
|
37
46
|
## API (all under `/api`, require session except auth)
|
|
38
47
|
|
|
39
48
|
- `POST /api/auth/login` – body: `{ email, password }`
|
package/server/db/init.js
CHANGED
|
@@ -67,6 +67,8 @@ async function init() {
|
|
|
67
67
|
await pool.query("ALTER TABLE sales ADD COLUMN IF NOT EXISTS customer_address text");
|
|
68
68
|
await pool.query("ALTER TABLE sales ADD COLUMN IF NOT EXISTS voided boolean NOT NULL DEFAULT false");
|
|
69
69
|
await pool.query("ALTER TABLE sales ADD COLUMN IF NOT EXISTS voided_at timestamptz");
|
|
70
|
+
await pool.query("ALTER TABLE sales ADD COLUMN IF NOT EXISTS external_id text UNIQUE");
|
|
71
|
+
await pool.query("ALTER TABLE sales ADD COLUMN IF NOT EXISTS source text");
|
|
70
72
|
await pool.query("ALTER TABLE bank_transactions ADD COLUMN IF NOT EXISTS account_type text");
|
|
71
73
|
await pool.query("ALTER TABLE bank_transactions ADD COLUMN IF NOT EXISTS account_note text");
|
|
72
74
|
await pool.query(`
|
package/server/db/schema.sql
CHANGED
|
@@ -87,8 +87,17 @@ CREATE TABLE IF NOT EXISTS sales (
|
|
|
87
87
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
--
|
|
91
|
-
|
|
90
|
+
-- Integration settings: single row for dynamic Stripe/Paddle credentials
|
|
91
|
+
CREATE TABLE IF NOT EXISTS integration_settings (
|
|
92
|
+
id int PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
93
|
+
data jsonb NOT NULL DEFAULT '{}'
|
|
94
|
+
);
|
|
95
|
+
INSERT INTO integration_settings (id, data) VALUES (1, '{}'::jsonb)
|
|
96
|
+
ON CONFLICT (id) DO NOTHING;
|
|
97
|
+
|
|
98
|
+
-- Migration: Add external sync tracking columns to sales if they don't exist
|
|
99
|
+
ALTER TABLE sales ADD COLUMN IF NOT EXISTS external_id text UNIQUE;
|
|
100
|
+
ALTER TABLE sales ADD COLUMN IF NOT EXISTS source text;
|
|
92
101
|
|
|
93
102
|
-- Migration: Add file columns to sales if they don't exist
|
|
94
103
|
ALTER TABLE sales ADD COLUMN IF NOT EXISTS file_name text;
|
|
@@ -249,3 +258,31 @@ CREATE TABLE IF NOT EXISTS approval_settings (
|
|
|
249
258
|
);
|
|
250
259
|
INSERT INTO approval_settings (id, enabled, approvers) VALUES (1, false, '[]')
|
|
251
260
|
ON CONFLICT (id) DO NOTHING;
|
|
261
|
+
|
|
262
|
+
-- Contact account numbers (for bank/ledger reference)
|
|
263
|
+
ALTER TABLE customers ADD COLUMN IF NOT EXISTS account_number text;
|
|
264
|
+
ALTER TABLE suppliers ADD COLUMN IF NOT EXISTS account_number text;
|
|
265
|
+
|
|
266
|
+
-- Payees table (people/entities we pay: freelancers, refund recipients, etc.)
|
|
267
|
+
CREATE TABLE IF NOT EXISTS payees (
|
|
268
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
269
|
+
name text NOT NULL,
|
|
270
|
+
email text,
|
|
271
|
+
address text,
|
|
272
|
+
phone text,
|
|
273
|
+
vat_number text,
|
|
274
|
+
company_number text,
|
|
275
|
+
account_number text,
|
|
276
|
+
notes text,
|
|
277
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
278
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
279
|
+
);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_payees_name ON payees(name);
|
|
281
|
+
|
|
282
|
+
-- Backfill bank_transaction_id on expenses that were matched via single-expense match
|
|
283
|
+
-- (reconciliation_ref_type = 'expense') so status shows as Paid on the Expenses page
|
|
284
|
+
UPDATE expenses e
|
|
285
|
+
SET bank_transaction_id = bt.id
|
|
286
|
+
FROM bank_transactions bt
|
|
287
|
+
WHERE bt.reconciliation_ref_type = 'expense' AND bt.reconciliation_ref_id = e.id
|
|
288
|
+
AND e.bank_transaction_id IS NULL AND e.reconciled = true;
|
package/server/index.js
CHANGED
|
@@ -14,10 +14,13 @@ const bankRoutes = require('./routes/bank');
|
|
|
14
14
|
const reconciliationRoutes = require('./routes/reconciliation');
|
|
15
15
|
const invoiceConfigRoutes = require('./routes/invoice-config');
|
|
16
16
|
const approvalSettingsRoutes = require('./routes/approval-settings');
|
|
17
|
+
const integrationsRoutes = require('./routes/integrations');
|
|
17
18
|
const dashboardRoutes = require('./routes/dashboard');
|
|
18
19
|
const customersRoutes = require('./routes/customers');
|
|
19
20
|
const suppliersRoutes = require('./routes/suppliers');
|
|
21
|
+
const payeesRoutes = require('./routes/payees');
|
|
20
22
|
const vatReportsRoutes = require('./routes/vat-reports');
|
|
23
|
+
const webhooksRoutes = require('./routes/webhooks');
|
|
21
24
|
|
|
22
25
|
const PgSession = connectPgSimple(session);
|
|
23
26
|
const app = express();
|
|
@@ -25,6 +28,9 @@ const app = express();
|
|
|
25
28
|
// Required when behind a reverse proxy (Apache/nginx) so cookies and redirects use correct scheme/host
|
|
26
29
|
app.set('trust proxy', 1);
|
|
27
30
|
|
|
31
|
+
// Mount webhooks before generic JSON parsing because Stripe requires raw body
|
|
32
|
+
app.use('/api/webhooks', webhooksRoutes);
|
|
33
|
+
|
|
28
34
|
app.use(cookieParser());
|
|
29
35
|
app.use(express.json());
|
|
30
36
|
|
|
@@ -61,6 +67,7 @@ app.use(
|
|
|
61
67
|
);
|
|
62
68
|
|
|
63
69
|
app.use('/api/auth', authRoutes);
|
|
70
|
+
app.use('/api/public', require('./routes/public'));
|
|
64
71
|
app.use('/api/expenses', requireAuth, expensesRoutes);
|
|
65
72
|
app.use('/api/expense-categories', requireAuth, expenseCategoriesRoutes);
|
|
66
73
|
app.use('/api/sales', requireAuth, salesRoutes);
|
|
@@ -68,31 +75,31 @@ app.use('/api/bank-transactions', requireAuth, bankRoutes);
|
|
|
68
75
|
app.use('/api/reconciliation', requireAuth, reconciliationRoutes);
|
|
69
76
|
app.use('/api/invoice-config', requireAuth, invoiceConfigRoutes);
|
|
70
77
|
app.use('/api/approval-settings', approvalSettingsRoutes);
|
|
78
|
+
app.use('/api/integrations', requireAuth, integrationsRoutes);
|
|
71
79
|
app.use('/api/dashboard', requireAuth, dashboardRoutes);
|
|
72
80
|
app.use('/api/customers', requireAuth, customersRoutes);
|
|
73
81
|
app.use('/api/suppliers', requireAuth, suppliersRoutes);
|
|
82
|
+
app.use('/api/payees', requireAuth, payeesRoutes);
|
|
74
83
|
app.use('/api/reports/vat', requireAuth, vatReportsRoutes);
|
|
75
84
|
|
|
76
|
-
const adminRoot = path.join(__dirname, '..', '
|
|
85
|
+
const adminRoot = path.join(__dirname, '..', 'frontend', 'dist');
|
|
86
|
+
|
|
87
|
+
// Serve static assets first so JS/CSS load without going through auth (avoids MIME type errors)
|
|
88
|
+
app.use(express.static(adminRoot));
|
|
77
89
|
|
|
90
|
+
// Redirect unauthenticated users to login only for page requests (not for /assets/*, etc.)
|
|
78
91
|
function serveLoginIfNeeded(req, res, next) {
|
|
79
|
-
if (req.path !== '/login' &&
|
|
92
|
+
if (req.path !== '/login' && !req.session?.userId) {
|
|
80
93
|
return res.redirect('/login');
|
|
81
94
|
}
|
|
82
95
|
next();
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
app.get('/login', (req, res) => {
|
|
86
|
-
if (req.session && req.session.userId) {
|
|
87
|
-
return res.redirect('/');
|
|
88
|
-
}
|
|
89
|
-
res.sendFile(path.join(adminRoot, 'login.html'));
|
|
90
|
-
});
|
|
91
|
-
|
|
92
98
|
app.use(serveLoginIfNeeded);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
|
|
100
|
+
// All unknown routes (except /api) should fall back to index.html for React Router
|
|
101
|
+
app.get('*', (req, res) => {
|
|
102
|
+
res.sendFile(path.join(adminRoot, 'index.html'));
|
|
96
103
|
});
|
|
97
104
|
|
|
98
105
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|