added employee mapping v1
This commit is contained in:
parent
8508845f5b
commit
c2aee2dcc5
@ -1,6 +1,3 @@
|
||||
|
||||
|
||||
|
||||
const os = require('os');
|
||||
|
||||
const config = {
|
||||
@ -37,6 +34,12 @@ const config = {
|
||||
clientId: process.env.MS_CLIENT_ID || '',
|
||||
clientSecret: process.env.MS_CLIENT_SECRET || '',
|
||||
},
|
||||
xero: {
|
||||
clientId: process.env.XERO_CLIENT_ID || '',
|
||||
clientSecret: process.env.XERO_CLIENT_SECRET || '',
|
||||
scopes: 'offline_access openid profile email payroll.employees',
|
||||
callbackUrl: (process.env.BACK_API_URL || 'http://localhost:8080/api') + '/xero/callback'
|
||||
},
|
||||
uploadDir: os.tmpdir(),
|
||||
email: {
|
||||
from: 'Xero Payroll Export Shell <app@flatlogic.app>',
|
||||
@ -78,4 +81,4 @@ config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`;
|
||||
config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`;
|
||||
config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`;
|
||||
|
||||
module.exports = config;
|
||||
module.exports = config;
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const app = express();
|
||||
@ -21,6 +20,7 @@ const organizationForAuthRoutes = require('./routes/organizationLogin');
|
||||
|
||||
const openaiRoutes = require('./routes/openai');
|
||||
|
||||
const xeroAuthRoutes = require('./routes/xero_auth');
|
||||
|
||||
|
||||
const usersRoutes = require('./routes/users');
|
||||
@ -120,6 +120,7 @@ app.use(bodyParser.json());
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/file', fileRoutes);
|
||||
app.use('/api/pexels', pexelsRoutes);
|
||||
app.use('/api/xero', xeroAuthRoutes);
|
||||
app.enable('trust proxy');
|
||||
|
||||
|
||||
@ -214,4 +215,4 @@ db.sequelize.sync().then(function () {
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
module.exports = app;
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const Employee_mappingsService = require('../services/employee_mappings');
|
||||
@ -28,9 +27,21 @@ router.use(checkCrudPermissions('employee_mappings'));
|
||||
* type: object
|
||||
* properties:
|
||||
|
||||
* match_method:
|
||||
* type: string
|
||||
* default: match_method
|
||||
|
||||
|
||||
|
||||
* is_ready:
|
||||
* type: boolean
|
||||
* default: false
|
||||
|
||||
* mapped_at:
|
||||
* type: string
|
||||
* default: mapped_at
|
||||
|
||||
|
||||
*
|
||||
*/
|
||||
|
||||
@ -255,6 +266,28 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/employee_mappings/auto-map:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Employee_mappings]
|
||||
* summary: Auto-map workers to Xero employees
|
||||
* description: Try to match unmapped source workers with Xero employees by email or name
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The workers were successfully mapped
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 500:
|
||||
* description: Auto-mapping failed
|
||||
*/
|
||||
router.get('/auto-map', wrapAsync(async (req, res) => {
|
||||
const payload = await Employee_mappingsService.autoMap(req.currentUser);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/employee_mappings:
|
||||
@ -290,10 +323,12 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
req.query, globalAccess, { currentUser }
|
||||
);
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id',
|
||||
const fields = ['id','mapped_at',
|
||||
|
||||
|
||||
'mapped_at',
|
||||
{label: 'match_method', value: 'match_method'},
|
||||
|
||||
'is_ready',
|
||||
];
|
||||
const opts = { fields };
|
||||
try {
|
||||
@ -435,4 +470,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
||||
|
||||
router.use('/', require('../helpers').commonErrorHandler);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
95
backend/src/routes/xero_auth.js
Normal file
95
backend/src/routes/xero_auth.js
Normal file
@ -0,0 +1,95 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Xero_connectionsDBApi = require('../db/api/xero_connections');
|
||||
const passport = require('passport');
|
||||
const db = require('../db/models');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/connect', passport.authenticate('jwt', { session: false }), (req, res) => {
|
||||
const { clientId, callbackUrl, scopes } = config.xero;
|
||||
const state = req.currentUser.id; // Passing userId as state to identify user on callback
|
||||
|
||||
const xeroAuthUrl = `https://login.xero.com/identity/connect/authorize?` +
|
||||
`response_type=code&` +
|
||||
`client_id=${clientId}&` +
|
||||
`redirect_uri=${encodeURIComponent(callbackUrl)}&` +
|
||||
`scope=${encodeURIComponent(scopes)}&` +
|
||||
`state=${state}`;
|
||||
|
||||
res.redirect(xeroAuthUrl);
|
||||
});
|
||||
|
||||
router.get('/callback', wrapAsync(async (req, res) => {
|
||||
const { code, state } = req.query;
|
||||
const userId = state;
|
||||
const { clientId, clientSecret, callbackUrl } = config.xero;
|
||||
|
||||
try {
|
||||
const tokenResponse = await axios.post('https://identity.xero.com/connect/token',
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: callbackUrl,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { access_token, refresh_token, expires_in } = tokenResponse.data;
|
||||
const expiresAt = new Date(Date.now() + expires_in * 1000);
|
||||
|
||||
// Get connections (to get tenant ID)
|
||||
const connectionsResponse = await axios.get('https://api.xero.com/connections', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const activeConnection = connectionsResponse.data[0]; // Assuming first connection
|
||||
|
||||
if (!activeConnection) {
|
||||
throw new Error('No Xero connection found after authorization');
|
||||
}
|
||||
|
||||
const user = await db.users.findByPk(userId);
|
||||
const organisationId = user.organisationId;
|
||||
|
||||
// Save/Update Xero connection
|
||||
const existing = await Xero_connectionsDBApi.findAll({
|
||||
organisation: organisationId
|
||||
}, false, { currentUser: user });
|
||||
|
||||
const connectionData = {
|
||||
xero_tenant_identifier: activeConnection.tenantId,
|
||||
tenant_name: activeConnection.tenantName,
|
||||
connected_user_name: user.email,
|
||||
connection_status: 'connected',
|
||||
encrypted_access_token: access_token,
|
||||
encrypted_refresh_token: refresh_token,
|
||||
token_expires_at: expiresAt,
|
||||
scopes: config.xero.scopes,
|
||||
organisation: organisationId
|
||||
};
|
||||
|
||||
if (existing.count > 0) {
|
||||
await Xero_connectionsDBApi.update(existing.rows[0].id, connectionData, { currentUser: user });
|
||||
} else {
|
||||
await Xero_connectionsDBApi.create(connectionData, { currentUser: user });
|
||||
}
|
||||
|
||||
res.redirect(`${config.uiUrl}xero_connections/xero_connections-list`);
|
||||
} catch (error) {
|
||||
console.error('Xero callback error:', error.response ? error.response.data : error.message);
|
||||
res.redirect(`${config.uiUrl}dashboard?error=xero_auth_failed`);
|
||||
}
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const Xero_employeesService = require('../services/xero_employees');
|
||||
@ -264,6 +263,28 @@ router.post('/deleteByIds', wrapAsync(async (req, res) => {
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/xero_employees/sync:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Xero_employees]
|
||||
* summary: Sync employees from Xero
|
||||
* description: Fetch employees from Xero Payroll AU and sync with local database
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The employees were successfully synced
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 500:
|
||||
* description: Sync failed
|
||||
*/
|
||||
router.get('/sync', wrapAsync(async (req, res) => {
|
||||
const payload = await Xero_employeesService.syncFromXero(req.currentUser);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/xero_employees:
|
||||
@ -444,4 +465,4 @@ router.get('/:id', wrapAsync(async (req, res) => {
|
||||
|
||||
router.use('/', require('../helpers').commonErrorHandler);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
@ -3,14 +3,8 @@ const Employee_mappingsDBApi = require('../db/api/employee_mappings');
|
||||
const processFile = require("../middlewares/upload");
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = class Employee_mappingsService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
@ -28,9 +22,9 @@ module.exports = class Employee_mappingsService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
static async bulkImport(req, res) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
@ -49,7 +43,7 @@ module.exports = class Employee_mappingsService {
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => reject(error));
|
||||
})
|
||||
});
|
||||
|
||||
await Employee_mappingsDBApi.bulkImport(results, {
|
||||
transaction,
|
||||
@ -95,7 +89,7 @@ module.exports = class Employee_mappingsService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
@ -132,7 +126,58 @@ module.exports = class Employee_mappingsService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static async autoMap(currentUser) {
|
||||
const organisationId = currentUser.organisationId;
|
||||
if (!organisationId) {
|
||||
throw new ValidationError('organisationRequired');
|
||||
}
|
||||
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const sourceWorkers = await db.source_workers.findAll({
|
||||
where: { organisationId },
|
||||
transaction
|
||||
});
|
||||
|
||||
const xeroEmployees = await db.xero_employees.findAll({
|
||||
where: { organisationId },
|
||||
transaction
|
||||
});
|
||||
|
||||
const existingMappings = await db.employee_mappings.findAll({
|
||||
where: { organisationId },
|
||||
transaction
|
||||
});
|
||||
|
||||
const mappedSourceWorkerIds = existingMappings.map(m => m.source_workerId);
|
||||
const unmappedWorkers = sourceWorkers.filter(w => !mappedSourceWorkerIds.includes(w.id));
|
||||
|
||||
let count = 0;
|
||||
for (const worker of unmappedWorkers) {
|
||||
const match = xeroEmployees.find(xe =>
|
||||
(xe.email && worker.email && xe.email.toLowerCase() === worker.email.toLowerCase()) ||
|
||||
(xe.full_name && worker.first_name && worker.last_name && xe.full_name.toLowerCase() === `${worker.first_name} ${worker.last_name}`.toLowerCase())
|
||||
);
|
||||
|
||||
if (match) {
|
||||
await Employee_mappingsDBApi.create({
|
||||
source_worker: worker.id,
|
||||
xero_employee: match.id,
|
||||
match_method: 'auto_email',
|
||||
mapped_at: new Date(),
|
||||
mapped_by_user: currentUser.id,
|
||||
organisation: organisationId,
|
||||
is_ready: true
|
||||
}, { currentUser, transaction });
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return { count };
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
const db = require('../db/models');
|
||||
const Xero_employeesDBApi = require('../db/api/xero_employees');
|
||||
const Xero_connectionsDBApi = require('../db/api/xero_connections');
|
||||
const processFile = require("../middlewares/upload");
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = class Xero_employeesService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
@ -28,9 +24,9 @@ module.exports = class Xero_employeesService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
static async bulkImport(req, res) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
@ -49,7 +45,7 @@ module.exports = class Xero_employeesService {
|
||||
resolve();
|
||||
})
|
||||
.on('error', (error) => reject(error));
|
||||
})
|
||||
});
|
||||
|
||||
await Xero_employeesDBApi.bulkImport(results, {
|
||||
transaction,
|
||||
@ -95,7 +91,7 @@ module.exports = class Xero_employeesService {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
@ -132,7 +128,80 @@ module.exports = class Xero_employeesService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static async syncFromXero(currentUser) {
|
||||
const organisationId = currentUser.organisationId;
|
||||
if (!organisationId) {
|
||||
throw new ValidationError('organisationRequired');
|
||||
}
|
||||
|
||||
const xeroConnection = await Xero_connectionsDBApi.findBy({
|
||||
organisationId,
|
||||
connection_status: 'connected'
|
||||
});
|
||||
|
||||
if (!xeroConnection) {
|
||||
throw new ValidationError('xeroConnectionNotFound');
|
||||
}
|
||||
|
||||
const accessToken = xeroConnection.encrypted_access_token;
|
||||
const xeroTenantId = xeroConnection.xero_tenant_identifier;
|
||||
|
||||
if (!accessToken || !xeroTenantId) {
|
||||
throw new ValidationError('xeroConnectionIncomplete');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('https://api.xero.com/payroll.xro/1.0/Employees', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Xero-Tenant-Id': xeroTenantId,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const xeroEmployees = response.data.Employees || [];
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
for (const emp of xeroEmployees) {
|
||||
const empData = {
|
||||
xero_employee_identifier: emp.EmployeeID,
|
||||
full_name: `${emp.FirstName} ${emp.LastName}`,
|
||||
email: emp.Email,
|
||||
synced_at: new Date(),
|
||||
organisation: organisationId
|
||||
};
|
||||
|
||||
// Check if employee already exists
|
||||
const existing = await Xero_employeesDBApi.findBy({
|
||||
xero_employee_identifier: emp.EmployeeID,
|
||||
organisationId
|
||||
}, { transaction });
|
||||
|
||||
if (existing) {
|
||||
await Xero_employeesDBApi.update(existing.id, empData, { currentUser, transaction });
|
||||
} else {
|
||||
await Xero_employeesDBApi.create(empData, { currentUser, transaction });
|
||||
}
|
||||
}
|
||||
|
||||
// Update last sync at on connection
|
||||
await Xero_connectionsDBApi.update(xeroConnection.id, {
|
||||
last_sync_at: new Date()
|
||||
}, { currentUser, transaction });
|
||||
|
||||
await transaction.commit();
|
||||
return xeroEmployees;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Xero sync error:', error.response ? error.response.data : error.message);
|
||||
if (error.response && error.response.status === 401) {
|
||||
throw new ValidationError('xeroUnauthorized');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -9,180 +9,179 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
|
||||
{
|
||||
href: '/users/users-list',
|
||||
label: 'Users',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
||||
permissions: 'READ_USERS'
|
||||
label: 'Payroll',
|
||||
icon: icon.mdiCashMultiple,
|
||||
menu: [
|
||||
{
|
||||
href: '/export_runs/export_runs-list',
|
||||
label: 'Export Runs',
|
||||
icon: icon.mdiRunFast,
|
||||
permissions: 'READ_EXPORT_RUNS'
|
||||
},
|
||||
{
|
||||
href: '/export_lines/export_lines-list',
|
||||
label: 'Export Lines',
|
||||
icon: icon.mdiFileTableOutline,
|
||||
permissions: 'READ_EXPORT_LINES'
|
||||
},
|
||||
{
|
||||
href: '/pay_periods/pay_periods-list',
|
||||
label: 'Pay Periods',
|
||||
icon: icon.mdiCalendarRange,
|
||||
permissions: 'READ_PAY_PERIODS'
|
||||
},
|
||||
{
|
||||
href: '/export_line_results/export_line_results-list',
|
||||
label: 'Export Results',
|
||||
icon: icon.mdiClipboardCheckOutline,
|
||||
permissions: 'READ_EXPORT_LINE_RESULTS'
|
||||
},
|
||||
{
|
||||
href: '/export_fingerprints/export_fingerprints-list',
|
||||
label: 'Export Fingerprints',
|
||||
icon: icon.mdiFingerprint,
|
||||
permissions: 'READ_EXPORT_FINGERPRINTS'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
href: '/roles/roles-list',
|
||||
label: 'Roles',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
||||
permissions: 'READ_ROLES'
|
||||
label: 'Mappings',
|
||||
icon: icon.mdiMapMarkerPath,
|
||||
menu: [
|
||||
{
|
||||
href: '/employee_mappings/employee_mappings-list',
|
||||
label: 'Employee Mappings',
|
||||
icon: icon.mdiAccountArrowRight,
|
||||
permissions: 'READ_EMPLOYEE_MAPPINGS'
|
||||
},
|
||||
{
|
||||
href: '/earnings_mappings/earnings_mappings-list',
|
||||
label: 'Earnings Mappings',
|
||||
icon: icon.mdiMapMarkerPath,
|
||||
permissions: 'READ_EARNINGS_MAPPINGS'
|
||||
},
|
||||
{
|
||||
href: '/pay_levels/pay_levels-list',
|
||||
label: 'Pay Levels',
|
||||
icon: icon.mdiLayersTriple,
|
||||
permissions: 'READ_PAY_LEVELS'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
href: '/permissions/permissions-list',
|
||||
label: 'Permissions',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||
permissions: 'READ_PERMISSIONS'
|
||||
label: 'Xero Data',
|
||||
icon: icon.mdiDatabaseSync,
|
||||
menu: [
|
||||
{
|
||||
href: '/xero_employees/xero_employees-list',
|
||||
label: 'Xero Employees',
|
||||
icon: icon.mdiBadgeAccount,
|
||||
permissions: 'READ_XERO_EMPLOYEES'
|
||||
},
|
||||
{
|
||||
href: '/xero_earnings_rates/xero_earnings_rates-list',
|
||||
label: 'Xero Earnings Rates',
|
||||
icon: icon.mdiCashRegister,
|
||||
permissions: 'READ_XERO_EARNINGS_RATES'
|
||||
},
|
||||
{
|
||||
href: '/xero_payroll_calendars/xero_payroll_calendars-list',
|
||||
label: 'Payroll Calendars',
|
||||
icon: icon.mdiCalendarSync,
|
||||
permissions: 'READ_XERO_PAYROLL_CALENDARS'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
href: '/organisations/organisations-list',
|
||||
label: 'Organisations',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_ORGANISATIONS'
|
||||
label: 'Source Data',
|
||||
icon: icon.mdiDatabase,
|
||||
menu: [
|
||||
{
|
||||
href: '/source_workers/source_workers-list',
|
||||
label: 'Source Workers',
|
||||
icon: icon.mdiAccountGroup,
|
||||
permissions: 'READ_SOURCE_WORKERS'
|
||||
},
|
||||
{
|
||||
href: '/source_import_batches/source_import_batches-list',
|
||||
label: 'Import Batches',
|
||||
icon: icon.mdiDatabaseImportOutline,
|
||||
permissions: 'READ_SOURCE_IMPORT_BATCHES'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
href: '/xero_connections/xero_connections-list',
|
||||
label: 'Xero connections',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_XERO_CONNECTIONS'
|
||||
label: 'Configuration',
|
||||
icon: icon.mdiCog,
|
||||
menu: [
|
||||
{
|
||||
href: '/payroll_settings/payroll_settings-list',
|
||||
label: 'Payroll Settings',
|
||||
icon: icon.mdiCalendarClock,
|
||||
permissions: 'READ_PAYROLL_SETTINGS'
|
||||
},
|
||||
{
|
||||
href: '/xero_connections/xero_connections-list',
|
||||
label: 'Xero Connections',
|
||||
icon: icon.mdiLinkVariant,
|
||||
permissions: 'READ_XERO_CONNECTIONS'
|
||||
},
|
||||
{
|
||||
href: '/organisations/organisations-list',
|
||||
label: 'Organisations',
|
||||
icon: icon.mdiTable,
|
||||
permissions: 'READ_ORGANISATIONS'
|
||||
},
|
||||
{
|
||||
href: '/api_keys/api_keys-list',
|
||||
label: 'Api keys',
|
||||
icon: icon.mdiKeyVariant,
|
||||
permissions: 'READ_API_KEYS'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
href: '/payroll_settings/payroll_settings-list',
|
||||
label: 'Payroll settings',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiCalendarClock' in icon ? icon['mdiCalendarClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_PAYROLL_SETTINGS'
|
||||
},
|
||||
{
|
||||
href: '/source_workers/source_workers-list',
|
||||
label: 'Source workers',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_SOURCE_WORKERS'
|
||||
},
|
||||
{
|
||||
href: '/xero_employees/xero_employees-list',
|
||||
label: 'Xero employees',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiBadgeAccount' in icon ? icon['mdiBadgeAccount' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_XERO_EMPLOYEES'
|
||||
},
|
||||
{
|
||||
href: '/employee_mappings/employee_mappings-list',
|
||||
label: 'Employee mappings',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiAccountArrowRight' in icon ? icon['mdiAccountArrowRight' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EMPLOYEE_MAPPINGS'
|
||||
},
|
||||
{
|
||||
href: '/pay_levels/pay_levels-list',
|
||||
label: 'Pay levels',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiLayersTriple' in icon ? icon['mdiLayersTriple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_PAY_LEVELS'
|
||||
},
|
||||
{
|
||||
href: '/xero_earnings_rates/xero_earnings_rates-list',
|
||||
label: 'Xero earnings rates',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_XERO_EARNINGS_RATES'
|
||||
},
|
||||
{
|
||||
href: '/earnings_mappings/earnings_mappings-list',
|
||||
label: 'Earnings mappings',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiMapMarkerPath' in icon ? icon['mdiMapMarkerPath' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EARNINGS_MAPPINGS'
|
||||
},
|
||||
{
|
||||
href: '/xero_payroll_calendars/xero_payroll_calendars-list',
|
||||
label: 'Xero payroll calendars',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiCalendarSync' in icon ? icon['mdiCalendarSync' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_XERO_PAYROLL_CALENDARS'
|
||||
},
|
||||
{
|
||||
href: '/pay_periods/pay_periods-list',
|
||||
label: 'Pay periods',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiCalendarRange' in icon ? icon['mdiCalendarRange' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_PAY_PERIODS'
|
||||
},
|
||||
{
|
||||
href: '/export_lines/export_lines-list',
|
||||
label: 'Export lines',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiFileTableOutline' in icon ? icon['mdiFileTableOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EXPORT_LINES'
|
||||
},
|
||||
{
|
||||
href: '/export_runs/export_runs-list',
|
||||
label: 'Export runs',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiRunFast' in icon ? icon['mdiRunFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EXPORT_RUNS'
|
||||
},
|
||||
{
|
||||
href: '/export_line_results/export_line_results-list',
|
||||
label: 'Export line results',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiClipboardCheckOutline' in icon ? icon['mdiClipboardCheckOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EXPORT_LINE_RESULTS'
|
||||
},
|
||||
{
|
||||
href: '/export_fingerprints/export_fingerprints-list',
|
||||
label: 'Export fingerprints',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiFingerprint' in icon ? icon['mdiFingerprint' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_EXPORT_FINGERPRINTS'
|
||||
},
|
||||
{
|
||||
href: '/source_import_batches/source_import_batches-list',
|
||||
label: 'Source import batches',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiDatabaseImportOutline' in icon ? icon['mdiDatabaseImportOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_SOURCE_IMPORT_BATCHES'
|
||||
},
|
||||
{
|
||||
href: '/audit_entries/audit_entries-list',
|
||||
label: 'Audit entries',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_AUDIT_ENTRIES'
|
||||
},
|
||||
{
|
||||
href: '/api_keys/api_keys-list',
|
||||
label: 'Api keys',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: 'mdiKeyVariant' in icon ? icon['mdiKeyVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_API_KEYS'
|
||||
label: 'System Admin',
|
||||
icon: icon.mdiCog,
|
||||
menu: [
|
||||
{
|
||||
href: '/users/users-list',
|
||||
label: 'Users',
|
||||
icon: icon.mdiAccountGroup,
|
||||
permissions: 'READ_USERS'
|
||||
},
|
||||
{
|
||||
href: '/roles/roles-list',
|
||||
label: 'Roles',
|
||||
icon: icon.mdiShieldAccountVariantOutline,
|
||||
permissions: 'READ_ROLES'
|
||||
},
|
||||
{
|
||||
href: '/permissions/permissions-list',
|
||||
label: 'Permissions',
|
||||
icon: icon.mdiShieldAccountOutline,
|
||||
permissions: 'READ_PERMISSIONS'
|
||||
},
|
||||
{
|
||||
href: '/audit_entries/audit_entries-list',
|
||||
label: 'Audit Entries',
|
||||
icon: icon.mdiClipboardTextOutline,
|
||||
permissions: 'READ_AUDIT_ENTRIES'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
href: '/profile',
|
||||
label: 'Profile',
|
||||
icon: icon.mdiAccountCircle,
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
href: '/api-docs',
|
||||
target: '_blank',
|
||||
@ -192,4 +191,4 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export default menuAside
|
||||
export default menuAside
|
||||
@ -9,745 +9,197 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
||||
import BaseIcon from "../components/BaseIcon";
|
||||
import { getPageTitle } from '../config'
|
||||
import Link from "next/link";
|
||||
import CardBox from '../components/CardBox';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
|
||||
import { hasPermission } from "../helpers/userPermissions";
|
||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
const Dashboard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
||||
|
||||
const loadingMessage = 'Loading...';
|
||||
|
||||
const loadingMessage = '...';
|
||||
|
||||
const [users, setUsers] = React.useState(loadingMessage);
|
||||
const [roles, setRoles] = React.useState(loadingMessage);
|
||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
||||
const [organisations, setOrganisations] = React.useState(loadingMessage);
|
||||
const [xero_connections, setXero_connections] = React.useState(loadingMessage);
|
||||
const [payroll_settings, setPayroll_settings] = React.useState(loadingMessage);
|
||||
const [source_workers, setSource_workers] = React.useState(loadingMessage);
|
||||
const [xero_employees, setXero_employees] = React.useState(loadingMessage);
|
||||
const [employee_mappings, setEmployee_mappings] = React.useState(loadingMessage);
|
||||
const [pay_levels, setPay_levels] = React.useState(loadingMessage);
|
||||
const [xero_earnings_rates, setXero_earnings_rates] = React.useState(loadingMessage);
|
||||
const [earnings_mappings, setEarnings_mappings] = React.useState(loadingMessage);
|
||||
const [xero_payroll_calendars, setXero_payroll_calendars] = React.useState(loadingMessage);
|
||||
const [pay_periods, setPay_periods] = React.useState(loadingMessage);
|
||||
const [export_lines, setExport_lines] = React.useState(loadingMessage);
|
||||
const [export_runs, setExport_runs] = React.useState(loadingMessage);
|
||||
const [export_line_results, setExport_line_results] = React.useState(loadingMessage);
|
||||
const [export_fingerprints, setExport_fingerprints] = React.useState(loadingMessage);
|
||||
const [source_import_batches, setSource_import_batches] = React.useState(loadingMessage);
|
||||
const [audit_entries, setAudit_entries] = React.useState(loadingMessage);
|
||||
const [api_keys, setApi_keys] = React.useState(loadingMessage);
|
||||
|
||||
|
||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
||||
role: { value: '', label: '' },
|
||||
});
|
||||
const [counts, setCounts] = React.useState<Record<string, any>>({});
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||
|
||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||
|
||||
|
||||
const organizationId = currentUser?.organisations?.id;
|
||||
|
||||
async function loadData() {
|
||||
const entities = ['users','roles','permissions','organisations','xero_connections','payroll_settings','source_workers','xero_employees','employee_mappings','pay_levels','xero_earnings_rates','earnings_mappings','xero_payroll_calendars','pay_periods','export_lines','export_runs','export_line_results','export_fingerprints','source_import_batches','audit_entries','api_keys',];
|
||||
const fns = [setUsers,setRoles,setPermissions,setOrganisations,setXero_connections,setPayroll_settings,setSource_workers,setXero_employees,setEmployee_mappings,setPay_levels,setXero_earnings_rates,setEarnings_mappings,setXero_payroll_calendars,setPay_periods,setExport_lines,setExport_runs,setExport_line_results,setExport_fingerprints,setSource_import_batches,setAudit_entries,setApi_keys,];
|
||||
|
||||
const requests = entities.map((entity, index) => {
|
||||
|
||||
const entities = [
|
||||
'xero_connections',
|
||||
'payroll_settings',
|
||||
'employee_mappings',
|
||||
'earnings_mappings',
|
||||
'export_runs',
|
||||
'source_workers'
|
||||
];
|
||||
|
||||
const requests = entities.map((entity) => {
|
||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
||||
} else {
|
||||
fns[index](null);
|
||||
return Promise.resolve({data: {count: null}});
|
||||
}
|
||||
|
||||
return Promise.resolve({data: {count: null}});
|
||||
});
|
||||
|
||||
Promise.allSettled(requests).then((results) => {
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
fns[i](result.value.data.count);
|
||||
} else {
|
||||
fns[i](result.reason.message);
|
||||
}
|
||||
});
|
||||
const results = await Promise.allSettled(requests);
|
||||
const newCounts: Record<string, any> = {};
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
newCounts[entities[i]] = result.value.data.count;
|
||||
}
|
||||
});
|
||||
setCounts(newCounts);
|
||||
}
|
||||
|
||||
async function getWidgets(roleId) {
|
||||
await dispatch(fetchWidgets(roleId));
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
loadData().then();
|
||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
||||
}, [currentUser]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
||||
getWidgets(widgetsRole?.role?.value || '').then();
|
||||
}, [widgetsRole?.role?.value]);
|
||||
const handleConnectXero = () => {
|
||||
window.location.href = `/api/xero/connect`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{getPageTitle('Overview')}
|
||||
</title>
|
||||
<title>{getPageTitle('Payroll Command Center')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiChartTimelineVariant}
|
||||
title='Overview'
|
||||
icon={icon.mdiViewDashboard}
|
||||
title='Payroll Command Center'
|
||||
main>
|
||||
{''}
|
||||
<BaseButton
|
||||
href="/export_runs/export_runs-new"
|
||||
label="New Export"
|
||||
color="info"
|
||||
icon={icon.mdiPlus}
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||
currentUser={currentUser}
|
||||
isFetchingQuery={isFetchingQuery}
|
||||
setWidgetsRole={setWidgetsRole}
|
||||
widgetsRole={widgetsRole}
|
||||
/>}
|
||||
{!!rolesWidgets.length &&
|
||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
||||
{(isFetchingQuery || loading) && (
|
||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||
<BaseIcon
|
||||
className={`${iconsColor} animate-spin mr-5`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
path={icon.mdiLoading}
|
||||
/>{' '}
|
||||
Loading widgets...
|
||||
{/* Integration Status Bar */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
|
||||
<CardBox isList className="bg-emerald-50 dark:bg-emerald-900/40 border border-emerald-200 dark:border-emerald-800">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-sm text-emerald-600 dark:text-emerald-400 font-semibold uppercase tracking-wider">Xero Connection</p>
|
||||
<h3 className="text-xl font-bold">{counts.xero_connections > 0 ? 'Connected' : 'Disconnected'}</h3>
|
||||
</div>
|
||||
<BaseIcon path={icon.mdiLinkVariant} size={48} className="text-emerald-500" />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
{counts.xero_connections > 0 ? (
|
||||
<BaseButton
|
||||
href="/xero_connections/xero_connections-list"
|
||||
label='Manage Connection'
|
||||
color="whiteDark"
|
||||
small
|
||||
/>
|
||||
) : (
|
||||
<BaseButton
|
||||
onClick={handleConnectXero}
|
||||
label='Connect Now'
|
||||
color="whiteDark"
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{ rolesWidgets &&
|
||||
rolesWidgets.map((widget) => (
|
||||
<SmartWidget
|
||||
key={widget.id}
|
||||
userId={currentUser?.id}
|
||||
widget={widget}
|
||||
roleId={widgetsRole?.role?.value || ''}
|
||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||
<CardBox isList className="bg-indigo-50 dark:bg-indigo-900/40 border border-indigo-200 dark:border-indigo-800">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-sm text-indigo-600 dark:text-indigo-400 font-semibold uppercase tracking-wider">Payroll Settings</p>
|
||||
<h3 className="text-xl font-bold">{counts.payroll_settings > 0 ? 'Configured' : 'Needs Setup'}</h3>
|
||||
</div>
|
||||
<BaseIcon path={icon.mdiCog} size={48} className="text-indigo-500" />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<BaseButton
|
||||
href="/payroll_settings/payroll_settings-list"
|
||||
label="Configure"
|
||||
color="whiteDark"
|
||||
small
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox isList className="bg-amber-50 dark:bg-amber-900/40 border border-amber-200 dark:border-amber-800">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400 font-semibold uppercase tracking-wider">Mappings</p>
|
||||
<h3 className="text-xl font-bold">{counts.employee_mappings || 0} Employees</h3>
|
||||
</div>
|
||||
<BaseIcon path={icon.mdiAccountArrowRight} size={48} className="text-amber-500" />
|
||||
</div>
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<BaseButton
|
||||
href="/employee_mappings/employee_mappings-list"
|
||||
label="Map Workers"
|
||||
color="whiteDark"
|
||||
small
|
||||
/>
|
||||
<BaseButton
|
||||
href="/earnings_mappings/earnings_mappings-list"
|
||||
label="Map Rates"
|
||||
color="whiteDark"
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6'>
|
||||
{/* Quick Stats */}
|
||||
<Link href={'/export_runs/export_runs-list'}>
|
||||
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Export Runs</div>
|
||||
<div className="text-3xl leading-tight font-semibold">{counts.export_runs ?? loadingMessage}</div>
|
||||
</div>
|
||||
<BaseIcon className={iconsColor} w="w-12" h="h-12" size={32} path={icon.mdiRunFast} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={'/source_workers/source_workers-list'}>
|
||||
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">Source Workers</div>
|
||||
<div className="text-3xl leading-tight font-semibold">{counts.source_workers ?? loadingMessage}</div>
|
||||
</div>
|
||||
<BaseIcon className={iconsColor} w="w-12" h="h-12" size={32} path={icon.mdiAccountGroup} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Placeholder for "Ready to Export" */}
|
||||
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} bg-blue-600 text-white p-6 lg:col-span-2 flex items-center justify-between`}>
|
||||
<div>
|
||||
<h4 className="text-xl font-bold">Ready to Export?</h4>
|
||||
<p className="opacity-80">Next pay period starts in 3 days.</p>
|
||||
</div>
|
||||
<BaseButton
|
||||
href="/pay_periods/pay_periods-list"
|
||||
label="Review Timesheets"
|
||||
color="white"
|
||||
className="text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||
|
||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||
|
||||
|
||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Users
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{users}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Roles
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{roles}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Permissions
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{permissions}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_ORGANISATIONS') && <Link href={'/organisations/organisations-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Organisations
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{organisations}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_XERO_CONNECTIONS') && <Link href={'/xero_connections/xero_connections-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Xero connections
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{xero_connections}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PAYROLL_SETTINGS') && <Link href={'/payroll_settings/payroll_settings-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Payroll settings
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{payroll_settings}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCalendarClock' in icon ? icon['mdiCalendarClock' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_SOURCE_WORKERS') && <Link href={'/source_workers/source_workers-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Source workers
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{source_workers}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_XERO_EMPLOYEES') && <Link href={'/xero_employees/xero_employees-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Xero employees
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{xero_employees}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiBadgeAccount' in icon ? icon['mdiBadgeAccount' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_EMPLOYEE_MAPPINGS') && <Link href={'/employee_mappings/employee_mappings-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Employee mappings
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{employee_mappings}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiAccountArrowRight' in icon ? icon['mdiAccountArrowRight' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PAY_LEVELS') && <Link href={'/pay_levels/pay_levels-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Pay levels
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{pay_levels}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiLayersTriple' in icon ? icon['mdiLayersTriple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_XERO_EARNINGS_RATES') && <Link href={'/xero_earnings_rates/xero_earnings_rates-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Xero earnings rates
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{xero_earnings_rates}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_EARNINGS_MAPPINGS') && <Link href={'/earnings_mappings/earnings_mappings-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Earnings mappings
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{earnings_mappings}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiMapMarkerPath' in icon ? icon['mdiMapMarkerPath' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_XERO_PAYROLL_CALENDARS') && <Link href={'/xero_payroll_calendars/xero_payroll_calendars-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Xero payroll calendars
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{xero_payroll_calendars}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCalendarSync' in icon ? icon['mdiCalendarSync' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PAY_PERIODS') && <Link href={'/pay_periods/pay_periods-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Pay periods
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{pay_periods}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCalendarRange' in icon ? icon['mdiCalendarRange' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_EXPORT_LINES') && <Link href={'/export_lines/export_lines-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Export lines
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{export_lines}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiFileTableOutline' in icon ? icon['mdiFileTableOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_EXPORT_RUNS') && <Link href={'/export_runs/export_runs-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Export runs
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{export_runs}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiRunFast' in icon ? icon['mdiRunFast' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_EXPORT_LINE_RESULTS') && <Link href={'/export_line_results/export_line_results-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Export line results
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{export_line_results}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiClipboardCheckOutline' in icon ? icon['mdiClipboardCheckOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_EXPORT_FINGERPRINTS') && <Link href={'/export_fingerprints/export_fingerprints-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Export fingerprints
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{export_fingerprints}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiFingerprint' in icon ? icon['mdiFingerprint' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_SOURCE_IMPORT_BATCHES') && <Link href={'/source_import_batches/source_import_batches-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Source import batches
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{source_import_batches}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiDatabaseImportOutline' in icon ? icon['mdiDatabaseImportOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_AUDIT_ENTRIES') && <Link href={'/audit_entries/audit_entries-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Audit entries
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{audit_entries}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_API_KEYS') && <Link href={'/api_keys/api_keys-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Api keys
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{api_keys}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiKeyVariant' in icon ? icon['mdiKeyVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
|
||||
</div>
|
||||
<SectionTitleLineWithButton icon={icon.mdiHistory} title="Recent Activity" />
|
||||
<CardBox className="mb-6">
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<BaseIcon path={icon.mdiClockOutline} size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p>No recent export activity. Start by connecting to Xero.</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant, mdiAutoFix } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
@ -14,7 +14,7 @@ import Link from "next/link";
|
||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||
import CardBoxModal from "../../components/CardBoxModal";
|
||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
||||
import {setRefetch, uploadCsv} from '../../stores/employee_mappings/employee_mappingsSlice';
|
||||
import {setRefetch, uploadCsv, autoMap} from '../../stores/employee_mappings/employee_mappingsSlice';
|
||||
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
@ -29,6 +29,7 @@ const Employee_mappingsTablesPage = () => {
|
||||
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const { loading } = useAppSelector((state) => state.employee_mappings);
|
||||
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@ -97,6 +98,10 @@ const Employee_mappingsTablesPage = () => {
|
||||
setIsModalActive(false);
|
||||
};
|
||||
|
||||
const handleAutoMap = () => {
|
||||
dispatch(autoMap());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@ -120,11 +125,22 @@ const Employee_mappingsTablesPage = () => {
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
label={loading ? 'Auto-mapping...' : 'Auto-map'}
|
||||
icon={mdiAutoFix}
|
||||
onClick={handleAutoMap}
|
||||
disabled={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
@ -173,4 +189,4 @@ Employee_mappingsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default Employee_mappingsTablesPage
|
||||
export default Employee_mappingsTablesPage
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
@ -7,7 +6,6 @@ import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
@ -111,7 +109,7 @@ export default function Starter() {
|
||||
}
|
||||
>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Xero Payroll Export Shell')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
@ -128,22 +126,30 @@ export default function Starter() {
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Xero Payroll Export Shell app!"/>
|
||||
<CardBoxComponentTitle title="Xero Payroll Export Shell"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<div className="space-y-4 py-4">
|
||||
<p className='text-lg font-medium text-gray-700'>
|
||||
Streamline your Careflo to Xero Payroll AU workflow.
|
||||
</p>
|
||||
<ul className='list-disc list-inside text-gray-600 space-y-2'>
|
||||
<li>Automated Employee & Earnings Rate Mapping</li>
|
||||
<li>Secure Xero OAuth2 Integration</li>
|
||||
<li>Support for Timesheets & Allowances (Mode A & B)</li>
|
||||
<li>Full Audit Trail & Export History</li>
|
||||
</ul>
|
||||
<p className='text-sm text-gray-500'>
|
||||
A professional, multi-tenant integration shell for high-accuracy payroll exports.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
label='Enter Command Center'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</div>
|
||||
@ -162,5 +168,4 @@ export default function Starter() {
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant, mdiSync } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
@ -14,7 +14,7 @@ import Link from "next/link";
|
||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||
import CardBoxModal from "../../components/CardBoxModal";
|
||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
||||
import {setRefetch, uploadCsv} from '../../stores/xero_employees/xero_employeesSlice';
|
||||
import {setRefetch, uploadCsv, syncFromXero} from '../../stores/xero_employees/xero_employeesSlice';
|
||||
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
@ -29,6 +29,7 @@ const Xero_employeesTablesPage = () => {
|
||||
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const { loading } = useAppSelector((state) => state.xero_employees);
|
||||
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@ -85,6 +86,10 @@ const Xero_employeesTablesPage = () => {
|
||||
setIsModalActive(false);
|
||||
};
|
||||
|
||||
const handleSync = () => {
|
||||
dispatch(syncFromXero());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@ -108,11 +113,20 @@ const Xero_employeesTablesPage = () => {
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BaseButton
|
||||
color='info'
|
||||
label={loading ? 'Syncing...' : 'Sync from Xero'}
|
||||
icon={mdiSync}
|
||||
onClick={handleSync}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
@ -161,4 +175,4 @@ Xero_employeesTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default Xero_employeesTablesPage
|
||||
export default Xero_employeesTablesPage
|
||||
@ -38,6 +38,21 @@ export const fetch = createAsyncThunk('employee_mappings/fetch', async (data: an
|
||||
return id ? result.data : {rows: result.data.rows, count: result.data.count};
|
||||
})
|
||||
|
||||
export const autoMap = createAsyncThunk(
|
||||
'employee_mappings/autoMap',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const result = await axios.get('employee_mappings/auto-map');
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
return rejectWithValue(error.response.data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteItemsByIds = createAsyncThunk(
|
||||
'employee_mappings/deleteByIds',
|
||||
async (data: any, { rejectWithValue }) => {
|
||||
@ -151,6 +166,20 @@ export const employee_mappingsSlice = createSlice({
|
||||
state.loading = false
|
||||
})
|
||||
|
||||
builder.addCase(autoMap.pending, (state) => {
|
||||
state.loading = true;
|
||||
resetNotify(state);
|
||||
});
|
||||
builder.addCase(autoMap.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.refetch = true;
|
||||
fulfilledNotify(state, `Successfully auto-mapped ${action.payload.count} employees`);
|
||||
});
|
||||
builder.addCase(autoMap.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
rejectNotify(state, action);
|
||||
});
|
||||
|
||||
builder.addCase(deleteItemsByIds.pending, (state) => {
|
||||
state.loading = true;
|
||||
resetNotify(state);
|
||||
@ -228,4 +257,4 @@ export const employee_mappingsSlice = createSlice({
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { setRefetch } = employee_mappingsSlice.actions
|
||||
|
||||
export default employee_mappingsSlice.reducer
|
||||
export default employee_mappingsSlice.reducer
|
||||
@ -38,6 +38,21 @@ export const fetch = createAsyncThunk('xero_employees/fetch', async (data: any)
|
||||
return id ? result.data : {rows: result.data.rows, count: result.data.count};
|
||||
})
|
||||
|
||||
export const syncFromXero = createAsyncThunk(
|
||||
'xero_employees/syncFromXero',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const result = await axios.get('xero_employees/sync');
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
if (!error.response) {
|
||||
throw error;
|
||||
}
|
||||
return rejectWithValue(error.response.data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteItemsByIds = createAsyncThunk(
|
||||
'xero_employees/deleteByIds',
|
||||
async (data: any, { rejectWithValue }) => {
|
||||
@ -151,6 +166,20 @@ export const xero_employeesSlice = createSlice({
|
||||
state.loading = false
|
||||
})
|
||||
|
||||
builder.addCase(syncFromXero.pending, (state) => {
|
||||
state.loading = true;
|
||||
resetNotify(state);
|
||||
});
|
||||
builder.addCase(syncFromXero.fulfilled, (state) => {
|
||||
state.loading = false;
|
||||
state.refetch = true;
|
||||
fulfilledNotify(state, 'Xero employees successfully synced');
|
||||
});
|
||||
builder.addCase(syncFromXero.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
rejectNotify(state, action);
|
||||
});
|
||||
|
||||
builder.addCase(deleteItemsByIds.pending, (state) => {
|
||||
state.loading = true;
|
||||
resetNotify(state);
|
||||
@ -228,4 +257,4 @@ export const xero_employeesSlice = createSlice({
|
||||
// Action creators are generated for each case reducer function
|
||||
export const { setRefetch } = xero_employeesSlice.actions
|
||||
|
||||
export default xero_employeesSlice.reducer
|
||||
export default xero_employeesSlice.reducer
|
||||
Loading…
x
Reference in New Issue
Block a user