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.
@@ -1,5 +1,5 @@
1
1
  # Compose file format 3.8 for compatibility with docker-compose (v1) and docker compose (v2)
2
- version: "3.8"
2
+ #version: "3.8"
3
3
 
4
4
  services:
5
5
  postgres:
@@ -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.1.0",
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
- "admin",
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 admin/ /admin/
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(`
@@ -87,8 +87,17 @@ CREATE TABLE IF NOT EXISTS sales (
87
87
  created_at timestamptz NOT NULL DEFAULT now()
88
88
  );
89
89
 
90
- -- Migration: Add customer_id column if table exists without it
91
- ALTER TABLE sales ADD COLUMN IF NOT EXISTS customer_id uuid REFERENCES customers(id) ON DELETE SET NULL;
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, '..', 'admin');
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' && req.path !== '/login.html' && !req.session?.userId) {
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
- app.use(express.static(adminRoot, { index: 'app.html' }));
94
- app.get('/', (req, res) => {
95
- res.sendFile(path.join(adminRoot, 'app.html'));
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);