Initial commit
This commit is contained in:
commit
04a449ccfa
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.* # .env.production, .env.development, etc.
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE/editor config
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Coverage or build output (if using any)
|
||||
coverage/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# PM2 logs (if using PM2 for deployment)
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Optional: Docker-related files
|
||||
*.local
|
||||
docker-compose.override.yml
|
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
||||
# Odoo FTI Backend Integration
|
||||
|
||||
This project is an Express.js backend API service designed to interface with an Odoo ERP system. It provides endpoints for managing and syncing **employee** and **attendance** data through XML-RPC.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- Connects to Odoo using **JSON-RPC** (via `axios`)
|
||||
- Environment-based configuration (supports `.env.development` / `.env.production`)
|
||||
- RESTful API endpoints for:
|
||||
- Fetching employee records
|
||||
- Logging attendance (check-in/check-out)
|
||||
- Modular architecture for scalability and clarityecture: separation of concerns for config, routes, controllers, and models
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
odoo-fti-be/
|
||||
│
|
||||
├── app.js # Main entry point
|
||||
├── package.json # Project metadata and dependencies
|
||||
├── config/
|
||||
│ └── odooConfig.js # XML-RPC connection configuration
|
||||
├── controllers/
|
||||
│ ├── attendanceController.js
|
||||
│ └── employeeController.js
|
||||
├── models/
|
||||
│ ├── odooService.js # Central Odoo client logic
|
||||
│ └── odoo/
|
||||
│ ├── attendance.js # Attendance-related logic
|
||||
│ ├── employee.js # Employee-related logic
|
||||
│ └── index.js
|
||||
├── routes/
|
||||
│ ├── attendanceRoutes.js # Attendance endpoints
|
||||
│ └── employeeRoutes.js # Employee endpoints
|
||||
└── utils/
|
||||
└── date.js # Date utility for formatting
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/odoo-fti-be.git
|
||||
cd odoo-fti-be
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Configure Odoo Connection**
|
||||
Update `config/odooConfig.js` with your Odoo server's URL, database, username, and password.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
url: "http://your-odoo-host",
|
||||
db: "your-db-name",
|
||||
username: "your-username",
|
||||
password: "your-password",
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 API Endpoints
|
||||
|
||||
### 👥 Employee
|
||||
|
||||
- **GET** `/employees`
|
||||
- List all employees from Odoo
|
||||
|
||||
### ⏱️ Attendance
|
||||
|
||||
- **POST** `/attendance/check`
|
||||
- Check in or checkout employee
|
||||
- Body:
|
||||
```json
|
||||
{
|
||||
"employee_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ Technologies Used
|
||||
|
||||
- **Node.js** with **Express.js**
|
||||
- **odoo-xmlrpc** for Odoo integration
|
||||
- Modular code structure for scalability and maintainability
|
||||
|
||||
---
|
||||
|
||||
## 🧾 License
|
32
app.js
Normal file
32
app.js
Normal file
@ -0,0 +1,32 @@
|
||||
// app.js
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config(); // Load from default .env file
|
||||
|
||||
import express from "express";
|
||||
import { authenticate } from "./models/odooService.js";
|
||||
|
||||
import attendanceRoutes from "./routes/attendanceRoutes.js";
|
||||
import employeeRoutes from "./routes/employeeRoutes.js";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.use("/api/employees", employeeRoutes);
|
||||
app.use("/api/attendances", attendanceRoutes);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`✅ Server running on http://localhost:${PORT}`);
|
||||
|
||||
try {
|
||||
const uid = await authenticate();
|
||||
if (uid) {
|
||||
console.log(`🟢 Connected to Odoo! UID: ${uid}`);
|
||||
} else {
|
||||
console.log(`🔴 Odoo connection failed: Invalid credentials.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`❌ Error connecting to Odoo:`, err.message);
|
||||
}
|
||||
});
|
10
config/odooConfig.js
Normal file
10
config/odooConfig.js
Normal file
@ -0,0 +1,10 @@
|
||||
// config/odooConfig.js
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config(); // Load from default .env file
|
||||
|
||||
export const odooConfig = {
|
||||
url: process.env.ODOO_URL,
|
||||
db: process.env.ODOO_DB,
|
||||
email: process.env.ODOO_EMAIL,
|
||||
apiKey: process.env.ODOO_API_KEY,
|
||||
};
|
22
controllers/attendanceController.js
Normal file
22
controllers/attendanceController.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { checkEmployeeExists } from "../models/odoo/employee.js";
|
||||
import { toggleEmployeeAttendance } from "../models/odoo/attendance.js";
|
||||
|
||||
export const toggleAttendance = async (req, res) => {
|
||||
try {
|
||||
const { employee_id } = req.body;
|
||||
|
||||
if (!employee_id) {
|
||||
return res.status(400).json({ error: "employee_id is required" });
|
||||
}
|
||||
|
||||
const exists = await checkEmployeeExists(employee_id);
|
||||
if (!exists) {
|
||||
return res.status(404).json({ error: "Employee not found" });
|
||||
}
|
||||
|
||||
const result = await toggleEmployeeAttendance(employee_id);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
33
controllers/employeeController.js
Normal file
33
controllers/employeeController.js
Normal file
@ -0,0 +1,33 @@
|
||||
// controllers/employeeController.js
|
||||
import { fetchEmployees, fetchEmployeeById } from "../models/odoo/employee.js";
|
||||
|
||||
export const getEmployees = async (req, res) => {
|
||||
try {
|
||||
const employees = await fetchEmployees();
|
||||
|
||||
if (!employees || employees.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ message: "No employee records found" });
|
||||
}
|
||||
|
||||
res.json(employees);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Failed to fetch employees" });
|
||||
}
|
||||
};
|
||||
|
||||
export const getEmployeeById = async (req, res) => {
|
||||
const { employee_id } = req.params;
|
||||
try {
|
||||
const employee = await fetchEmployeeById(parseInt(employee_id));
|
||||
if (!employee) {
|
||||
return res.status(404).json({ error: "Employee not found" });
|
||||
}
|
||||
res.json(employee);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Failed to fetch employee" });
|
||||
}
|
||||
};
|
137
models/odoo/attendance.js
Normal file
137
models/odoo/attendance.js
Normal file
@ -0,0 +1,137 @@
|
||||
// models/odoo/attendance.js
|
||||
import axios from "axios";
|
||||
import { authenticate, JSON_RPC_URL, odooConfig } from "./index.js";
|
||||
import { toOdooDatetimeFormat } from "../../utils/date.js";
|
||||
|
||||
export const toggleEmployeeAttendance = async (employee_id) => {
|
||||
try {
|
||||
const uid = await authenticate();
|
||||
|
||||
if (!uid) throw new Error("Authentication failed.");
|
||||
|
||||
// Step 1: Check if employee exists
|
||||
const empCheck = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search",
|
||||
[[["id", "=", employee_id]]],
|
||||
],
|
||||
},
|
||||
id: 0,
|
||||
};
|
||||
|
||||
const empRes = await axios.post(JSON_RPC_URL, empCheck);
|
||||
if (!empRes.data.result || empRes.data.result.length === 0) {
|
||||
throw new Error(`Employee ${employee_id} not found.`);
|
||||
}
|
||||
|
||||
// Step 2: Look for open attendance
|
||||
const searchPayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.attendance",
|
||||
"search_read",
|
||||
[
|
||||
[
|
||||
["employee_id", "=", employee_id],
|
||||
["check_out", "=", false],
|
||||
],
|
||||
],
|
||||
{ fields: ["id", "check_in"], limit: 1 },
|
||||
],
|
||||
},
|
||||
id: 3,
|
||||
};
|
||||
|
||||
const searchRes = await axios.post(JSON_RPC_URL, searchPayload);
|
||||
const openRecords = searchRes.data.result;
|
||||
|
||||
if (openRecords.length > 0) {
|
||||
// Step 3: Write check_out
|
||||
const attendanceId = openRecords[0].id;
|
||||
const now = toOdooDatetimeFormat(new Date());
|
||||
|
||||
const writePayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.attendance",
|
||||
"write",
|
||||
[[attendanceId], { check_out: now }],
|
||||
],
|
||||
},
|
||||
id: 4,
|
||||
};
|
||||
|
||||
const writeRes = await axios.post(JSON_RPC_URL, writePayload);
|
||||
|
||||
if (!writeRes.data.result) throw new Error("Check-out failed.");
|
||||
|
||||
return {
|
||||
status: "Checked out",
|
||||
attendance_id: attendanceId,
|
||||
check_out: now,
|
||||
odoo_response: writeRes.data,
|
||||
};
|
||||
} else {
|
||||
// Step 4: Create new check_in
|
||||
const now = toOdooDatetimeFormat(new Date());
|
||||
|
||||
const createPayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.attendance",
|
||||
"create",
|
||||
[{ employee_id, check_in: now }],
|
||||
],
|
||||
},
|
||||
id: 5,
|
||||
};
|
||||
|
||||
const createRes = await axios.post(JSON_RPC_URL, createPayload);
|
||||
|
||||
if (createRes.data.error) {
|
||||
console.error("Odoo error:", createRes.data.error);
|
||||
throw new Error(createRes.data.error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
status: "Checked in",
|
||||
attendance_id: createRes.data.result,
|
||||
check_in: now,
|
||||
odoo_response: createRes.data,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("toggleEmployeeAttendance error:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
81
models/odoo/employee.js
Normal file
81
models/odoo/employee.js
Normal file
@ -0,0 +1,81 @@
|
||||
// models/odoo/employee.js
|
||||
import axios from "axios";
|
||||
import { authenticate, JSON_RPC_URL, odooConfig } from "./index.js";
|
||||
|
||||
export const fetchEmployees = async () => {
|
||||
const uid = await authenticate();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search_read",
|
||||
[],
|
||||
{ fields: ["id", "name", "work_email"], limit: 10 },
|
||||
],
|
||||
},
|
||||
id: 2,
|
||||
};
|
||||
|
||||
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||
return data.result;
|
||||
};
|
||||
|
||||
export const checkEmployeeExists = async (employee_id) => {
|
||||
const uid = await authenticate();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search",
|
||||
[[["id", "=", employee_id]]],
|
||||
0,
|
||||
],
|
||||
},
|
||||
id: 10,
|
||||
};
|
||||
|
||||
const res = await axios.post(JSON_RPC_URL, payload);
|
||||
return res.data.result && res.data.result.length > 0;
|
||||
};
|
||||
|
||||
export const fetchEmployeeById = async (employee_id) => {
|
||||
const uid = await authenticate();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search_read",
|
||||
[[["id", "=", employee_id]]],
|
||||
{ fields: ["id", "name", "work_email"], limit: 1 },
|
||||
],
|
||||
},
|
||||
id: 3,
|
||||
};
|
||||
|
||||
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||
return data.result[0] || null;
|
||||
};
|
23
models/odoo/index.js
Normal file
23
models/odoo/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
// models/odoo/index.js
|
||||
import axios from "axios";
|
||||
import { odooConfig } from "../../config/odooConfig.js";
|
||||
|
||||
export const JSON_RPC_URL = `${odooConfig.url}/jsonrpc`;
|
||||
|
||||
export const authenticate = async () => {
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "common",
|
||||
method: "login",
|
||||
args: [odooConfig.db, odooConfig.email, odooConfig.apiKey],
|
||||
},
|
||||
id: 1,
|
||||
};
|
||||
|
||||
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||
return data.result;
|
||||
};
|
||||
|
||||
export { odooConfig }; // re-export for other modules
|
236
models/odooService.js
Normal file
236
models/odooService.js
Normal file
@ -0,0 +1,236 @@
|
||||
// models/odooService.js
|
||||
import axios from "axios";
|
||||
import { odooConfig } from "../config/odooConfig.js";
|
||||
import { toOdooDatetimeFormat } from "../utils/date.js"; // Adjust path if needed
|
||||
|
||||
const JSON_RPC_URL = `${odooConfig.url}/jsonrpc`;
|
||||
|
||||
export const authenticate = async () => {
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "common",
|
||||
method: "login",
|
||||
args: [odooConfig.db, odooConfig.email, odooConfig.apiKey],
|
||||
},
|
||||
id: 1,
|
||||
};
|
||||
|
||||
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||
return data.result;
|
||||
};
|
||||
|
||||
export const fetchEmployees = async () => {
|
||||
const uid = await authenticate();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search_read",
|
||||
[],
|
||||
{ fields: ["id", "name", "work_email"], limit: 10 },
|
||||
],
|
||||
},
|
||||
id: 2,
|
||||
};
|
||||
|
||||
const { data } = await axios.post(JSON_RPC_URL, payload);
|
||||
return data.result;
|
||||
};
|
||||
|
||||
export const checkEmployeeExists = async (employee_id) => {
|
||||
const uid = await authenticate();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search",
|
||||
[[["id", "=", employee_id]]],
|
||||
0,
|
||||
],
|
||||
},
|
||||
id: 10,
|
||||
};
|
||||
|
||||
const res = await axios.post(JSON_RPC_URL, payload);
|
||||
return res.data.result && res.data.result.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles attendance check-in/check-out for an employee
|
||||
* @param {number} employee_id
|
||||
* @returns {object} status and attendance record info
|
||||
*/
|
||||
export const toggleEmployeeAttendance = async (employee_id) => {
|
||||
try {
|
||||
const uid = await authenticate();
|
||||
|
||||
if (!uid) {
|
||||
throw new Error("Authentication failed: no user ID returned.");
|
||||
}
|
||||
|
||||
// Optional: Verify employee exists
|
||||
const checkEmployeePayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.employee",
|
||||
"search",
|
||||
[[["id", "=", employee_id]]],
|
||||
],
|
||||
},
|
||||
id: 0,
|
||||
};
|
||||
const empRes = await axios.post(JSON_RPC_URL, checkEmployeePayload);
|
||||
if (!empRes.data.result || empRes.data.result.length === 0) {
|
||||
throw new Error(`Employee with ID ${employee_id} not found.`);
|
||||
}
|
||||
|
||||
// Step 1: Search for open attendance record (check_out = false)
|
||||
const searchPayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.attendance",
|
||||
"search_read",
|
||||
[
|
||||
[
|
||||
["employee_id", "=", employee_id],
|
||||
["check_out", "=", false],
|
||||
],
|
||||
],
|
||||
{ fields: ["id", "check_in"], limit: 1 },
|
||||
],
|
||||
},
|
||||
id: 3,
|
||||
};
|
||||
|
||||
const searchRes = await axios.post(JSON_RPC_URL, searchPayload);
|
||||
const openRecords = searchRes.data.result;
|
||||
|
||||
if (openRecords.length > 0) {
|
||||
// Step 2: Write check_out datetime
|
||||
const attendanceId = openRecords[0].id;
|
||||
const now = toOdooDatetimeFormat(new Date());
|
||||
|
||||
const writePayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.attendance",
|
||||
"write",
|
||||
[[attendanceId], { check_out: now }],
|
||||
],
|
||||
},
|
||||
id: 4,
|
||||
};
|
||||
|
||||
const writeRes = await axios.post(JSON_RPC_URL, writePayload);
|
||||
|
||||
if (!writeRes.data.result) {
|
||||
throw new Error("Failed to write check_out datetime.");
|
||||
}
|
||||
|
||||
return {
|
||||
status: "Checked out",
|
||||
attendance_id: attendanceId,
|
||||
check_out: now,
|
||||
odoo_response: writeRes.data,
|
||||
};
|
||||
} else {
|
||||
// Step 3: Create new attendance record with check_in
|
||||
const now = toOdooDatetimeFormat(new Date());
|
||||
|
||||
const createPayload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "call",
|
||||
params: {
|
||||
service: "object",
|
||||
method: "execute_kw",
|
||||
args: [
|
||||
odooConfig.db,
|
||||
uid,
|
||||
odooConfig.apiKey,
|
||||
"hr.attendance",
|
||||
"create",
|
||||
[{ employee_id, check_in: now }],
|
||||
],
|
||||
},
|
||||
id: 5,
|
||||
};
|
||||
|
||||
const createRes = await axios.post(JSON_RPC_URL, createPayload);
|
||||
|
||||
// DEBUG LOGGING - print entire response from Odoo
|
||||
console.log(
|
||||
"Create attendance response from Odoo:",
|
||||
JSON.stringify(createRes.data, null, 2)
|
||||
);
|
||||
|
||||
if (createRes.data.error) {
|
||||
// Odoo returned an error object - log it and throw
|
||||
console.error(
|
||||
"Odoo error during attendance creation:",
|
||||
createRes.data.error
|
||||
);
|
||||
throw new Error(
|
||||
createRes.data.error.message ||
|
||||
"Unknown Odoo error during attendance creation"
|
||||
);
|
||||
}
|
||||
|
||||
if (!createRes.data.result) {
|
||||
throw new Error(
|
||||
"Failed to create attendance record - no result returned."
|
||||
);
|
||||
}
|
||||
|
||||
const attendanceId = createRes.data.result;
|
||||
|
||||
return {
|
||||
status: "Checked in",
|
||||
attendance_id: attendanceId,
|
||||
check_in: now,
|
||||
odoo_response: createRes.data,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("toggleEmployeeAttendance error:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
1314
package-lock.json
generated
Normal file
1314
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "odoo-fti-be",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "app.js",
|
||||
"description": "FTI BACKEND API FOR HRIS",
|
||||
"author": "https://obanana.com",
|
||||
"license": "ISC",
|
||||
"keywords": [
|
||||
"odoo",
|
||||
"express",
|
||||
"api",
|
||||
"hris",
|
||||
"fti"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "nodemon app.js",
|
||||
"start": "node app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
8
routes/attendanceRoutes.js
Normal file
8
routes/attendanceRoutes.js
Normal file
@ -0,0 +1,8 @@
|
||||
import express from "express";
|
||||
import { toggleAttendance } from "../controllers/attendanceController.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/check", toggleAttendance);
|
||||
|
||||
export default router;
|
13
routes/employeeRoutes.js
Normal file
13
routes/employeeRoutes.js
Normal file
@ -0,0 +1,13 @@
|
||||
// routes/employeeRoutes.js
|
||||
import express from "express";
|
||||
import {
|
||||
getEmployees,
|
||||
getEmployeeById,
|
||||
} from "../controllers/employeeController.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", getEmployees);
|
||||
router.get("/:employee_id", getEmployeeById);
|
||||
|
||||
export default router;
|
11
utils/date.js
Normal file
11
utils/date.js
Normal file
@ -0,0 +1,11 @@
|
||||
// utils/date.js
|
||||
|
||||
/**
|
||||
* Converts a JS Date or ISO string to Odoo datetime format: 'YYYY-MM-DD HH:mm:ss'
|
||||
* @param {Date|string} date
|
||||
* @returns {string} formatted datetime
|
||||
*/
|
||||
export function toOdooDatetimeFormat(date) {
|
||||
const d = new Date(date);
|
||||
return d.toISOString().slice(0, 19).replace("T", " ");
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user