diff --git a/backend/app.js b/backend/app.js index 373a547..0ade124 100644 --- a/backend/app.js +++ b/backend/app.js @@ -40,6 +40,7 @@ app.use('/api/products', require('./routes/products')); app.use('/api/customers', require('./routes/customers')); app.use('/api/suppliers', require('./routes/suppliers')); app.use('/api/subcontractors', require('./routes/subcontractors')); +app.use('/api/project-receipts', require('./routes/project-receipts')); app.use('/api/projects', require('./routes/projects')); app.use('/api/upload', require('./routes/upload')); app.use('/api/construction', require('./routes/construction')); @@ -69,6 +70,7 @@ app.use('/api/process-templates', require('./routes/process-templates')); app.use('/api/receiving', require('./routes/receiving')); app.use('/api/expense-categories', require('./routes/expense-categories')); app.use('/api/financial-records', require('./routes/financial-records')); +app.use('/api/cash-management', require('./routes/cash-management')); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../frontend/dist/index.html')); diff --git a/backend/public/templates/财务记账导入模板.xlsx b/backend/public/templates/财务记账导入模板.xlsx index cf4b8a4..15ff74f 100644 Binary files a/backend/public/templates/财务记账导入模板.xlsx and b/backend/public/templates/财务记账导入模板.xlsx differ diff --git a/backend/routes/advances.js b/backend/routes/advances.js index c49285f..69cd6c5 100644 --- a/backend/routes/advances.js +++ b/backend/routes/advances.js @@ -1,216 +1,216 @@ -const express = require('express'); -const db = require('../db'); -const { authenticate, requireAdmin } = require('../middleware/auth'); -const { body, validationResult } = require('express-validator'); - -const router = express.Router(); - -// 验证错误处理中间件 -const validate = (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ - success: false, - errors: errors.array() - }); - } - next(); -}; - -router.get('/', async (req, res) => { - try { - const result = await db.query(` - SELECT a.*, u.name as user_name, p.name as project_name - FROM advances a - LEFT JOIN users u ON a.applicant_id = u.id - LEFT JOIN projects p ON a.project_id = p.id - ORDER BY a.created_at DESC - `); - - // 解析每个预支申请的 attachments 字段为数组 - const data = result.rows.map(item => { - if (item.attachments) { - try { - item.attachments = JSON.parse(item.attachments); - } catch (error) { - item.attachments = []; - } - } else { - item.attachments = []; - } - return item; - }); - - res.json({ success: true, data, count: data.length }); - } catch (error) { - console.error('获取预支款失败:', error); - res.status(500).json({ - success: false, - message: '获取预支款失败', - error: process.env.NODE_ENV === 'development' ? error.message : '操作失败' - }); - } -}); - -router.post('/', [ - body('amount').isFloat({ min: 0.01 }), - body('reason').notEmpty() -], validate, async (req, res) => { - try { - const { amount, reason, project_id, currency, advance_date, attachments, amount_cny, applicant, status } = req.body; - const user_id = 1; // 临时使用admin用户 - - // 生成预支编号 - const advanceCode = `ADV-${Date.now()}`; - - const result = await db.query( - 'INSERT INTO advances (applicant_id, project_id, amount, currency, amount_cny, reason, advance_date, advance_code, status, applicant, attachments) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id', - [applicant_id, project_id, amount, currency || 'CNY', amount_cny || 0, reason, advance_date, advanceCode, status || 'pending', applicant, JSON.stringify(attachments || [])] - ); - - const data = { id: result.rows[0]?.id, applicant_id, project_id, amount, currency, reason, advance_date, advance_code: advanceCode, status, applicant }; - res.json({ success: true, data }); - } catch (error) { - console.error('创建预支申请失败:', error); - res.status(500).json({ success: false, message: '创建预支申请失败' }); - } -}); - -router.get('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('SELECT * FROM advances WHERE id = $1', [id]); - - if (result.rows.length > 0) { - const data = result.rows[0]; - // 解析 attachments 字段为数组 - if (data.attachments) { - try { - data.attachments = JSON.parse(data.attachments); - } catch (error) { - data.attachments = []; - } - } else { - data.attachments = []; - } - res.json({ success: true, data }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('获取预支申请失败:', error); - res.status(500).json({ success: false, message: '获取预支申请失败' }); - } -}); - -router.put('/:id', async (req, res) => { - try { - const { id } = req.params; - const { amount, reason, project_id, currency, advance_date, attachments, amount_cny, applicant, status } = req.body; - - const result = await db.query( - 'UPDATE advances SET amount = $1, reason = $2, project_id = $3, currency = $4, advance_date = $5, attachments = $6, amount_cny = $7, applicant = $8, status = $9 WHERE id = $10', - [amount, reason, project_id, currency, advance_date, JSON.stringify(attachments || []), amount_cny, applicant, status, id] - ); - - if (result.changes > 0) { - res.json({ success: true, message: '更新成功' }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('更新预支申请失败:', error); - res.status(500).json({ success: false, message: '更新预支申请失败' }); - } -}); - -router.delete('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('DELETE FROM advances WHERE id = $1', [id]); - - if (result.changes > 0) { - res.json({ success: true, message: '删除成功' }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('删除预支申请失败:', error); - res.status(500).json({ success: false, message: '删除预支申请失败' }); - } -}); - -router.post('/:id/submit', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['pending', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '提交成功' }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('提交预支申请失败:', error); - res.status(500).json({ success: false, message: '提交预支申请失败' }); - } -}); - -router.post('/:id/withdraw', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['withdrawn', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '撤回成功' }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('撤回预支申请失败:', error); - res.status(500).json({ success: false, message: '撤回预支申请失败' }); - } -}); - -router.post('/:id/approve', async (req, res) => { - try { - const { id } = req.params; - const { remark } = req.body; - - const result = await db.query('UPDATE advances SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); - - if (result.changes > 0) { - res.json({ success: true, message: '审批通过成功' }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('审批预支申请失败:', error); - res.status(500).json({ success: false, message: '审批预支申请失败' }); - } -}); - -router.post('/:id/reject', async (req, res) => { - try { - const { id } = req.params; - const { rejectReason } = req.body; - - const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['pending_edit', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '退回成功' }); - } else { - res.status(404).json({ success: false, message: '预支申请不存在' }); - } - } catch (error) { - console.error('退回预支申请失败:', error); - res.status(500).json({ success: false, message: '退回预支申请失败' }); - } -}); - - +const express = require('express'); +const db = require('../db'); +const { authenticate, requireAdmin } = require('../middleware/auth'); +const { body, validationResult } = require('express-validator'); + +const router = express.Router(); + +// 验证错误处理中间件 +const validate = (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + next(); +}; + +router.get('/', async (req, res) => { + try { + const result = await db.query(` + SELECT a.*, u.name as user_name, p.name as project_name + FROM advances a + LEFT JOIN users u ON a.applicant_id = u.id + LEFT JOIN projects p ON a.project_id = p.id + ORDER BY a.created_at DESC + `); + + // 解析每个预支申请的 attachments 字段为数组 + const data = result.rows.map(item => { + if (item.attachments) { + try { + item.attachments = JSON.parse(item.attachments); + } catch (error) { + item.attachments = []; + } + } else { + item.attachments = []; + } + return item; + }); + + res.json({ success: true, data, count: data.length }); + } catch (error) { + console.error('获取预支款失败:', error); + res.status(500).json({ + success: false, + message: '获取预支款失败', + error: process.env.NODE_ENV === 'development' ? error.message : '操作失败' + }); + } +}); + +router.post('/', [ + body('amount').isFloat({ min: 0.01 }), + body('reason').notEmpty() +], validate, async (req, res) => { + try { + const { amount, reason, project_id, currency, advance_date, attachments, amount_cny, applicant, status } = req.body; + const user_id = 1; // 临时使用admin用户 + + // 生成预支编号 + const advanceCode = `ADV-${Date.now()}`; + + const result = await db.query( + 'INSERT INTO advances (applicant_id, project_id, amount, currency, amount_cny, reason, advance_date, advance_code, status, applicant, attachments) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id', + [applicant_id, project_id, amount, currency || 'CNY', amount_cny || 0, reason, advance_date, advanceCode, status || 'pending', applicant, JSON.stringify(attachments || [])] + ); + + const data = { id: result.rows[0]?.id, applicant_id, project_id, amount, currency, reason, advance_date, advance_code: advanceCode, status, applicant }; + res.json({ success: true, data }); + } catch (error) { + console.error('创建预支申请失败:', error); + res.status(500).json({ success: false, message: '创建预支申请失败' }); + } +}); + +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('SELECT * FROM advances WHERE id = $1', [id]); + + if (result.rows.length > 0) { + const data = result.rows[0]; + // 解析 attachments 字段为数组 + if (data.attachments) { + try { + data.attachments = JSON.parse(data.attachments); + } catch (error) { + data.attachments = []; + } + } else { + data.attachments = []; + } + res.json({ success: true, data }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('获取预支申请失败:', error); + res.status(500).json({ success: false, message: '获取预支申请失败' }); + } +}); + +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { amount, reason, project_id, currency, advance_date, attachments, amount_cny, applicant, status } = req.body; + + const result = await db.query( + 'UPDATE advances SET amount = $1, reason = $2, project_id = $3, currency = $4, advance_date = $5, attachments = $6, amount_cny = $7, applicant = $8, status = $9 WHERE id = $10', + [amount, reason, project_id, currency, advance_date, JSON.stringify(attachments || []), amount_cny, applicant, status, id] + ); + + if (result.rowCount > 0) { + res.json({ success: true, message: '更新成功' }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('更新预支申请失败:', error); + res.status(500).json({ success: false, message: '更新预支申请失败' }); + } +}); + +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('DELETE FROM advances WHERE id = $1', [id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '删除成功' }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('删除预支申请失败:', error); + res.status(500).json({ success: false, message: '删除预支申请失败' }); + } +}); + +router.post('/:id/submit', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '提交成功' }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('提交预支申请失败:', error); + res.status(500).json({ success: false, message: '提交预支申请失败' }); + } +}); + +router.post('/:id/withdraw', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['withdrawn', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '撤回成功' }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('撤回预支申请失败:', error); + res.status(500).json({ success: false, message: '撤回预支申请失败' }); + } +}); + +router.post('/:id/approve', async (req, res) => { + try { + const { id } = req.params; + const { remark } = req.body; + + const result = await db.query('UPDATE advances SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '审批通过成功' }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('审批预支申请失败:', error); + res.status(500).json({ success: false, message: '审批预支申请失败' }); + } +}); + +router.post('/:id/reject', async (req, res) => { + try { + const { id } = req.params; + const { rejectReason } = req.body; + + const result = await db.query('UPDATE advances SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '退回成功' }); + } else { + res.status(404).json({ success: false, message: '预支申请不存在' }); + } + } catch (error) { + console.error('退回预支申请失败:', error); + res.status(500).json({ success: false, message: '退回预支申请失败' }); + } +}); + + module.exports = router; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index cd32a3a..26d0f04 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -16,7 +16,7 @@ router.post('/login', async (req, res) => { } const result = await db.query( - 'SELECT id, username, name, email, phone, role, password_hash FROM users WHERE username = $1', + 'SELECT id, username, name, email, phone, role, avatar, passport, driver_license, password_hash FROM users WHERE username = $1', [username] ); @@ -53,6 +53,9 @@ router.post('/login', async (req, res) => { phone: user.phone, role: user.role, department: '', + avatar: user.avatar || null, + passport: user.passport || null, + driverLicense: user.driver_license || null, token: token } }); diff --git a/backend/routes/cash-management.js b/backend/routes/cash-management.js new file mode 100644 index 0000000..71dcae8 --- /dev/null +++ b/backend/routes/cash-management.js @@ -0,0 +1,280 @@ +const express = require('express'); +const db = require('../db'); +const { authenticate, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); + +function generateCode(prefix) { + const now = new Date(); + const dateStr = now.getFullYear().toString() + + (now.getMonth() + 1).toString().padStart(2, '0') + + now.getDate().toString().padStart(2, '0'); + const rand = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); + return `${prefix}-${dateStr}-${rand}`; +} + +router.get('/summary', authenticate, async (req, res) => { + try { + const { date_from, date_to } = req.query; + let where = "WHERE status != 'voided'"; + const params = []; + let idx = 1; + + if (date_from) { params.push(date_from); where += ` AND record_date >= $${idx++}`; } + if (date_to) { params.push(date_to); where += ` AND record_date <= $${idx++}`; } + + const totalResult = await db.query( + `SELECT + COALESCE(SUM(CASE WHEN txn_type = 'income' THEN amount_cny ELSE 0 END), 0) as total_income, + COALESCE(SUM(CASE WHEN txn_type = 'expense' THEN amount_cny ELSE 0 END), 0) as total_expense, + COALESCE(SUM(CASE WHEN txn_type = 'income' THEN amount_cny ELSE -amount_cny END), 0) as net_amount + FROM financial_records ${where}`, + params + ); + + const byCategory = await db.query( + `SELECT category_level1, category_level2, + COALESCE(SUM(amount_cny), 0) as total_amount, + COUNT(*) as count + FROM financial_records ${where} + GROUP BY category_level1, category_level2 + ORDER BY category_level1, total_amount DESC`, + params + ); + + const recentResult = await db.query( + `SELECT fr.*, p.name as project_name + FROM financial_records fr + LEFT JOIN projects p ON fr.project_id = p.id + ${where} + ORDER BY fr.record_date DESC, fr.created_at DESC + LIMIT 20`, + params + ); + + res.json({ + success: true, + data: { + totals: totalResult.rows[0], + byCategory: byCategory.rows, + recent: recentResult.rows + } + }); + } catch (error) { + console.error('获取资金概览失败:', error); + res.status(500).json({ success: false, message: '获取资金概览失败' }); + } +}); + +router.get('/records', authenticate, async (req, res) => { + try { + const { + txn_type, category_level1, category_level2, + project_id, date_from, date_to, + page = 1, pageSize = 20 + } = req.query; + + let sql = `SELECT fr.*, p.name as project_name FROM financial_records fr LEFT JOIN projects p ON fr.project_id = p.id WHERE 1=1`; + const params = []; + let idx = 1; + + if (txn_type) { params.push(txn_type); sql += ` AND fr.txn_type = $${idx++}`; } + if (category_level1) { params.push(category_level1); sql += ` AND fr.category_level1 = $${idx++}`; } + if (category_level2) { params.push(category_level2); sql += ` AND fr.category_level2 = $${idx++}`; } + if (project_id) { params.push(project_id); sql += ` AND fr.project_id = $${idx++}`; } + if (date_from) { params.push(date_from); sql += ` AND fr.record_date >= $${idx++}`; } + if (date_to) { params.push(date_to); sql += ` AND fr.record_date <= $${idx++}`; } + + const countResult = await db.query(`SELECT COUNT(*) as total FROM (${sql}) sub`, params); + const total = parseInt(countResult.rows[0].total); + + sql += ' ORDER BY fr.record_date DESC, fr.created_at DESC'; + const offset = (parseInt(page) - 1) * parseInt(pageSize); + params.push(parseInt(pageSize)); + sql += ` LIMIT $${idx++}`; + params.push(offset); + sql += ` OFFSET $${idx++}`; + + const result = await db.query(sql, params); + res.json({ + success: true, + data: result.rows, + pagination: { + page: parseInt(page), + pageSize: parseInt(pageSize), + total, + totalPages: Math.ceil(total / parseInt(pageSize)) + } + }); + } catch (error) { + console.error('查询收支记录失败:', error); + res.status(500).json({ success: false, message: '查询收支记录失败' }); + } +}); + +router.post('/', authenticate, async (req, res) => { + const client = await db.pool.connect(); + try { + await client.query('BEGIN'); + const { + txn_type, category_level1, category_level2, + project_id, amount, currency, exchange_rate, + record_date, counterparty_name, counterparty_type, counterparty_id, + description, voucher_url + } = req.body; + + const userId = req.user?.id || req.user?.userId; + + if (!txn_type || !category_level1 || !category_level2 || !amount || !record_date) { + await client.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '缺少必填字段' }); + } + + const amt = parseFloat(amount) || 0; + const rate = parseFloat(exchange_rate) || 1; + const amountCny = parseFloat((amt * rate).toFixed(2)); + + const today = new Date(); + const dateStr = today.toISOString().slice(0, 10).replace(/-/g, ''); + const codeResult = await client.query( + "SELECT COUNT(*) as cnt FROM financial_records WHERE record_code LIKE $1", + [`FIN-${dateStr}%`] + ); + const seq = String(parseInt(codeResult.rows[0].cnt) + 1).padStart(4, '0'); + const recordCode = `FIN-${dateStr}-${seq}`; + + const frResult = await client.query( + `INSERT INTO financial_records + (record_code, txn_type, category_level1, category_level2, project_id, user_id, user_name, + amount_original, currency, exchange_rate, amount_cny, record_date, + counterparty_name, counterparty_type, counterparty_id, source, source_code, description, attachments, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, 'cash_management', $16, $17, $18, 'confirmed') + RETURNING *`, + [ + recordCode, txn_type, category_level1, category_level2, + project_id || null, userId, req.user?.name || req.user?.username || '', + amt, currency || 'CNY', rate, amountCny, + record_date, + counterparty_name || null, counterparty_type || null, counterparty_id || null, + recordCode, description || null, voucher_url || null + ] + ); + + if (txn_type === 'income' && project_id && ['contract_payment', 'customer_advance'].includes(category_level2)) { + const msResult = await client.query( + 'SELECT id FROM project_milestones WHERE project_id = $1 ORDER BY id LIMIT 1', + [project_id] + ); + if (msResult.rows.length > 0) { + const milestoneId = msResult.rows[0].id; + const msData = await client.query('SELECT amount FROM project_milestones WHERE id = $1', [milestoneId]); + if (msData.rows.length > 0 && parseFloat(msData.rows[0].amount) > 0) { + const prResult = await client.query( + `INSERT INTO project_receipts + (project_id, receipt_type, milestone_id, amount, currency, exchange_rate, amount_cny, + receipt_date, payer_name, description, voucher_url, financial_record_id, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING *`, + [ + project_id, + category_level2 === 'customer_advance' ? 'advance' : 'node', + milestoneId, amt, currency || 'CNY', rate, amountCny, + record_date, counterparty_name || '', description || '', voucher_url || '', + frResult.rows[0].id, userId + ] + ); + + const totalReceived = await client.query( + `SELECT COALESCE(SUM(amount), 0) as total FROM project_receipts + WHERE milestone_id = $1 AND receipt_type = 'node'`, + [milestoneId] + ); + const progress = Math.min(100, Math.round((parseFloat(totalReceived.rows[0].total) / parseFloat(msData.rows[0].amount)) * 100)); + await client.query( + `UPDATE project_milestones SET completion_progress = $1::numeric, + status = CASE WHEN $1::numeric >= 100 THEN 'completed' ELSE 'in_progress' END, + actual_date = CASE WHEN $1::numeric >= 100 THEN $2 ELSE actual_date END + WHERE id = $3`, + [progress, record_date, milestoneId] + ); + } + } + } + + await client.query('COMMIT'); + + const fullResult = await db.query( + `SELECT fr.*, p.name as project_name FROM financial_records fr LEFT JOIN projects p ON fr.project_id = p.id WHERE fr.id = $1`, + [frResult.rows[0].id] + ); + + res.json({ success: true, data: fullResult.rows[0] }); + } catch (error) { + await client.query('ROLLBACK'); + console.error('新增收支记录失败:', error); + res.status(500).json({ success: false, message: '新增收支记录失败' }); + } finally { + client.release(); + } +}); + +router.delete('/:id', authenticate, async (req, res) => { + const client = await db.pool.connect(); + try { + await client.query('BEGIN'); + const { id } = req.params; + + const recordResult = await client.query( + `SELECT * FROM financial_records WHERE id = $1 AND source = 'cash_management'`, + [id] + ); + if (recordResult.rowCount === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '记录不存在或无权删除' }); + } + + const record = recordResult.rows[0]; + + if (record.txn_type === 'income' && record.project_id) { + const prResult = await client.query( + 'SELECT id FROM project_receipts WHERE financial_record_id = $1', + [id] + ); + if (prResult.rows.length > 0) { + const receipt = prResult.rows[0]; + await client.query('DELETE FROM project_receipts WHERE id = $1', [receipt.id]); + + if (receipt.milestone_id) { + const msData = await client.query('SELECT amount FROM project_milestones WHERE id = $1', [receipt.milestone_id]); + if (msData.rows.length > 0 && parseFloat(msData.rows[0].amount) > 0) { + const totalReceived = await client.query( + `SELECT COALESCE(SUM(amount), 0) as total FROM project_receipts + WHERE milestone_id = $1 AND receipt_type = 'node'`, + [receipt.milestone_id] + ); + const progress = Math.min(100, Math.round((parseFloat(totalReceived.rows[0].total) / parseFloat(msData.rows[0].amount)) * 100)); + await client.query( + `UPDATE project_milestones SET completion_progress = $1::numeric, + status = CASE WHEN $1::numeric >= 100 THEN 'completed' WHEN $1::numeric > 0 THEN 'in_progress' ELSE 'pending' END + WHERE id = $2`, + [progress, receipt.milestone_id] + ); + } + } + } + } + + await client.query('DELETE FROM financial_records WHERE id = $1', [id]); + + await client.query('COMMIT'); + res.json({ success: true, message: '删除成功' }); + } catch (error) { + await client.query('ROLLBACK'); + console.error('删除收支记录失败:', error); + res.status(500).json({ success: false, message: '删除收支记录失败' }); + } finally { + client.release(); + } +}); + +module.exports = router; diff --git a/backend/routes/construction.js b/backend/routes/construction.js index f302c59..185691f 100644 --- a/backend/routes/construction.js +++ b/backend/routes/construction.js @@ -4,6 +4,7 @@ const { authenticate, requireAdmin } = require('../middleware/auth'); const router = express.Router(); +// 获取施工项目列表 router.get('/my-projects', async (req, res) => { try { const result = await db.query(` @@ -30,4 +31,298 @@ router.get('/my-projects', async (req, res) => { } }); +// ==================== 施工日志 ==================== + +// 获取项目施工日志 +router.get('/projects/:projectId/logs', async (req, res) => { + try { + const { projectId } = req.params; + const result = await db.query( + 'SELECT * FROM construction_logs WHERE project_id = $1 ORDER BY log_date DESC, created_at DESC', + [projectId] + ); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取施工日志失败:', error); + res.status(500).json({ success: false, message: '获取施工日志失败' }); + } +}); + +// 新增施工日志 +router.post('/projects/:projectId/logs', authenticate, async (req, res) => { + try { + const { projectId } = req.params; + const { log_date, weather, work_content, next_plan, issues, photos } = req.body; + const userId = req.user?.id || req.user?.userId; + + const result = await db.query( + `INSERT INTO construction_logs (project_id, log_date, weather, recorded_by, work_content, next_plan, issues, photos) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`, + [projectId, log_date, weather || 'sunny', userId, work_content || '', next_plan || '', issues || '', photos || []] + ); + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + console.error('新增施工日志失败:', error); + res.status(500).json({ success: false, message: '新增施工日志失败' }); + } +}); + +// 删除施工日志 +router.delete('/logs/:logId', authenticate, async (req, res) => { + try { + const { logId } = req.params; + const result = await db.query('DELETE FROM construction_logs WHERE id = $1 RETURNING id', [logId]); + if (result.rowCount === 0) { + return res.status(404).json({ success: false, message: '日志不存在' }); + } + res.json({ success: true, message: '删除成功' }); + } catch (error) { + console.error('删除施工日志失败:', error); + res.status(500).json({ success: false, message: '删除施工日志失败' }); + } +}); + +// ==================== 施工进度 ==================== + +// 获取项目施工进度(包含phases和项目信息) +router.get('/projects/:projectId/progress', async (req, res) => { + try { + const { projectId } = req.params; + + // 获取项目信息 + const projectResult = await db.query( + `SELECT p.*, c.name as customer_name, u.name as manager_name + FROM projects p + LEFT JOIN customers c ON p.customer_id = c.id + LEFT JOIN users u ON p.project_manager_id = u.id + WHERE p.id = $1`, + [projectId] + ); + + if (projectResult.rowCount === 0) { + return res.status(404).json({ success: false, message: '项目不存在' }); + } + + // 获取阶段 + const phasesResult = await db.query( + 'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order', + [projectId] + ); + + res.json({ + success: true, + data: { + project: projectResult.rows[0], + phases: phasesResult.rows + } + }); + } catch (error) { + console.error('获取施工进度失败:', error); + res.status(500).json({ success: false, message: '获取施工进度失败' }); + } +}); + +// 完成阶段 +router.post('/projects/:projectId/phases/:phaseId/complete', authenticate, async (req, res) => { + try { + const { projectId, phaseId } = req.params; + const { remark, photos } = req.body; + const userId = req.user?.id || req.user?.userId; + + // 标记阶段完成 + await db.query( + `UPDATE project_phases SET status = 'completed', completed_at = CURRENT_TIMESTAMP, completed_by = $1, remark = COALESCE(remark, '') || $2, photos = COALESCE(photos, '{}') || $3 WHERE id = $4 AND project_id = $5`, + [userId, remark ? '\n' + remark : '', photos || [], phaseId, projectId] + ); + + // 计算总体进度 + const progressResult = await db.query( + 'SELECT COUNT(*) as total, COUNT(CASE WHEN status = $1 THEN 1 END) as completed FROM project_phases WHERE project_id = $2', + ['completed', projectId] + ); + const total = parseInt(progressResult.rows[0].total); + const completed = parseInt(progressResult.rows[0].completed); + const progress = total > 0 ? Math.round((completed / total) * 100) : 0; + + // 找下一个待开始的阶段 + const nextPhaseResult = await db.query( + `SELECT * FROM project_phases WHERE project_id = $1 AND status = 'pending' ORDER BY phase_order LIMIT 1`, + [projectId] + ); + + if (nextPhaseResult.rows.length > 0) { + await db.query( + `UPDATE project_phases SET status = 'in_progress', started_at = CURRENT_TIMESTAMP WHERE id = $1`, + [nextPhaseResult.rows[0].id] + ); + // 更新项目当前阶段 + await db.query( + `UPDATE projects SET current_phase = $1, phase_progress = $2 WHERE id = $3`, + [nextPhaseResult.rows[0].phase_name, progress, projectId] + ); + } else { + // 所有阶段完成 + await db.query( + `UPDATE projects SET current_phase = '已完成', phase_progress = 100, status = 'completed' WHERE id = $1`, + [projectId] + ); + } + + res.json({ success: true, progress }); + } catch (error) { + console.error('完成阶段失败:', error); + res.status(500).json({ success: false, message: '完成阶段失败' }); + } +}); + +// 重新打开阶段 +router.put('/phases/:phaseId/reopen', authenticate, async (req, res) => { + try { + const { phaseId } = req.params; + + await db.query( + `UPDATE project_phases SET status = 'in_progress', completed_at = NULL, completed_by = NULL WHERE id = $1`, + [phaseId] + ); + + // 重新计算进度 + const phaseResult = await db.query('SELECT project_id FROM project_phases WHERE id = $1', [phaseId]); + if (phaseResult.rows.length > 0) { + const projectId = phaseResult.rows[0].project_id; + const progressResult = await db.query( + 'SELECT COUNT(*) as total, COUNT(CASE WHEN status = $1 THEN 1 END) as completed FROM project_phases WHERE project_id = $2', + ['completed', projectId] + ); + const total = parseInt(progressResult.rows[0].total); + const completed = parseInt(progressResult.rows[0].completed); + const progress = total > 0 ? Math.round((completed / total) * 100) : 0; + + await db.query( + `UPDATE projects SET phase_progress = $1, status = 'active' WHERE id = $2`, + [progress, projectId] + ); + } + + res.json({ success: true }); + } catch (error) { + console.error('重新打开阶段失败:', error); + res.status(500).json({ success: false, message: '重新打开阶段失败' }); + } +}); + +// 更新子项状态(支持完成时间、照片、与资料管理共享) +router.put('/phases/:phaseId/sub-item', authenticate, async (req, res) => { + try { + const { phaseId } = req.params; + const { itemIndex, completed, completed_at, photos } = req.body; + const userId = req.user?.id || req.user?.userId; + + const phaseResult = await db.query('SELECT sub_items, project_id FROM project_phases WHERE id = $1', [phaseId]); + if (phaseResult.rowCount === 0) { + return res.status(404).json({ success: false, message: '阶段不存在' }); + } + + const subItems = phaseResult.rows[0].sub_items || []; + const projectId = phaseResult.rows[0].project_id; + + if (itemIndex >= 0 && itemIndex < subItems.length) { + subItems[itemIndex].completed = completed; + if (completed) { + subItems[itemIndex].completed_at = completed_at || new Date().toISOString(); + if (photos && photos.length > 0) { + subItems[itemIndex].photos = photos; + // 同步写入 project_documents 实现与资料管理共享 + for (const url of photos) { + const fileName = url.split('/').pop() || 'photo.jpg'; + await db.query( + `INSERT INTO project_documents (project_id, doc_type, file_name, file_url, description, uploaded_by) + VALUES ($1, 'image', $2, $3, $4, $5)`, + [projectId, fileName, url, `阶段子项: ${subItems[itemIndex].name}`, userId] + ); + } + } + } else { + subItems[itemIndex].completed_at = null; + subItems[itemIndex].photos = []; + } + await db.query('UPDATE project_phases SET sub_items = $1 WHERE id = $2', [JSON.stringify(subItems), phaseId]); + } + + res.json({ success: true, subItems }); + } catch (error) { + console.error('更新子项状态失败:', error); + res.status(500).json({ success: false, message: '更新子项状态失败' }); + } +}); + +// ==================== 资料管理 ==================== + +// 获取项目资料列表 +router.get('/projects/:projectId/documents', async (req, res) => { + try { + const { projectId } = req.params; + const { doc_type } = req.query; + + let query = 'SELECT d.*, u.name as uploader_name FROM project_documents d LEFT JOIN users u ON d.uploaded_by = u.id WHERE d.project_id = $1'; + const params = [projectId]; + + if (doc_type) { + query += ' AND d.doc_type = $2'; + params.push(doc_type); + } + + query += ' ORDER BY d.created_at DESC'; + + const result = await db.query(query, params); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取项目资料失败:', error); + res.status(500).json({ success: false, message: '获取项目资料失败' }); + } +}); + +// 上传项目资料 +router.post('/projects/:projectId/documents', authenticate, async (req, res) => { + try { + const { projectId } = req.params; + const { doc_type, file_name, file_url, description } = req.body; + const userId = req.user?.id || req.user?.userId; + + const result = await db.query( + `INSERT INTO project_documents (project_id, doc_type, file_name, file_url, description, uploaded_by) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, + [projectId, doc_type || 'document', file_name, file_url, description || '', userId] + ); + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + console.error('上传项目资料失败:', error); + res.status(500).json({ success: false, message: '上传项目资料失败' }); + } +}); + +// 删除项目资料 +router.delete('/documents/:docId', authenticate, async (req, res) => { + try { + const { docId } = req.params; + const userId = req.user?.id || req.user?.userId; + const isAdmin = req.user?.role === 'admin'; + + // 管理员或上传者可删除 + const docResult = await db.query('SELECT * FROM project_documents WHERE id = $1', [docId]); + if (docResult.rowCount === 0) { + return res.status(404).json({ success: false, message: '资料不存在' }); + } + + if (!isAdmin && docResult.rows[0].uploaded_by !== userId) { + return res.status(403).json({ success: false, message: '无权删除' }); + } + + await db.query('DELETE FROM project_documents WHERE id = $1', [docId]); + res.json({ success: true, message: '删除成功' }); + } catch (error) { + console.error('删除项目资料失败:', error); + res.status(500).json({ success: false, message: '删除项目资料失败' }); + } +}); + module.exports = router; diff --git a/backend/routes/executions.js b/backend/routes/executions.js index 37e4a1b..bb04135 100644 --- a/backend/routes/executions.js +++ b/backend/routes/executions.js @@ -85,7 +85,7 @@ router.post('/', async (req, res) => { let status = action === 'execute' ? 'executed' : 'rejected'; if (action === 'reject') { - status = 'pending_edit'; + status = 'pending'; } const executeDate = new Date().toISOString().split('T')[0]; diff --git a/backend/routes/financial-records.js b/backend/routes/financial-records.js index 61a7627..2344d50 100644 --- a/backend/routes/financial-records.js +++ b/backend/routes/financial-records.js @@ -236,8 +236,14 @@ router.post('/batch', authenticate, async (req, res) => { for (let i = 0; i < records.length; i++) { const r = records[i]; try { - if (!r.txn_type || !r.category_level1 || !r.category_level2 || !r.amount_original || !r.record_date) { - errors.push({ row: i + 1, message: '缺少必填字段' }); + const missingFields = []; + if (!r.txn_type) missingFields.push('收支类型'); + if (!r.category_level1) missingFields.push('一级分类'); + if (!r.category_level2) missingFields.push('二级分类'); + if (!r.amount_original) missingFields.push('金额'); + if (!r.record_date) missingFields.push('日期'); + if (missingFields.length > 0) { + errors.push({ row: i + 1, message: `缺少必填字段: ${missingFields.join(', ')}`, data: r }); continue; } @@ -246,15 +252,34 @@ router.post('/batch', authenticate, async (req, res) => { let projectId = r.project_id || null; if (!projectId && r.project_name) { + // 先精确匹配 const projResult = await db.query("SELECT id FROM projects WHERE name = $1", [r.project_name]); if (projResult.rows.length > 0) { projectId = projResult.rows[0].id; + } else { + // 模糊匹配:去除空格后比较 + const fuzzyResult = await db.query( + "SELECT id, name FROM projects WHERE REPLACE(name, ' ', '') = REPLACE($1, ' ', '')", + [r.project_name] + ); + if (fuzzyResult.rows.length > 0) { + projectId = fuzzyResult.rows[0].id; + } else { + // 包含匹配 + const likeResult = await db.query( + "SELECT id, name FROM projects WHERE name LIKE '%' || $1 || '%' OR $1 LIKE '%' || name || '%'", + [r.project_name] + ); + if (likeResult.rows.length > 0) { + projectId = likeResult.rows[0].id; + } + } } } let userId = r.user_id || null; if (!userId && r.user_name) { - const userResult = await db.query("SELECT id FROM users WHERE name = $1", [r.user_name]); + const userResult = await db.query("SELECT id FROM users WHERE name = $1 OR username = $1", [r.user_name]); if (userResult.rows.length > 0) { userId = userResult.rows[0].id; } @@ -280,7 +305,7 @@ router.post('/batch', authenticate, async (req, res) => { ); results.push(result.rows[0]); } catch (err) { - errors.push({ row: i + 1, message: err.message }); + errors.push({ row: i + 1, message: err.message, data: { txn_type: r.txn_type, category_level1: r.category_level1, category_level2: r.category_level2, project_name: r.project_name, amount: r.amount_original } }); } } diff --git a/backend/routes/inventory.js b/backend/routes/inventory.js index 5beecc7..8395b23 100644 --- a/backend/routes/inventory.js +++ b/backend/routes/inventory.js @@ -81,6 +81,32 @@ router.get('/summary', async (req, res) => { } }); +router.post('/in', async (req, res) => { + try { + const { project_id, product_id, quantity, unit_price, total_amount, operator, remark } = req.body; + + const result = await db.query(` + INSERT INTO inventory_records + (record_type, project_id, product_id, quantity, unit_price, total_amount, record_date, operator, remark) + VALUES ($1, $2, $3, $4, $5, $6, CURRENT_DATE, $7, $8) + RETURNING id + `, ['in', project_id || null, product_id, quantity, unit_price || null, total_amount || null, operator || '系统', remark || null]); + + res.json({ + success: true, + message: '入库成功', + data: { id: result.rows[0]?.id } + }); + } catch (error) { + console.error('入库失败:', error); + res.status(500).json({ + success: false, + message: '入库失败', + error: process.env.NODE_ENV === 'development' ? error.message : '操作失败' + }); + } +}); + router.post('/out', async (req, res) => { try { const { project_id, product_id, quantity, unit_price, total_amount, operator, remark } = req.body; diff --git a/backend/routes/payments.js b/backend/routes/payments.js index d0da26d..13e9d73 100644 --- a/backend/routes/payments.js +++ b/backend/routes/payments.js @@ -4,256 +4,256 @@ const { authenticate, requireAdmin } = require('../middleware/auth'); const router = express.Router(); -router.get('/', async (req, res) => { - try { - const result = await db.query(` - SELECT * FROM payment_requests - ORDER BY created_at DESC - `); - - const data = result.rows.map(item => { - if (item.attachments) { - try { - item.attachments = JSON.parse(item.attachments); - } catch (error) { - item.attachments = []; - } - } else { - item.attachments = []; - } - if (item.detail_items) { - try { - item.detail_items = JSON.parse(item.detail_items); - } catch (error) { - item.detail_items = []; - } - } else { - item.detail_items = []; - } - return item; - }); - - res.json({ success: true, data, count: data.length }); - } catch (error) { - console.error('获取付款申请失败:', error); - res.status(500).json({ success: false, message: '获取付款申请失败' }); - } -}); +router.get('/', async (req, res) => { + try { + const result = await db.query(` + SELECT * FROM payment_requests + ORDER BY created_at DESC + `); + + const data = result.rows.map(item => { + if (item.attachments) { + try { + item.attachments = JSON.parse(item.attachments); + } catch (error) { + item.attachments = []; + } + } else { + item.attachments = []; + } + if (item.detail_items) { + try { + item.detail_items = JSON.parse(item.detail_items); + } catch (error) { + item.detail_items = []; + } + } else { + item.detail_items = []; + } + return item; + }); + + res.json({ success: true, data, count: data.length }); + } catch (error) { + console.error('获取付款申请失败:', error); + res.status(500).json({ success: false, message: '获取付款申请失败' }); + } +}); -router.post('/', async (req, res) => { - try { - const { - payment_date, payee, bank_account, bank_name, currency, reason, - detail_items, attachments, applicant, - payee_type, payee_id, expense_type, expense_category, project_id, amount - } = req.body; - - // 生成付款申请编号 - const requestCode = `PAY-${Date.now()}`; - - // 使用默认值处理可选字段 - const finalBankAccount = bank_account || ''; - const finalBankName = bank_name || ''; - const finalAmount = amount || 0; - - const result = await db.query( - `INSERT INTO payment_requests ( - payee, bank_account, bank_name, amount, currency, reason, payment_date, - request_code, status, applicant, detail_items, attachments, - payee_type, payee_id, expense_type, expense_category, project_id - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17`, - [ - payee, finalBankAccount, finalBankName, finalAmount, currency || 'CNY', - reason, payment_date, requestCode, 'pending', applicant, - JSON.stringify(detail_items || []), JSON.stringify(attachments || []), - payee_type || 'other', payee_id || null, expense_type || 'company', - expense_category || '', project_id || null - ] - ); - - // SQLite不支持RETURNING,所以需要查询刚插入的数据 - const lastInsert = await db.query('SELECT * FROM payment_requests ORDER BY id DESC LIMIT 1'); - res.json({ success: true, data: lastInsert.rows[0] }); - } catch (error) { - console.error('创建付款申请失败:', error); - res.status(500).json({ success: false, message: '创建付款申请失败' }); - } -}); +router.post('/', async (req, res) => { + try { + const { + payment_date, payee, bank_account, bank_name, currency, reason, + detail_items, attachments, applicant, + payee_type, payee_id, expense_type, expense_category, project_id, amount + } = req.body; + + // 生成付款申请编号 + const requestCode = `PAY-${Date.now()}`; + + // 使用默认值处理可选字段 + const finalBankAccount = bank_account || ''; + const finalBankName = bank_name || ''; + const finalAmount = amount || 0; + + const result = await db.query( + `INSERT INTO payment_requests ( + payee, bank_account, bank_name, amount, currency, reason, payment_date, + request_code, status, applicant, detail_items, attachments, + payee_type, payee_id, expense_type, expense_category, project_id + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`, + [ + payee, finalBankAccount, finalBankName, finalAmount, currency || 'CNY', + reason, payment_date, requestCode, 'pending', applicant, + JSON.stringify(detail_items || []), JSON.stringify(attachments || []), + payee_type || 'other', payee_id || null, expense_type || 'company', + expense_category || '', project_id || null + ] + ); + + // SQLite不支持RETURNING,所以需要查询刚插入的数据 + const lastInsert = await db.query('SELECT * FROM payment_requests ORDER BY id DESC LIMIT 1'); + res.json({ success: true, data: lastInsert.rows[0] }); + } catch (error) { + console.error('创建付款申请失败:', error); + res.status(500).json({ success: false, message: '创建付款申请失败' }); + } +}); -router.get('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('SELECT * FROM payment_requests WHERE id = $1', [id]); - - if (result.rows.length > 0) { - const data = result.rows[0]; - if (data.attachments) { - try { - data.attachments = JSON.parse(data.attachments); - } catch (error) { - data.attachments = []; - } - } else { - data.attachments = []; - } - if (data.detail_items) { - try { - data.detail_items = JSON.parse(data.detail_items); - } catch (error) { - data.detail_items = []; - } - } else { - data.detail_items = []; - } - res.json({ success: true, data }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('获取付款申请失败:', error); - res.status(500).json({ success: false, message: '获取付款申请失败' }); - } -}); +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('SELECT * FROM payment_requests WHERE id = $1', [id]); + + if (result.rows.length > 0) { + const data = result.rows[0]; + if (data.attachments) { + try { + data.attachments = JSON.parse(data.attachments); + } catch (error) { + data.attachments = []; + } + } else { + data.attachments = []; + } + if (data.detail_items) { + try { + data.detail_items = JSON.parse(data.detail_items); + } catch (error) { + data.detail_items = []; + } + } else { + data.detail_items = []; + } + res.json({ success: true, data }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('获取付款申请失败:', error); + res.status(500).json({ success: false, message: '获取付款申请失败' }); + } +}); -router.put('/:id', async (req, res) => { - try { - const { id } = req.params; - const { - payment_date, payee, bank_account, bank_name, currency, reason, - detail_items, attachments, applicant, status, - payee_type, payee_id, expense_type, expense_category, project_id, amount - } = req.body; - - // 构建动态更新SQL,只更新提供的字段 - const updates = []; - const params = []; - - if (payment_date !== undefined) { updates.push('payment_date = $1'); params.push(payment_date); } - if (payee !== undefined) { updates.push('payee = $1'); params.push(payee); } - if (bank_account !== undefined) { updates.push('bank_account = $1'); params.push(bank_account); } - if (bank_name !== undefined) { updates.push('bank_name = $1'); params.push(bank_name); } - if (amount !== undefined) { updates.push('amount = $1'); params.push(amount); } - if (currency !== undefined) { updates.push('currency = $1'); params.push(currency); } - if (reason !== undefined) { updates.push('reason = $1'); params.push(reason); } - if (detail_items !== undefined) { updates.push('detail_items = $1'); params.push(JSON.stringify(detail_items || [])); } - if (attachments !== undefined) { updates.push('attachments = $1'); params.push(JSON.stringify(attachments || [])); } - if (applicant !== undefined) { updates.push('applicant = $1'); params.push(applicant); } - if (status !== undefined) { updates.push('status = $1'); params.push(status); } - if (payee_type !== undefined) { updates.push('payee_type = $1'); params.push(payee_type); } - if (payee_id !== undefined) { updates.push('payee_id = $1'); params.push(payee_id); } - if (expense_type !== undefined) { updates.push('expense_type = $1'); params.push(expense_type); } - if (expense_category !== undefined) { updates.push('expense_category = $1'); params.push(expense_category); } - if (project_id !== undefined) { updates.push('project_id = $1'); params.push(project_id); } - - if (updates.length === 0) { - return res.status(400).json({ success: false, message: '没有要更新的字段' }); - } - - params.push(id); - - const result = await db.query( - `UPDATE payment_requests SET ${updates.join(', ')} WHERE id = $1`, - params - ); - - if (result.changes > 0) { - res.json({ success: true, message: '更新成功' }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('更新付款申请失败:', error); - res.status(500).json({ success: false, message: '更新付款申请失败' }); - } -}); +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { + payment_date, payee, bank_account, bank_name, currency, reason, + detail_items, attachments, applicant, status, + payee_type, payee_id, expense_type, expense_category, project_id, amount + } = req.body; + + // 构建动态更新SQL,只更新提供的字段 + const updates = []; + const params = []; + + if (payment_date !== undefined) { updates.push('payment_date = $1'); params.push(payment_date); } + if (payee !== undefined) { updates.push('payee = $1'); params.push(payee); } + if (bank_account !== undefined) { updates.push('bank_account = $1'); params.push(bank_account); } + if (bank_name !== undefined) { updates.push('bank_name = $1'); params.push(bank_name); } + if (amount !== undefined) { updates.push('amount = $1'); params.push(amount); } + if (currency !== undefined) { updates.push('currency = $1'); params.push(currency); } + if (reason !== undefined) { updates.push('reason = $1'); params.push(reason); } + if (detail_items !== undefined) { updates.push('detail_items = $1'); params.push(JSON.stringify(detail_items || [])); } + if (attachments !== undefined) { updates.push('attachments = $1'); params.push(JSON.stringify(attachments || [])); } + if (applicant !== undefined) { updates.push('applicant = $1'); params.push(applicant); } + if (status !== undefined) { updates.push('status = $1'); params.push(status); } + if (payee_type !== undefined) { updates.push('payee_type = $1'); params.push(payee_type); } + if (payee_id !== undefined) { updates.push('payee_id = $1'); params.push(payee_id); } + if (expense_type !== undefined) { updates.push('expense_type = $1'); params.push(expense_type); } + if (expense_category !== undefined) { updates.push('expense_category = $1'); params.push(expense_category); } + if (project_id !== undefined) { updates.push('project_id = $1'); params.push(project_id); } + + if (updates.length === 0) { + return res.status(400).json({ success: false, message: '没有要更新的字段' }); + } + + params.push(id); + + const result = await db.query( + `UPDATE payment_requests SET ${updates.join(', ')} WHERE id = $1`, + params + ); + + if (result.rowCount > 0) { + res.json({ success: true, message: '更新成功' }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('更新付款申请失败:', error); + res.status(500).json({ success: false, message: '更新付款申请失败' }); + } +}); -router.delete('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('DELETE FROM payment_requests WHERE id = $1', [id]); - - if (result.changes > 0) { - res.json({ success: true, message: '删除成功' }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('删除付款申请失败:', error); - res.status(500).json({ success: false, message: '删除付款申请失败' }); - } -}); +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('DELETE FROM payment_requests WHERE id = $1', [id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '删除成功' }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('删除付款申请失败:', error); + res.status(500).json({ success: false, message: '删除付款申请失败' }); + } +}); -router.post('/:id/submit', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['pending', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '提交成功' }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('提交付款申请失败:', error); - res.status(500).json({ success: false, message: '提交付款申请失败' }); - } -}); +router.post('/:id/submit', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '提交成功' }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('提交付款申请失败:', error); + res.status(500).json({ success: false, message: '提交付款申请失败' }); + } +}); -router.post('/:id/withdraw', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['withdrawn', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '撤回成功' }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('撤回报销申请失败:', error); - res.status(500).json({ success: false, message: '撤回报销申请失败' }); - } -}); +router.post('/:id/withdraw', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['withdrawn', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '撤回成功' }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('撤回报销申请失败:', error); + res.status(500).json({ success: false, message: '撤回报销申请失败' }); + } +}); -router.post('/:id/approve', async (req, res) => { - try { - const { id } = req.params; - const { remark } = req.body; - - const result = await db.query('UPDATE payment_requests SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); - - if (result.changes > 0) { - res.json({ success: true, message: '审批通过成功' }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('审批付款申请失败:', error); - res.status(500).json({ success: false, message: '审批付款申请失败' }); - } -}); +router.post('/:id/approve', async (req, res) => { + try { + const { id } = req.params; + const { remark } = req.body; + + const result = await db.query('UPDATE payment_requests SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '审批通过成功' }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('审批付款申请失败:', error); + res.status(500).json({ success: false, message: '审批付款申请失败' }); + } +}); -router.post('/:id/reject', async (req, res) => { - try { - const { id } = req.params; - const { rejectReason } = req.body; - - const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['pending_edit', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '退回成功' }); - } else { - res.status(404).json({ success: false, message: '付款申请不存在' }); - } - } catch (error) { - console.error('退回报销申请失败:', error); - res.status(500).json({ success: false, message: '退回报销申请失败' }); - } -}); +router.post('/:id/reject', async (req, res) => { + try { + const { id } = req.params; + const { rejectReason } = req.body; + + const result = await db.query('UPDATE payment_requests SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '退回成功' }); + } else { + res.status(404).json({ success: false, message: '付款申请不存在' }); + } + } catch (error) { + console.error('退回报销申请失败:', error); + res.status(500).json({ success: false, message: '退回报销申请失败' }); + } +}); module.exports = router; \ No newline at end of file diff --git a/backend/routes/process-templates.js b/backend/routes/process-templates.js index 31ccf15..387d0ab 100644 --- a/backend/routes/process-templates.js +++ b/backend/routes/process-templates.js @@ -106,7 +106,7 @@ router.post('/:id/copy', async (req, res) => { const src = existing.rows[0]; const result = await db.query( 'INSERT INTO project_type_templates (name, description, phases, is_system) VALUES ($1, $2, $3, false) RETURNING id', - [name || `${src.name} (副本)`, src.description, src.phases] + [name || `${src.name} (副本)`, src.description, JSON.stringify(src.phases)] ); res.json({ success: true, message: '模板复制成功', data: { id: result.rows[0].id } }); } catch (error) { diff --git a/backend/routes/project-receipts.js b/backend/routes/project-receipts.js new file mode 100644 index 0000000..eb49549 --- /dev/null +++ b/backend/routes/project-receipts.js @@ -0,0 +1,203 @@ +const express = require('express'); +const db = require('../db'); +const { authenticate, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); + +// 获取项目收款记录 +router.get('/', async (req, res) => { + try { + const { projectId } = req.query; + if (!projectId) { + return res.status(400).json({ success: false, message: '缺少项目ID' }); + } + const result = await db.query( + `SELECT r.*, m.milestone_name, m.percentage as milestone_percentage, + u.name as creator_name + FROM project_receipts r + LEFT JOIN project_milestones m ON r.milestone_id = m.id + LEFT JOIN users u ON r.created_by = u.id + WHERE r.project_id = $1 + ORDER BY r.receipt_date DESC, r.created_at DESC`, + [projectId] + ); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取收款记录失败:', error); + res.status(500).json({ success: false, message: '获取收款记录失败' }); + } +}); + +// 获取项目付款节点列表(用于收款时选择) +router.get('/milestones', async (req, res) => { + try { + const { projectId } = req.query; + if (!projectId) { + return res.status(400).json({ success: false, message: '缺少项目ID' }); + } + const result = await db.query( + `SELECT id, milestone_name, percentage, amount, status, completion_progress + FROM project_milestones WHERE project_id = $1 ORDER BY id`, + [projectId] + ); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取付款节点失败:', error); + res.status(500).json({ success: false, message: '获取付款节点失败' }); + } +}); + +// 新增收款记录(同时写入 financial_records 统一记账) +router.post('/', authenticate, async (req, res) => { + const client = await db.pool.connect(); + try { + await client.query('BEGIN'); + const { project_id, receipt_type, milestone_id, amount, currency, exchange_rate, receipt_date, payer_name, description, voucher_url, counterparty_id } = req.body; + const userId = req.user?.id || req.user?.userId; + + if (!project_id) { + await client.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '缺少项目ID' }); + } + + const amt = parseFloat(amount) || 0; + const rate = parseFloat(exchange_rate) || 1; + const amountCny = parseFloat((amt * rate).toFixed(2)); + + // 1. 写入 financial_records(统一记账) + const today = new Date(); + const dateStr = today.toISOString().slice(0, 10).replace(/-/g, ''); + const codeResult = await client.query( + "SELECT COUNT(*) as cnt FROM financial_records WHERE record_code LIKE $1", + [`FIN-${dateStr}%`] + ); + const seq = String(parseInt(codeResult.rows[0].cnt) + 1).padStart(4, '0'); + const recordCode = `FIN-${dateStr}-${seq}`; + + // 确定分类 + let categoryLevel2 = 'contract_payment'; + if (receipt_type === 'advance') { + categoryLevel2 = 'customer_advance'; + } else if (receipt_type === 'other') { + categoryLevel2 = 'other_income'; + } + + const frResult = await client.query( + `INSERT INTO financial_records + (record_code, txn_type, category_level1, category_level2, project_id, user_id, user_name, + amount_original, currency, exchange_rate, amount_cny, record_date, + counterparty_name, counterparty_type, counterparty_id, source, source_code, description, status) + VALUES ($1, 'income', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'customer', $13, 'receipt', $14, $15, 'confirmed') + RETURNING id`, + [ + recordCode, 'income', categoryLevel2, project_id, + userId, req.user?.name || req.user?.username || '', + amt, currency || 'CNY', rate, amountCny, + receipt_date, payer_name || '', + counterparty_id || null, + recordCode, description || '' + ] + ); + const financialRecordId = frResult.rows[0].id; + + // 2. 写入 project_receipts + const receiptResult = await client.query( + `INSERT INTO project_receipts + (project_id, receipt_type, milestone_id, amount, currency, exchange_rate, amount_cny, + receipt_date, payer_name, description, voucher_url, financial_record_id, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING *`, + [project_id, receipt_type || 'node', milestone_id || null, amt, currency || 'CNY', rate, amountCny, + receipt_date, payer_name || '', description || '', voucher_url || '', financialRecordId, userId] + ); + + // 3. 如果是节点收款,更新里程碑完成进度 + if (receipt_type === 'node' && milestone_id) { + const msResult = await client.query('SELECT amount FROM project_milestones WHERE id = $1', [milestone_id]); + if (msResult.rows.length > 0 && parseFloat(msResult.rows[0].amount) > 0) { + const totalReceived = await client.query( + `SELECT COALESCE(SUM(amount), 0) as total FROM project_receipts + WHERE milestone_id = $1 AND receipt_type = 'node'`, + [milestone_id] + ); + const progress = Math.min(100, Math.round((parseFloat(totalReceived.rows[0].total) / parseFloat(msResult.rows[0].amount)) * 100)); + await client.query( + `UPDATE project_milestones SET completion_progress = $1::numeric, status = CASE WHEN $1::numeric >= 100 THEN 'completed' ELSE 'in_progress' END, actual_date = CASE WHEN $1::numeric >= 100 THEN $2 ELSE actual_date END WHERE id = $3`, + [progress, receipt_date, milestone_id] + ); + } + } + + await client.query('COMMIT'); + + // 返回带里程碑名称的完整记录 + const fullResult = await db.query( + `SELECT r.*, m.milestone_name FROM project_receipts r LEFT JOIN project_milestones m ON r.milestone_id = m.id WHERE r.id = $1`, + [receiptResult.rows[0].id] + ); + + res.json({ success: true, data: fullResult.rows[0] }); + } catch (error) { + await client.query('ROLLBACK'); + console.error('新增收款记录失败:', error); + res.status(500).json({ success: false, message: '新增收款记录失败' }); + } finally { + client.release(); + } +}); + +// 删除收款记录(同时删除 financial_records) +router.delete('/:receiptId', authenticate, async (req, res) => { + const client = await db.pool.connect(); + try { + await client.query('BEGIN'); + const { receiptId } = req.params; + + const receiptResult = await client.query( + 'SELECT * FROM project_receipts WHERE id = $1', + [receiptId] + ); + if (receiptResult.rowCount === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '收款记录不存在' }); + } + + const receipt = receiptResult.rows[0]; + const financialRecordId = receipt.financial_record_id; + + // 先删除 project_receipts(引用方),再删除 financial_records(被引用方) + await client.query('DELETE FROM project_receipts WHERE id = $1', [receiptId]); + + if (financialRecordId) { + await client.query('DELETE FROM financial_records WHERE id = $1', [financialRecordId]); + } + + // 如果是节点收款,重新计算里程碑进度 + if (receipt.receipt_type === 'node' && receipt.milestone_id) { + const msResult = await client.query('SELECT amount FROM project_milestones WHERE id = $1', [receipt.milestone_id]); + if (msResult.rows.length > 0 && parseFloat(msResult.rows[0].amount) > 0) { + const totalReceived = await client.query( + `SELECT COALESCE(SUM(amount), 0) as total FROM project_receipts + WHERE milestone_id = $1 AND receipt_type = 'node'`, + [receipt.milestone_id] + ); + const progress = Math.min(100, Math.round((parseFloat(totalReceived.rows[0].total) / parseFloat(msResult.rows[0].amount)) * 100)); + await client.query( + `UPDATE project_milestones SET completion_progress = $1::numeric, status = CASE WHEN $1::numeric >= 100 THEN 'completed' WHEN $1::numeric > 0 THEN 'in_progress' ELSE 'pending' END WHERE id = $2`, + [progress, receipt.milestone_id] + ); + } + } + + await client.query('COMMIT'); + res.json({ success: true, message: '删除成功' }); + } catch (error) { + await client.query('ROLLBACK'); + console.error('删除收款记录失败:', error); + res.status(500).json({ success: false, message: '删除收款记录失败' }); + } finally { + client.release(); + } +}); + +module.exports = router; diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 6247851..f002c63 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -19,10 +19,17 @@ router.get('/', async (req, res) => { p.status, p.location, c.name as customer_name, - u.name as manager_name + u.name as manager_name, + COALESCE(expense_summary.total_expense, 0) as total_expense FROM projects p LEFT JOIN customers c ON p.customer_id = c.id LEFT JOIN users u ON p.project_manager_id = u.id + LEFT JOIN ( + SELECT project_id, SUM(amount_cny::numeric) as total_expense + FROM financial_records + WHERE txn_type = 'expense' AND status = 'confirmed' + GROUP BY project_id + ) expense_summary ON p.id = expense_summary.project_id ORDER BY p.created_at DESC LIMIT 50 `); @@ -472,6 +479,14 @@ router.put('/:id', async (req, res) => { ); } + // 合同金额变更时,自动按比例更新付款节点金额 + if (contract_amount !== undefined && contract_amount !== null) { + await db.query( + `UPDATE project_milestones SET amount = ROUND($1 * percentage / 100, 2) WHERE project_id = $2`, + [contract_amount, id] + ); + } + if (start_date && end_date) { const start = new Date(start_date); const end = new Date(end_date); @@ -553,6 +568,12 @@ router.put('/:id/contract', async (req, res) => { [id, node.name, node.condition || '', node.percentage, node.amount, 'pending'] ); } + } else if (contract_total) { + // 没有传付款节点但合同金额变了,按比例更新现有里程碑金额 + await db.query( + `UPDATE project_milestones SET amount = ROUND($1 * percentage / 100, 2) WHERE project_id = $2`, + [contract_total, id] + ); } // 4. 处理单价项 @@ -581,55 +602,108 @@ router.put('/:id/contract', async (req, res) => { } }); +router.get('/:id/financial-details', async (req, res) => { + try { + const { id } = req.params; + const { txn_type, category_level1, category_level2, date_from, date_to, page = 1, pageSize = 20 } = req.query; + + let sql = `SELECT fr.*, p.name as project_name FROM financial_records fr LEFT JOIN projects p ON fr.project_id = p.id WHERE fr.project_id = $1 AND fr.status != 'voided'`; + const params = [id]; + let idx = 2; + + if (txn_type) { params.push(txn_type); sql += ` AND fr.txn_type = $${idx++}`; } + if (category_level1) { params.push(category_level1); sql += ` AND fr.category_level1 = $${idx++}`; } + if (category_level2) { params.push(category_level2); sql += ` AND fr.category_level2 = $${idx++}`; } + if (date_from) { params.push(date_from); sql += ` AND fr.record_date >= $${idx++}`; } + if (date_to) { params.push(date_to); sql += ` AND fr.record_date <= $${idx++}`; } + + const countResult = await db.query(`SELECT COUNT(*) as total FROM (${sql}) sub`, params); + const total = parseInt(countResult.rows[0].total); + + sql += ' ORDER BY fr.record_date DESC, fr.created_at DESC'; + const offset = (parseInt(page) - 1) * parseInt(pageSize); + params.push(parseInt(pageSize)); + sql += ` LIMIT $${idx++}`; + params.push(offset); + sql += ` OFFSET $${idx++}`; + + const result = await db.query(sql, params); + res.json({ + success: true, + data: result.rows, + pagination: { page: parseInt(page), pageSize: parseInt(pageSize), total, totalPages: Math.ceil(total / parseInt(pageSize)) } + }); + } catch (error) { + console.error('获取项目财务明细失败:', error); + res.status(500).json({ success: false, message: '获取项目财务明细失败' }); + } +}); + router.get('/:id/cost-summary', async (req, res) => { try { const { id } = req.params; - - const purchaseResult = await db.query(` - SELECT - expense_category, - SUM(total_amount) as total_amount - FROM purchase_requests - WHERE project_id = $1 AND status IN ('approved', 'executed') - GROUP BY expense_category - `, [id]); - - const paymentResult = await db.query(` - SELECT - SUM(amount) as total_payment - FROM payment_requests - WHERE project_id = $1 AND status = 'approved' AND payment_type = 'company' - `, [id]); - + const projectResult = await db.query('SELECT * FROM projects WHERE id = $1', [id]); - if (projectResult.rows.length === 0) { return res.status(404).json({ success: false, message: '项目不存在' }); } - const project = projectResult.rows[0]; - const purchaseByCategory = {}; - let totalPurchase = 0; - - purchaseResult.rows.forEach(row => { - purchaseByCategory[row.expense_category] = row.total_amount; - totalPurchase += row.total_amount; + + const incomeResult = await db.query(` + SELECT category_level2, COALESCE(SUM(amount_cny), 0) as total_amount + FROM financial_records + WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided' + GROUP BY category_level2 + `, [id]); + + const expenseResult = await db.query(` + SELECT category_level1, category_level2, COALESCE(SUM(amount_cny), 0) as total_amount + FROM financial_records + WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided' + GROUP BY category_level1, category_level2 + `, [id]); + + const totalIncomeRow = await db.query(` + SELECT COALESCE(SUM(amount_cny), 0) as total FROM financial_records + WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided' + `, [id]); + + const totalExpenseRow = await db.query(` + SELECT COALESCE(SUM(amount_cny), 0) as total FROM financial_records + WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided' + `, [id]); + + const incomeByCategory = {}; + let totalIncome = parseFloat(totalIncomeRow.rows[0].total); + incomeResult.rows.forEach(row => { + incomeByCategory[row.category_level2] = parseFloat(row.total_amount); }); - - const totalPayment = paymentResult.rows[0]?.total_payment || 0; - + + const expenseByCategory = {}; + const expenseByLevel1 = {}; + let totalExpense = parseFloat(totalExpenseRow.rows[0].total); + expenseResult.rows.forEach(row => { + expenseByCategory[row.category_level2] = parseFloat(row.total_amount); + if (!expenseByLevel1[row.category_level1]) expenseByLevel1[row.category_level1] = 0; + expenseByLevel1[row.category_level1] += parseFloat(row.total_amount); + }); + res.json({ success: true, data: { project_name: project.name, contract_amount: project.contract_amount || 0, - purchase_cost: { - total: totalPurchase, - by_category: purchaseByCategory + income: { + total: totalIncome, + by_category: incomeByCategory }, - payment_cost: totalPayment, - total_cost: totalPurchase + totalPayment, - profit: (project.contract_amount || 0) - (totalPurchase + totalPayment) + expense: { + total: totalExpense, + by_category: expenseByCategory, + by_level1: expenseByLevel1 + }, + total_cost: totalExpense, + profit: totalIncome - totalExpense } }); } catch (error) { diff --git a/backend/routes/purchase-orders.js b/backend/routes/purchase-orders.js index 404d4d7..043409b 100644 --- a/backend/routes/purchase-orders.js +++ b/backend/routes/purchase-orders.js @@ -203,7 +203,7 @@ router.put('/:id', async (req, res) => { WHERE id = ? `, [supplier_id, supplier_country, contract_url, quotation_url, remark, id]); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '采购订单不存在' }); } @@ -303,7 +303,7 @@ router.post('/:id/cancel', async (req, res) => { [id] ); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '采购订单不存在' }); } @@ -391,7 +391,7 @@ router.put('/:id/items/:itemId', async (req, res) => { WHERE id = ? AND purchase_order_id = ? `, [product_id, product_name, specification, unit, quantity, unit_price, total_price, itemId, id]); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '商品明细不存在' }); } @@ -418,7 +418,7 @@ router.delete('/:id/items/:itemId', async (req, res) => { [itemId, id] ); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '商品明细不存在' }); } @@ -506,7 +506,7 @@ router.put('/:id/payment-plans/:planId', async (req, res) => { WHERE id = ? AND purchase_order_id = ? `, [stage, planned_date, planned_amount, planned_percentage, remark, planId, id]); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '付款计划不存在' }); } @@ -533,7 +533,7 @@ router.delete('/:id/payment-plans/:planId', async (req, res) => { [planId, id] ); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '付款计划不存在' }); } diff --git a/backend/routes/purchase.js b/backend/routes/purchase.js index 8c9b379..f0761e5 100644 --- a/backend/routes/purchase.js +++ b/backend/routes/purchase.js @@ -128,7 +128,7 @@ router.post('/', async (req, res) => { VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING id`, [requestCode, project_id, purchase_type || 'inventory', total_amount || 0, currency || 'CNY', - 'pending_edit', remark]); + 'pending', remark]); res.json({ success: true, @@ -219,7 +219,7 @@ router.post('/:id/submit', async (req, res) => { const { id } = req.params; const result = await db.query( - 'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2', + 'UPDATE purchase_requests SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', ['pending', id] ); @@ -256,7 +256,7 @@ router.post('/:id/approve', async (req, res) => { try { await db.query( - 'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2', + 'UPDATE purchase_requests SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', ['approved', id] ); @@ -297,8 +297,8 @@ router.post('/:id/reject', async (req, res) => { const { id } = req.params; const result = await db.query( - 'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2', - ['pending_edit', id] + 'UPDATE purchase_requests SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', + ['pending', id] ); if (result.rowCount === 0) { @@ -321,7 +321,7 @@ router.post('/:id/withdraw', async (req, res) => { const { id } = req.params; const result = await db.query( - 'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2', + 'UPDATE purchase_requests SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', ['withdrawn', id] ); @@ -345,7 +345,7 @@ router.post('/:id/execute', async (req, res) => { const { id } = req.params; await db.query( - 'UPDATE purchase_requests SET status = $1, updated_at = datetime(\'now\') WHERE id = $2', + 'UPDATE purchase_requests SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', ['executed', id] ); diff --git a/backend/routes/reimbursements.js b/backend/routes/reimbursements.js index d311e05..e3adadf 100644 --- a/backend/routes/reimbursements.js +++ b/backend/routes/reimbursements.js @@ -17,223 +17,223 @@ const validate = (req, res, next) => { next(); }; -router.get('/', async (req, res) => { - try { - const result = await db.query(` - SELECT r.*, u.name as user_name, p.name as project_name - FROM reimbursements r - LEFT JOIN users u ON r.applicant_id = u.id - LEFT JOIN projects p ON r.project_id = p.id - ORDER BY r.created_at DESC - `); - - // 解析每个报销申请的 attachments 和 detail_items 字段为数组 - const data = result.rows.map(item => { - // 解析 attachments 字段 - if (item.attachments) { - try { - item.attachments = JSON.parse(item.attachments); - } catch (error) { - item.attachments = []; - } - } else { - item.attachments = []; - } - // 解析 detail_items 字段 - if (item.detail_items) { - try { - item.detail_items = JSON.parse(item.detail_items); - } catch (error) { - item.detail_items = []; - } - } else { - item.detail_items = []; - } - return item; - }); - - res.json({ success: true, data, count: data.length }); - } catch (error) { - console.error('获取报销记录失败:', error); - res.status(500).json({ - success: false, - message: '获取报销记录失败', - error: process.env.NODE_ENV === 'development' ? error.message : '操作失败' - }); - } -}); +router.get('/', async (req, res) => { + try { + const result = await db.query(` + SELECT r.*, u.name as user_name, p.name as project_name + FROM reimbursements r + LEFT JOIN users u ON r.applicant_id = u.id + LEFT JOIN projects p ON r.project_id = p.id + ORDER BY r.created_at DESC + `); + + // 解析每个报销申请的 attachments 和 detail_items 字段为数组 + const data = result.rows.map(item => { + // 解析 attachments 字段 + if (item.attachments) { + try { + item.attachments = JSON.parse(item.attachments); + } catch (error) { + item.attachments = []; + } + } else { + item.attachments = []; + } + // 解析 detail_items 字段 + if (item.detail_items) { + try { + item.detail_items = JSON.parse(item.detail_items); + } catch (error) { + item.detail_items = []; + } + } else { + item.detail_items = []; + } + return item; + }); + + res.json({ success: true, data, count: data.length }); + } catch (error) { + console.error('获取报销记录失败:', error); + res.status(500).json({ + success: false, + message: '获取报销记录失败', + error: process.env.NODE_ENV === 'development' ? error.message : '操作失败' + }); + } +}); -router.post('/', [ - body('amount').isFloat({ min: 0.01 }), - body('reason').notEmpty(), - body('expense_type').notEmpty() -], validate, async (req, res) => { - try { - const { amount, reason, project_id, currency, reimbursement_date, attachments, amount_cny, applicant, expense_type, detail_items } = req.body; - const user_id = 1; // 临时使用admin用户 - - // 生成报销编号 - const reimbursementCode = `REIMB-${Date.now()}`; - - const result = await db.query( - 'INSERT INTO reimbursements (user_id, project_id, amount, currency, amount_cny, reason, reimbursement_date, reimbursement_code, status, applicant, expense_type, detail_items, attachments) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)', - [user_id, project_id, amount, currency || 'CNY', amount_cny || 0, reason, reimbursement_date, reimbursementCode, 'pending', applicant, expense_type, JSON.stringify(detail_items || []), JSON.stringify(attachments || [])] - ); - - // SQLite不支持RETURNING,所以需要查询刚插入的数据 - const lastInsert = await db.query('SELECT * FROM reimbursements ORDER BY id DESC LIMIT 1'); - res.json({ success: true, data: lastInsert.rows[0] }); - } catch (error) { - console.error('创建报销申请失败:', error); - res.status(500).json({ success: false, message: '创建报销申请失败' }); - } -}); +router.post('/', [ + body('amount').isFloat({ min: 0.01 }), + body('reason').notEmpty(), + body('expense_type').notEmpty() +], validate, async (req, res) => { + try { + const { amount, reason, project_id, currency, reimbursement_date, attachments, amount_cny, applicant, expense_type, detail_items } = req.body; + const user_id = 1; // 临时使用admin用户 + + // 生成报销编号 + const reimbursementCode = `REIMB-${Date.now()}`; + + const result = await db.query( + 'INSERT INTO reimbursements (user_id, project_id, amount, currency, amount_cny, reason, reimbursement_date, reimbursement_code, status, applicant, expense_type, detail_items, attachments) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)', + [user_id, project_id, amount, currency || 'CNY', amount_cny || 0, reason, reimbursement_date, reimbursementCode, 'pending', applicant, expense_type, JSON.stringify(detail_items || []), JSON.stringify(attachments || [])] + ); + + // SQLite不支持RETURNING,所以需要查询刚插入的数据 + const lastInsert = await db.query('SELECT * FROM reimbursements ORDER BY id DESC LIMIT 1'); + res.json({ success: true, data: lastInsert.rows[0] }); + } catch (error) { + console.error('创建报销申请失败:', error); + res.status(500).json({ success: false, message: '创建报销申请失败' }); + } +}); -router.get('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('SELECT * FROM reimbursements WHERE id = $1', [id]); - - if (result.rows.length > 0) { - const data = result.rows[0]; - // 解析 attachments 字段为数组 - if (data.attachments) { - try { - data.attachments = JSON.parse(data.attachments); - } catch (error) { - data.attachments = []; - } - } else { - data.attachments = []; - } - // 解析 detail_items 字段为数组 - if (data.detail_items) { - try { - data.detail_items = JSON.parse(data.detail_items); - } catch (error) { - data.detail_items = []; - } - } else { - data.detail_items = []; - } - res.json({ success: true, data }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('获取报销申请失败:', error); - res.status(500).json({ success: false, message: '获取报销申请失败' }); - } -}); +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('SELECT * FROM reimbursements WHERE id = $1', [id]); + + if (result.rows.length > 0) { + const data = result.rows[0]; + // 解析 attachments 字段为数组 + if (data.attachments) { + try { + data.attachments = JSON.parse(data.attachments); + } catch (error) { + data.attachments = []; + } + } else { + data.attachments = []; + } + // 解析 detail_items 字段为数组 + if (data.detail_items) { + try { + data.detail_items = JSON.parse(data.detail_items); + } catch (error) { + data.detail_items = []; + } + } else { + data.detail_items = []; + } + res.json({ success: true, data }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('获取报销申请失败:', error); + res.status(500).json({ success: false, message: '获取报销申请失败' }); + } +}); -router.put('/:id', async (req, res) => { - try { - const { id } = req.params; - const { amount, reason, project_id, currency, reimbursement_date, attachments, amount_cny, applicant, expense_type, detail_items, status } = req.body; - - const result = await db.query( - 'UPDATE reimbursements SET amount = $1, reason = $2, project_id = $3, currency = $4, reimbursement_date = $5, attachments = $6, amount_cny = $7, applicant = $8, expense_type = $9, detail_items = $10, status = $11 WHERE id = $12', - [amount, reason, project_id, currency, reimbursement_date, JSON.stringify(attachments || []), amount_cny, applicant, expense_type, JSON.stringify(detail_items || []), status, id] - ); - - if (result.changes > 0) { - res.json({ success: true, message: '更新成功' }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('更新报销申请失败:', error); - res.status(500).json({ success: false, message: '更新报销申请失败' }); - } -}); +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { amount, reason, project_id, currency, reimbursement_date, attachments, amount_cny, applicant, expense_type, detail_items, status } = req.body; + + const result = await db.query( + 'UPDATE reimbursements SET amount = $1, reason = $2, project_id = $3, currency = $4, reimbursement_date = $5, attachments = $6, amount_cny = $7, applicant = $8, expense_type = $9, detail_items = $10, status = $11 WHERE id = $12', + [amount, reason, project_id, currency, reimbursement_date, JSON.stringify(attachments || []), amount_cny, applicant, expense_type, JSON.stringify(detail_items || []), status, id] + ); + + if (result.rowCount > 0) { + res.json({ success: true, message: '更新成功' }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('更新报销申请失败:', error); + res.status(500).json({ success: false, message: '更新报销申请失败' }); + } +}); -router.delete('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('DELETE FROM reimbursements WHERE id = $1', [id]); - - if (result.changes > 0) { - res.json({ success: true, message: '删除成功' }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('删除报销申请失败:', error); - res.status(500).json({ success: false, message: '删除报销申请失败' }); - } -}); +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('DELETE FROM reimbursements WHERE id = $1', [id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '删除成功' }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('删除报销申请失败:', error); + res.status(500).json({ success: false, message: '删除报销申请失败' }); + } +}); -router.post('/:id/submit', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['pending', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '提交成功' }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('提交报销申请失败:', error); - res.status(500).json({ success: false, message: '提交报销申请失败' }); - } -}); +router.post('/:id/submit', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '提交成功' }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('提交报销申请失败:', error); + res.status(500).json({ success: false, message: '提交报销申请失败' }); + } +}); -router.post('/:id/withdraw', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['withdrawn', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '撤回成功' }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('撤回报销申请失败:', error); - res.status(500).json({ success: false, message: '撤回报销申请失败' }); - } -}); +router.post('/:id/withdraw', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['withdrawn', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '撤回成功' }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('撤回报销申请失败:', error); + res.status(500).json({ success: false, message: '撤回报销申请失败' }); + } +}); -router.post('/:id/approve', async (req, res) => { - try { - const { id } = req.params; - const { remark } = req.body; - - const result = await db.query('UPDATE reimbursements SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); - - if (result.changes > 0) { - res.json({ success: true, message: '审批通过成功' }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('审批报销申请失败:', error); - res.status(500).json({ success: false, message: '审批报销申请失败' }); - } -}); +router.post('/:id/approve', async (req, res) => { + try { + const { id } = req.params; + const { remark } = req.body; + + const result = await db.query('UPDATE reimbursements SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '审批通过成功' }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('审批报销申请失败:', error); + res.status(500).json({ success: false, message: '审批报销申请失败' }); + } +}); -router.post('/:id/reject', async (req, res) => { - try { - const { id } = req.params; - const { rejectReason } = req.body; - - const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['pending_edit', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '退回成功' }); - } else { - res.status(404).json({ success: false, message: '报销申请不存在' }); - } - } catch (error) { - console.error('退回报销申请失败:', error); - res.status(500).json({ success: false, message: '退回报销申请失败' }); - } -}); +router.post('/:id/reject', async (req, res) => { + try { + const { id } = req.params; + const { rejectReason } = req.body; + + const result = await db.query('UPDATE reimbursements SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '退回成功' }); + } else { + res.status(404).json({ success: false, message: '报销申请不存在' }); + } + } catch (error) { + console.error('退回报销申请失败:', error); + res.status(500).json({ success: false, message: '退回报销申请失败' }); + } +}); module.exports = router; \ No newline at end of file diff --git a/backend/routes/returns.js b/backend/routes/returns.js index 1b9ecd6..f75fea2 100644 --- a/backend/routes/returns.js +++ b/backend/routes/returns.js @@ -248,7 +248,7 @@ router.post('/:id/reject', async (req, res) => { WHERE id = ? `, [reason || '无', id]); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '退库单不存在' }); } diff --git a/backend/routes/users.js b/backend/routes/users.js index 4789918..ab10918 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -4,6 +4,34 @@ const db = require('../db'); const { hashPassword, verifyPassword } = require('../utils/auth'); const { authenticate, requireAdmin } = require('../middleware/auth'); +router.get('/me', authenticate, async (req, res) => { + try { + const result = await db.query('SELECT id, username, name, email, phone, role, avatar, passport, driver_license, is_active, created_at, updated_at FROM users WHERE id = $1', [req.user.id]); + if (result.rows.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + const u = result.rows[0]; + res.json({ + success: true, + data: { + id: u.id, + username: u.username, + name: u.name, + email: u.email, + phone: u.phone, + role: u.role, + avatar: u.avatar, + passport: u.passport, + driverLicense: u.driver_license, + is_active: u.is_active + } + }); + } catch (error) { + console.error('获取用户信息失败:', error); + res.status(500).json({ success: false, message: '获取用户信息失败' }); + } +}); + router.get('/', authenticate, requireAdmin, async (req, res) => { try { const usersResult = await db.query('SELECT id, username, name, email, phone, role, is_active, created_at, updated_at FROM users ORDER BY id'); @@ -45,25 +73,55 @@ router.post('/', authenticate, requireAdmin, async (req, res) => { } }); +router.put('/:id/profile', authenticate, async (req, res) => { + try { + const { id } = req.params; + const userId = parseInt(id); + + if (req.user.id !== userId && req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '只能修改自己的个人信息' }); + } + + const { name, email, phone, avatar, passport, driver_license } = req.body; + + await db.query( + 'UPDATE users SET name = $1, email = $2, phone = $3, avatar = $4, passport = $5, driver_license = $6, updated_at = NOW() WHERE id = $7', + [name, email || null, phone || null, avatar || null, passport || null, driver_license || null, userId] + ); + + const updatedUserResult = await db.query('SELECT id, username, name, email, phone, role, avatar, passport, driver_license, is_active, created_at, updated_at FROM users WHERE id = $1', [userId]); + const updatedUser = updatedUserResult.rows[0]; + + if (!updatedUser) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ success: true, data: updatedUser }); + } catch (error) { + console.error('更新个人信息失败:', error); + res.status(500).json({ success: false, message: '更新个人信息失败' }); + } +}); + router.put('/:id', authenticate, requireAdmin, async (req, res) => { try { const { id } = req.params; - const { name, email, phone, role, password } = req.body; + const { name, email, phone, role, password, avatar, passport, driver_license } = req.body; if (password) { const passwordHash = hashPassword(password); await db.query( - 'UPDATE users SET name = $1, email = $2, phone = $3, role = $4, password_hash = $5, updated_at = NOW() WHERE id = $6', - [name, email || null, phone || null, role, passwordHash, id] + 'UPDATE users SET name = $1, email = $2, phone = $3, role = $4, avatar = $5, passport = $6, driver_license = $7, password_hash = $8, updated_at = NOW() WHERE id = $9', + [name, email || null, phone || null, role, avatar || null, passport || null, driver_license || null, passwordHash, id] ); } else { await db.query( - 'UPDATE users SET name = $1, email = $2, phone = $3, role = $4, updated_at = NOW() WHERE id = $5', - [name, email || null, phone || null, role, id] + 'UPDATE users SET name = $1, email = $2, phone = $3, role = $4, avatar = $5, passport = $6, driver_license = $7, updated_at = NOW() WHERE id = $8', + [name, email || null, phone || null, role, avatar || null, passport || null, driver_license || null, id] ); } - const updatedUserResult = await db.query('SELECT id, username, name, email, phone, role, is_active, created_at, updated_at FROM users WHERE id = $1', [id]); + const updatedUserResult = await db.query('SELECT id, username, name, email, phone, role, avatar, passport, driver_license, is_active, created_at, updated_at FROM users WHERE id = $1', [id]); const updatedUser = updatedUserResult.rows[0]; if (!updatedUser) { diff --git a/backend/routes/verifications-new.js b/backend/routes/verifications-new.js index fa831a9..12ef08a 100644 --- a/backend/routes/verifications-new.js +++ b/backend/routes/verifications-new.js @@ -284,7 +284,7 @@ router.post('/:id/reject', async (req, res) => { WHERE id = ? `, [reason || '无', id]); - if (result.changes === 0) { + if (result.rowCount === 0) { return res.status(404).json({ success: false, message: '验收单不存在' }); } diff --git a/backend/routes/verifications.js b/backend/routes/verifications.js index 1ea0e21..c1ec3cc 100644 --- a/backend/routes/verifications.js +++ b/backend/routes/verifications.js @@ -4,362 +4,362 @@ const { authenticate, requireAdmin } = require('../middleware/auth'); const router = express.Router(); -router.get('/', async (req, res) => { - try { - const { advance_id } = req.query; - let query = ` - SELECT v.*, a.advance_code, a.applicant_id as advance_applicant_id - FROM verifications v - LEFT JOIN advances a ON v.advance_id = a.id - `; - const params = []; - - if (advance_id) { - query += ` WHERE v.advance_id = $1`; - params.push(advance_id); - } - - query += ` ORDER BY v.created_at DESC`; - - const result = await db.query(query, params); - - const data = result.rows.map(item => { - if (item.attachments) { - try { - item.attachments = JSON.parse(item.attachments); - } catch (error) { - item.attachments = []; - } - } else { - item.attachments = []; - } - if (item.detail_items) { - try { - item.detail_items = JSON.parse(item.detail_items); - } catch (error) { - item.detail_items = []; - } - } else { - item.detail_items = []; - } - return item; - }); - - res.json({ success: true, data, count: data.length }); - } catch (error) { - console.error('获取核销记录失败:', error); - res.status(500).json({ success: false, message: '获取核销记录失败' }); - } -}); +router.get('/', async (req, res) => { + try { + const { advance_id } = req.query; + let query = ` + SELECT v.*, a.advance_code, a.applicant_id as advance_applicant_id + FROM verifications v + LEFT JOIN advances a ON v.advance_id = a.id + `; + const params = []; + + if (advance_id) { + query += ` WHERE v.advance_id = $1`; + params.push(advance_id); + } + + query += ` ORDER BY v.created_at DESC`; + + const result = await db.query(query, params); + + const data = result.rows.map(item => { + if (item.attachments) { + try { + item.attachments = JSON.parse(item.attachments); + } catch (error) { + item.attachments = []; + } + } else { + item.attachments = []; + } + if (item.detail_items) { + try { + item.detail_items = JSON.parse(item.detail_items); + } catch (error) { + item.detail_items = []; + } + } else { + item.detail_items = []; + } + return item; + }); + + res.json({ success: true, data, count: data.length }); + } catch (error) { + console.error('获取核销记录失败:', error); + res.status(500).json({ success: false, message: '获取核销记录失败' }); + } +}); -router.post('/', async (req, res) => { - try { - const { verification_date, advance_id, advance_code, advance_amount, currency, reason, detail_items, attachments, applicant, expense_type, project_id, settlement, settlement_amount } = req.body; - - // 生成核销编号 - const verificationCode = `VER-${Date.now()}`; - const amount = detail_items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0; - - // 验证关联预支单 - if (!advance_id && !advance_code) { - return res.status(400).json({ success: false, message: '关联预支单是必填项' }); - } - - let finalAdvanceCode = advance_code; - let finalAdvanceId = advance_id; - - // 如果advance_code为空,根据advance_id查询预支单的advance_code - if (!finalAdvanceCode && finalAdvanceId) { - const advanceResult = await db.query('SELECT advance_code FROM advances WHERE id = $1', [finalAdvanceId]); - if (advanceResult.rows.length > 0) { - finalAdvanceCode = advanceResult.rows[0].advance_code; - } else { - return res.status(400).json({ success: false, message: '关联的预支单不存在' }); - } - } - - // 如果advance_id为空,根据advance_code查询预支单的id - if (!finalAdvanceId && finalAdvanceCode) { - const advanceResult = await db.query('SELECT id FROM advances WHERE advance_code = $1', [finalAdvanceCode]); - if (advanceResult.rows.length > 0) { - finalAdvanceId = advanceResult.rows[0].id; - } else { - return res.status(400).json({ success: false, message: '关联的预支单不存在' }); - } - } - - // 如果仍然为空,返回错误 - if (!finalAdvanceCode || !finalAdvanceId) { - return res.status(400).json({ success: false, message: '关联预支单不存在' }); - } - - // 开始事务 - await db.query('BEGIN TRANSACTION'); - - try { - // 插入核销申请 - const result = await db.query( - 'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, detail_items, attachments, expense_type, project_id, settlement, settlement_amount) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)', - [finalAdvanceId, amount, currency || 'CNY', reason, verification_date, verificationCode, 'pending', applicant, finalAdvanceCode, advance_amount || 0, JSON.stringify(detail_items || []), JSON.stringify(attachments || []), expense_type || 'company', project_id, settlement ? 1 : 0, settlement_amount || 0] - ); - - // 提交事务 - await db.query('COMMIT'); - - // SQLite不支持RETURNING,所以需要查询刚插入的数据 - const lastInsert = await db.query('SELECT * FROM verifications ORDER BY id DESC LIMIT 1'); - res.json({ success: true, data: lastInsert.rows[0] }); - } catch (error) { - // 回滚事务 - await db.query('ROLLBACK'); - throw error; - } - } catch (error) { - console.error('创建核销申请失败:', error); - res.status(500).json({ success: false, message: '创建核销申请失败' }); - } -}); +router.post('/', async (req, res) => { + try { + const { verification_date, advance_id, advance_code, advance_amount, currency, reason, detail_items, attachments, applicant, expense_type, project_id, settlement, settlement_amount } = req.body; + + // 生成核销编号 + const verificationCode = `VER-${Date.now()}`; + const amount = detail_items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0; + + // 验证关联预支单 + if (!advance_id && !advance_code) { + return res.status(400).json({ success: false, message: '关联预支单是必填项' }); + } + + let finalAdvanceCode = advance_code; + let finalAdvanceId = advance_id; + + // 如果advance_code为空,根据advance_id查询预支单的advance_code + if (!finalAdvanceCode && finalAdvanceId) { + const advanceResult = await db.query('SELECT advance_code FROM advances WHERE id = $1', [finalAdvanceId]); + if (advanceResult.rows.length > 0) { + finalAdvanceCode = advanceResult.rows[0].advance_code; + } else { + return res.status(400).json({ success: false, message: '关联的预支单不存在' }); + } + } + + // 如果advance_id为空,根据advance_code查询预支单的id + if (!finalAdvanceId && finalAdvanceCode) { + const advanceResult = await db.query('SELECT id FROM advances WHERE advance_code = $1', [finalAdvanceCode]); + if (advanceResult.rows.length > 0) { + finalAdvanceId = advanceResult.rows[0].id; + } else { + return res.status(400).json({ success: false, message: '关联的预支单不存在' }); + } + } + + // 如果仍然为空,返回错误 + if (!finalAdvanceCode || !finalAdvanceId) { + return res.status(400).json({ success: false, message: '关联预支单不存在' }); + } + + // 开始事务 + await db.query('BEGIN TRANSACTION'); + + try { + // 插入核销申请 + const result = await db.query( + 'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, detail_items, attachments, expense_type, project_id, settlement, settlement_amount) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)', + [finalAdvanceId, amount, currency || 'CNY', reason, verification_date, verificationCode, 'pending', applicant, finalAdvanceCode, advance_amount || 0, JSON.stringify(detail_items || []), JSON.stringify(attachments || []), expense_type || 'company', project_id, settlement ? 1 : 0, settlement_amount || 0] + ); + + // 提交事务 + await db.query('COMMIT'); + + // SQLite不支持RETURNING,所以需要查询刚插入的数据 + const lastInsert = await db.query('SELECT * FROM verifications ORDER BY id DESC LIMIT 1'); + res.json({ success: true, data: lastInsert.rows[0] }); + } catch (error) { + // 回滚事务 + await db.query('ROLLBACK'); + throw error; + } + } catch (error) { + console.error('创建核销申请失败:', error); + res.status(500).json({ success: false, message: '创建核销申请失败' }); + } +}); -router.get('/:id', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('SELECT * FROM verifications WHERE id = $1', [id]); - - if (result.rows.length > 0) { - const data = result.rows[0]; - if (data.attachments) { - try { - data.attachments = JSON.parse(data.attachments); - } catch (error) { - data.attachments = []; - } - } else { - data.attachments = []; - } - if (data.detail_items) { - try { - data.detail_items = JSON.parse(data.detail_items); - } catch (error) { - data.detail_items = []; - } - } else { - data.detail_items = []; - } - res.json({ success: true, data }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - console.error('获取核销申请失败:', error); - res.status(500).json({ success: false, message: '获取核销申请失败' }); - } -}); +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('SELECT * FROM verifications WHERE id = $1', [id]); + + if (result.rows.length > 0) { + const data = result.rows[0]; + if (data.attachments) { + try { + data.attachments = JSON.parse(data.attachments); + } catch (error) { + data.attachments = []; + } + } else { + data.attachments = []; + } + if (data.detail_items) { + try { + data.detail_items = JSON.parse(data.detail_items); + } catch (error) { + data.detail_items = []; + } + } else { + data.detail_items = []; + } + res.json({ success: true, data }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + console.error('获取核销申请失败:', error); + res.status(500).json({ success: false, message: '获取核销申请失败' }); + } +}); -router.put('/:id', async (req, res) => { - try { - const { id } = req.params; - const { verification_date, advance_id, advance_code, advance_amount, currency, reason, detail_items, attachments, applicant, status, expense_type, project_id, settlement, settlement_amount } = req.body; - const amount = detail_items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0; - - // 开始事务 - await db.query('BEGIN TRANSACTION'); - - try { - // 获取原核销金额 - const oldVerification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]); - const oldAmount = oldVerification.rows[0]?.amount || 0; - const oldAdvanceId = oldVerification.rows[0]?.advance_id; - - let finalAdvanceCode = advance_code; - - // 如果advance_code为空,根据advance_id查询预支单的advance_code - if (!finalAdvanceCode && advance_id) { - const advanceResult = await db.query('SELECT advance_code FROM advances WHERE id = $1', [advance_id]); - if (advanceResult.rows.length > 0) { - finalAdvanceCode = advanceResult.rows[0].advance_code; - } - } - - // 如果仍然为空,使用默认值 - if (!finalAdvanceCode) { - finalAdvanceCode = 'UNKNOWN'; - } - - // 更新核销申请 - const result = await db.query( - 'UPDATE verifications SET verification_date = $1, advance_id = $2, amount = $3, currency = $4, reason = $5, advance_code = $6, advance_amount = $7, detail_items = $8, attachments = $9, applicant = $10, status = $11, expense_type = $12, project_id = $13, settlement = $14, settlement_amount = $15 WHERE id = $16', - [verification_date, advance_id, amount, currency, reason, finalAdvanceCode, advance_amount || 0, JSON.stringify(detail_items || []), JSON.stringify(attachments || []), applicant, status, expense_type || 'company', project_id, settlement ? 1 : 0, settlement_amount || 0, id] - ); - - // 不在这里更新预支单已核销金额,而是在执行核销时更新 - // if (oldAdvanceId) { - // const amountDiff = amount - oldAmount; - // if (amountDiff !== 0) { - // await db.query( - // 'UPDATE advances SET total_reimbursed = total_reimbursed + $1 WHERE id = $2', - // [amountDiff, oldAdvanceId] - // ); - // } - // } - - // 提交事务 - await db.query('COMMIT'); - - if (result.changes > 0) { - res.json({ success: true, message: '更新成功' }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - // 回滚事务 - await db.query('ROLLBACK'); - throw error; - } - } catch (error) { - console.error('更新核销申请失败:', error); - res.status(500).json({ success: false, message: '更新核销申请失败' }); - } -}); +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const { verification_date, advance_id, advance_code, advance_amount, currency, reason, detail_items, attachments, applicant, status, expense_type, project_id, settlement, settlement_amount } = req.body; + const amount = detail_items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0; + + // 开始事务 + await db.query('BEGIN TRANSACTION'); + + try { + // 获取原核销金额 + const oldVerification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]); + const oldAmount = oldVerification.rows[0]?.amount || 0; + const oldAdvanceId = oldVerification.rows[0]?.advance_id; + + let finalAdvanceCode = advance_code; + + // 如果advance_code为空,根据advance_id查询预支单的advance_code + if (!finalAdvanceCode && advance_id) { + const advanceResult = await db.query('SELECT advance_code FROM advances WHERE id = $1', [advance_id]); + if (advanceResult.rows.length > 0) { + finalAdvanceCode = advanceResult.rows[0].advance_code; + } + } + + // 如果仍然为空,使用默认值 + if (!finalAdvanceCode) { + finalAdvanceCode = 'UNKNOWN'; + } + + // 更新核销申请 + const result = await db.query( + 'UPDATE verifications SET verification_date = $1, advance_id = $2, amount = $3, currency = $4, reason = $5, advance_code = $6, advance_amount = $7, detail_items = $8, attachments = $9, applicant = $10, status = $11, expense_type = $12, project_id = $13, settlement = $14, settlement_amount = $15 WHERE id = $16', + [verification_date, advance_id, amount, currency, reason, finalAdvanceCode, advance_amount || 0, JSON.stringify(detail_items || []), JSON.stringify(attachments || []), applicant, status, expense_type || 'company', project_id, settlement ? 1 : 0, settlement_amount || 0, id] + ); + + // 不在这里更新预支单已核销金额,而是在执行核销时更新 + // if (oldAdvanceId) { + // const amountDiff = amount - oldAmount; + // if (amountDiff !== 0) { + // await db.query( + // 'UPDATE advances SET total_reimbursed = total_reimbursed + $1 WHERE id = $2', + // [amountDiff, oldAdvanceId] + // ); + // } + // } + + // 提交事务 + await db.query('COMMIT'); + + if (result.rowCount > 0) { + res.json({ success: true, message: '更新成功' }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + // 回滚事务 + await db.query('ROLLBACK'); + throw error; + } + } catch (error) { + console.error('更新核销申请失败:', error); + res.status(500).json({ success: false, message: '更新核销申请失败' }); + } +}); -router.delete('/:id', async (req, res) => { - try { - const { id } = req.params; - - // 开始事务 - await db.query('BEGIN TRANSACTION'); - - try { - // 获取核销金额和预支单ID - const verification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]); - const amount = verification.rows[0]?.amount || 0; - const advanceId = verification.rows[0]?.advance_id; - - // 删除核销申请 - const result = await db.query('DELETE FROM verifications WHERE id = $1', [id]); - - // 不在这里恢复预支单已核销金额,因为只有执行的核销才会增加已核销金额 - // if (advanceId && amount > 0) { - // await db.query( - // 'UPDATE advances SET total_reimbursed = total_reimbursed - $1 WHERE id = $2', - // [amount, advanceId] - // ); - // } - - // 提交事务 - await db.query('COMMIT'); - - if (result.changes > 0) { - res.json({ success: true, message: '删除成功' }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - // 回滚事务 - await db.query('ROLLBACK'); - throw error; - } - } catch (error) { - console.error('删除核销申请失败:', error); - res.status(500).json({ success: false, message: '删除核销申请失败' }); - } -}); +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + + // 开始事务 + await db.query('BEGIN TRANSACTION'); + + try { + // 获取核销金额和预支单ID + const verification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]); + const amount = verification.rows[0]?.amount || 0; + const advanceId = verification.rows[0]?.advance_id; + + // 删除核销申请 + const result = await db.query('DELETE FROM verifications WHERE id = $1', [id]); + + // 不在这里恢复预支单已核销金额,因为只有执行的核销才会增加已核销金额 + // if (advanceId && amount > 0) { + // await db.query( + // 'UPDATE advances SET total_reimbursed = total_reimbursed - $1 WHERE id = $2', + // [amount, advanceId] + // ); + // } + + // 提交事务 + await db.query('COMMIT'); + + if (result.rowCount > 0) { + res.json({ success: true, message: '删除成功' }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + // 回滚事务 + await db.query('ROLLBACK'); + throw error; + } + } catch (error) { + console.error('删除核销申请失败:', error); + res.status(500).json({ success: false, message: '删除核销申请失败' }); + } +}); -router.post('/:id/submit', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['pending', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '提交成功' }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - console.error('提交核销申请失败:', error); - res.status(500).json({ success: false, message: '提交核销申请失败' }); - } -}); +router.post('/:id/submit', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['pending', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '提交成功' }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + console.error('提交核销申请失败:', error); + res.status(500).json({ success: false, message: '提交核销申请失败' }); + } +}); -router.post('/:id/withdraw', async (req, res) => { - try { - const { id } = req.params; - - const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['withdrawn', id]); - - if (result.changes > 0) { - res.json({ success: true, message: '撤回成功' }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - console.error('撤回核销申请失败:', error); - res.status(500).json({ success: false, message: '撤回核销申请失败' }); - } -}); +router.post('/:id/withdraw', async (req, res) => { + try { + const { id } = req.params; + + const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['withdrawn', id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '撤回成功' }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + console.error('撤回核销申请失败:', error); + res.status(500).json({ success: false, message: '撤回核销申请失败' }); + } +}); -router.post('/:id/approve', async (req, res) => { - try { - const { id } = req.params; - const { remark } = req.body; - - const result = await db.query('UPDATE verifications SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); - - if (result.changes > 0) { - res.json({ success: true, message: '审批通过成功' }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - console.error('审批核销申请失败:', error); - res.status(500).json({ success: false, message: '审批核销申请失败' }); - } -}); +router.post('/:id/approve', async (req, res) => { + try { + const { id } = req.params; + const { remark } = req.body; + + const result = await db.query('UPDATE verifications SET status = $1, approval_remark = $2 WHERE id = $3', ['approved', remark, id]); + + if (result.rowCount > 0) { + res.json({ success: true, message: '审批通过成功' }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + console.error('审批核销申请失败:', error); + res.status(500).json({ success: false, message: '审批核销申请失败' }); + } +}); -router.post('/:id/reject', async (req, res) => { - try { - const { id } = req.params; - const { rejectReason } = req.body; - - // 开始事务 - await db.query('BEGIN TRANSACTION'); - - try { - // 获取核销金额和预支单ID - const verification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]); - const amount = verification.rows[0]?.amount || 0; - const advanceId = verification.rows[0]?.advance_id; - - // 退回核销申请 - const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['pending_edit', id]); - - // 不在这里恢复预支单已核销金额,因为只有执行的核销才会增加已核销金额 - // if (advanceId && amount > 0) { - // await db.query( - // 'UPDATE advances SET total_reimbursed = total_reimbursed - $1 WHERE id = $2', - // [amount, advanceId] - // ); - // } - - // 提交事务 - await db.query('COMMIT'); - - if (result.changes > 0) { - res.json({ success: true, message: '退回成功' }); - } else { - res.status(404).json({ success: false, message: '核销申请不存在' }); - } - } catch (error) { - // 回滚事务 - await db.query('ROLLBACK'); - throw error; - } - } catch (error) { - console.error('退回核销申请失败:', error); - res.status(500).json({ success: false, message: '退回核销申请失败' }); - } -}); +router.post('/:id/reject', async (req, res) => { + try { + const { id } = req.params; + const { rejectReason } = req.body; + + // 开始事务 + await db.query('BEGIN TRANSACTION'); + + try { + // 获取核销金额和预支单ID + const verification = await db.query('SELECT amount, advance_id FROM verifications WHERE id = $1', [id]); + const amount = verification.rows[0]?.amount || 0; + const advanceId = verification.rows[0]?.advance_id; + + // 退回核销申请 + const result = await db.query('UPDATE verifications SET status = $1 WHERE id = $2', ['pending', id]); + + // 不在这里恢复预支单已核销金额,因为只有执行的核销才会增加已核销金额 + // if (advanceId && amount > 0) { + // await db.query( + // 'UPDATE advances SET total_reimbursed = total_reimbursed - $1 WHERE id = $2', + // [amount, advanceId] + // ); + // } + + // 提交事务 + await db.query('COMMIT'); + + if (result.rowCount > 0) { + res.json({ success: true, message: '退回成功' }); + } else { + res.status(404).json({ success: false, message: '核销申请不存在' }); + } + } catch (error) { + // 回滚事务 + await db.query('ROLLBACK'); + throw error; + } + } catch (error) { + console.error('退回核销申请失败:', error); + res.status(500).json({ success: false, message: '退回核销申请失败' }); + } +}); module.exports = router; \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f662f92..ef9da86 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -73,6 +73,7 @@ import { useLanguageStore } from './store/languageStore' // 路由守卫组件 const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { isAuthenticated, user, isLoading } = useAuthStore() + const { t, currentLanguage } = useLanguageStore() if (isLoading) { return ( @@ -83,7 +84,7 @@ const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => height: '100vh', fontSize: '24px' }}> - 加载中... + {t('common.loading')} ) } @@ -143,6 +144,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/BusinessLedgerTab.tsx b/frontend/src/components/BusinessLedgerTab.tsx index 7c0a179..ddcc09a 100644 --- a/frontend/src/components/BusinessLedgerTab.tsx +++ b/frontend/src/components/BusinessLedgerTab.tsx @@ -1,6 +1,7 @@ import React from 'react' import { Table, Card, Row, Col, Statistic, Empty, Tag } from 'antd' import { DollarOutlined } from '@ant-design/icons' +import { useLanguageStore } from '../store/languageStore' interface LedgerItem { id: number @@ -56,21 +57,23 @@ const formatAmount = (amount: number | undefined, currency?: string) => { const getStatusTag = (status: string | undefined) => { if (!status) return '-' + const t = useLanguageStore.getState().t const map: Record = { - completed: { color: 'success', text: '已完成' }, - in_progress: { color: 'processing', text: '进行中' }, - planning: { color: 'default', text: '规划中' }, - pending: { color: 'default', text: '待处理' }, - approved: { color: 'success', text: '已批准' }, - paid: { color: 'green', text: '已支付' }, - requested: { color: 'blue', text: '已申请' }, - active: { color: 'processing', text: '进行中' }, + completed: { color: 'success', text: t('businessLedger.completed') }, + in_progress: { color: 'processing', text: t('businessLedger.inProgress') }, + planning: { color: 'default', text: t('businessLedger.planning') }, + pending: { color: 'default', text: t('businessLedger.pending') }, + approved: { color: 'success', text: t('businessLedger.approved') }, + paid: { color: 'green', text: t('businessLedger.paid') }, + requested: { color: 'blue', text: t('businessLedger.applied') }, + active: { color: 'processing', text: t('businessLedger.inProgress') }, } const info = map[status] || { color: 'default', text: status } return {info.text} } const BusinessLedgerTab: React.FC = ({ partnerType, summary, items, loading }) => { + const { t, currentLanguage } = useLanguageStore() const renderSummaryCards = () => { switch (partnerType) { case 'subcontractor': @@ -78,22 +81,22 @@ const BusinessLedgerTab: React.FC = ({ partnerType, summ - + - + - + - + @@ -103,22 +106,22 @@ const BusinessLedgerTab: React.FC = ({ partnerType, summ - + - + - + - + @@ -128,22 +131,22 @@ const BusinessLedgerTab: React.FC = ({ partnerType, summ - + - + - + - + @@ -153,22 +156,22 @@ const BusinessLedgerTab: React.FC = ({ partnerType, summ - + - + - + - + @@ -178,36 +181,36 @@ const BusinessLedgerTab: React.FC = ({ partnerType, summ const getColumns = () => { const baseColumns: any[] = [ - { title: '编号', dataIndex: 'code', key: 'code', width: 120 }, - { title: '名称', dataIndex: 'name', key: 'name', render: (v: string) => {v} }, + { title: t('businessLedger.code'), dataIndex: 'code', key: 'code', width: 120 }, + { title: t('businessLedger.name'), dataIndex: 'name', key: 'name', render: (v: string) => {v} }, ] switch (partnerType) { case 'subcontractor': return [ ...baseColumns, - { title: '合同金额', dataIndex: 'contract_amount', key: 'contract_amount', align: 'right' as const, render: (v: number) => formatAmount(v) }, - { title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, + { title: t('businessLedger.contractAmount'), dataIndex: 'contract_amount', key: 'contract_amount', align: 'right' as const, render: (v: number) => formatAmount(v) }, + { title: t('businessLedger.status'), dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, ] case 'customer': return [ ...baseColumns, - { title: '合同金额', dataIndex: 'contract_amount', key: 'contract_amount', align: 'right' as const, render: (v: number) => formatAmount(v) }, - { title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, + { title: t('businessLedger.contractAmount'), dataIndex: 'contract_amount', key: 'contract_amount', align: 'right' as const, render: (v: number) => formatAmount(v) }, + { title: t('businessLedger.status'), dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, ] case 'supplier': return [ ...baseColumns, - { title: '采购金额', dataIndex: 'order_amount', key: 'order_amount', align: 'right' as const, render: (v: number) => formatAmount(v) }, - { title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, + { title: t('businessLedger.purchaseAmount'), dataIndex: 'order_amount', key: 'order_amount', align: 'right' as const, render: (v: number) => formatAmount(v) }, + { title: t('businessLedger.status'), dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, ] case 'logistics': return [ ...baseColumns, - { title: '项目', dataIndex: 'project_name', key: 'project_name', render: (v: string) => v || '-' }, - { title: '一次运费', dataIndex: 'primary_freight', key: 'primary_freight', align: 'right' as const, render: (v: number, r: LedgerItem) => formatAmount(v, r.primary_freight_currency) }, - { title: '一次运费状态', dataIndex: 'primary_freight_status', key: 'primary_freight_status', align: 'center' as const, width: 100, render: getStatusTag }, - { title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, + { title: t('businessLedger.project'), dataIndex: 'project_name', key: 'project_name', render: (v: string) => v || '-' }, + { title: t('businessLedger.freight1'), dataIndex: 'primary_freight', key: 'primary_freight', align: 'right' as const, render: (v: number, r: LedgerItem) => formatAmount(v, r.primary_freight_currency) }, + { title: t('businessLedger.freight1Status'), dataIndex: 'primary_freight_status', key: 'primary_freight_status', align: 'center' as const, width: 100, render: getStatusTag }, + { title: t('businessLedger.status'), dataIndex: 'status', key: 'status', align: 'center' as const, render: getStatusTag }, ] default: return baseColumns @@ -228,7 +231,7 @@ const BusinessLedgerTab: React.FC = ({ partnerType, summ bordered /> ) : ( - + )} ) diff --git a/frontend/src/components/ContactManager.tsx b/frontend/src/components/ContactManager.tsx index 723522e..50f7587 100644 --- a/frontend/src/components/ContactManager.tsx +++ b/frontend/src/components/ContactManager.tsx @@ -76,8 +76,8 @@ const ContactManager: React.FC = ({ render: (_, record) => ( {record.mobile &&
{record.mobile}
} - {record.phone &&
电话: {record.phone}
} - {record.wechat &&
微信: {record.wechat}
} + {record.phone &&
{t('component.phonePrefix')}{record.phone}
} + {record.wechat &&
{t('component.wechatPrefix')}{record.wechat}
}
) }, @@ -192,7 +192,7 @@ const ContactManager: React.FC = ({ - + diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index 3e7a528..7c6f253 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -1,4 +1,5 @@ import React, { Component, ReactNode } from 'react' +import { useLanguageStore } from '../store/languageStore' interface ErrorBoundaryProps { children: ReactNode @@ -8,24 +9,44 @@ interface ErrorBoundaryProps { interface ErrorBoundaryState { hasError: boolean error?: Error + currentLanguage: string } class ErrorBoundary extends Component { + unsubscribe: (() => void) | null = null + constructor(props: ErrorBoundaryProps) { super(props) - this.state = { hasError: false } + this.state = { + hasError: false, + currentLanguage: useLanguageStore.getState().currentLanguage + } } - static getDerivedStateFromError(error: Error): ErrorBoundaryState { + static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error } } + componentDidMount() { + this.unsubscribe = useLanguageStore.subscribe((state) => { + if (state.currentLanguage !== this.state.currentLanguage) { + this.setState({ currentLanguage: state.currentLanguage }) + } + }) + } + + componentWillUnmount() { + this.unsubscribe?.() + } + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('组件渲染错误:', error) console.error('错误信息:', errorInfo.componentStack) } render() { + const t = useLanguageStore.getState().t + if (this.state.hasError) { if (this.props.fallback) { return this.props.fallback @@ -45,9 +66,9 @@ class ErrorBoundary extends Component { boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
⚠️
-

页面加载出错

+

{t('errorBoundary.title')}

- 抱歉,页面渲染时发生了错误。请尝试刷新页面或联系管理员。 + {t('errorBoundary.description')}

{this.state.error && (
{ maxWidth: '600px', overflow: 'auto' }}> -
错误信息: {this.state.error.message}
+
{t('errorBoundary.errorInfo')} {this.state.error.message}
{this.state.error.stack && (
- 错误堆栈: + {t('errorBoundary.stackTrace')}
                     {this.state.error.stack}
                   
@@ -85,7 +106,7 @@ class ErrorBoundary extends Component { fontSize: '14px' }} > - 刷新页面 + {t('errorBoundary.refresh')}
@@ -96,4 +117,4 @@ class ErrorBoundary extends Component { } } -export default ErrorBoundary \ No newline at end of file +export default ErrorBoundary diff --git a/frontend/src/components/FileUpload.tsx b/frontend/src/components/FileUpload.tsx index e33a8d6..53057a3 100644 --- a/frontend/src/components/FileUpload.tsx +++ b/frontend/src/components/FileUpload.tsx @@ -1,185 +1,203 @@ -import React, { useState, useEffect } from 'react'; -import { Upload, Modal, Image, Spin, Progress, message } from 'antd'; -import { PlusOutlined, FileOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; -import type { UploadFile, UploadProps } from 'antd/es/upload/interface'; - -interface FileUploadProps { - value?: string[]; - onChange?: (urls: string[]) => void; - maxCount?: number; - accept?: string; -} - -// 支持的图片格式 -const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; -const officeFormats = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']; -const isImage = (url: string) => { - const ext = url.split('.').pop()?.toLowerCase(); - return imageFormats.includes(ext || ''); -}; - -const isOfficeFile = (url: string) => { - const ext = url.split('.').pop()?.toLowerCase(); - return officeFormats.includes(ext || ''); -}; - -const getOfficePreviewUrl = (url: string) => { - // 使用微软的Office 365在线预览服务 - return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`; -}; - -const FileUpload: React.FC = ({ - value = [], - onChange, - maxCount = 9, - accept = 'image/*' -}) => { - const [previewOpen, setPreviewOpen] = useState(false); - const [previewImage, setPreviewImage] = useState(''); - const [fileList, setFileList] = useState([]); - const [uploading, setUploading] = useState(false); - - // 当 value 变化时,更新 fileList - useEffect(() => { - // 只有当 value 是数组时才更新 fileList - // 这样可以避免在上传过程中被重置 - if (Array.isArray(value)) { - const newFileList = value.map((url, index) => ({ - uid: `-${index}`, - name: url.split('/').pop() || `file-${index}`, - status: 'done', - url, - thumbUrl: isImage(url) ? url : undefined - })); - - setFileList(newFileList); - } - }, [value]); - - const handlePreview = async (file: UploadFile) => { - const url = file.url || ''; - if (isImage(url)) { - setPreviewImage(url); - setPreviewOpen(true); - } else if (isOfficeFile(url)) { - // Office文件,使用微软的在线预览服务 - const previewUrl = getOfficePreviewUrl(url); - window.open(previewUrl, '_blank'); - } else { - // 其他文件,新窗口打开 - window.open(url, '_blank'); - } - }; - - const handleChange: UploadProps['onChange'] = (info) => { - const { fileList } = info; - setFileList(fileList); - - // 只有当文件状态发生变化时才调用 onChange - // 避免在初始化时触发无限循环 - if (info.file.status === 'done' || info.file.status === 'removed') { - // 提取已上传成功的URL - const urls = fileList - .filter(file => file.status === 'done') - .map(file => { - // 处理不同格式的文件对象 - if (file.url) { - return file.url; - } else if (file.response && file.response.url) { - return file.response.url; - } else if (file.response && typeof file.response === 'string') { - return file.response; - } - return ''; - }) - .filter(url => url); // 过滤空字符串 - - onChange?.(urls); - } - }; - - const customRequest = async (options: any) => { - const { file, onSuccess, onError, onProgress } = options; - - setUploading(true); - - const formData = new FormData(); - formData.append('file', file); - - try { - const res = await fetch('/api/upload/single', { - method: 'POST', - body: formData - }); - - const data = await res.json(); - - - if (data.success) { - onProgress({ percent: 100 }); - // 传递包含url属性的对象,这是Ant Design Upload组件在customRequest中期望的格式 - onSuccess({ url: data.data.url }, file); - message.success('上传成功'); - } else { - onError(new Error(data.error)); - message.error(data.error || '上传失败'); - } - } catch (error) { - console.error('上传错误:', error); - onError(error); - message.error('上传失败'); - } finally { - setUploading(false); - } - }; - - const uploadButton = ( -
- -
上传
-
- ); - - return ( - <> - - {fileList.length >= maxCount ? null : uploadButton} - - - {/* 图片预览弹窗 */} - setPreviewOpen(false)} - width="80%" - centered - > -
- -
-
- - {uploading && ( -
- 上传中... -
- )} - - ); -}; - -export default FileUpload; +import React, { useState, useEffect } from 'react'; +import { Upload, Modal, Image, Spin, Progress, message } from 'antd'; +import { PlusOutlined, FileOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons'; +import type { UploadFile, UploadProps } from 'antd/es/upload/interface'; +import { useLanguageStore } from '../store/languageStore'; + +interface FileUploadProps { + value?: string[]; + onChange?: (urls: string[]) => void; + maxCount?: number; + accept?: string; +} + +// 支持的图片格式 +const imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; +const officeFormats = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']; +const isImage = (url: string) => { + const ext = url.split('.').pop()?.toLowerCase(); + return imageFormats.includes(ext || ''); +}; + +const isOfficeFile = (url: string) => { + const ext = url.split('.').pop()?.toLowerCase(); + return officeFormats.includes(ext || ''); +}; + +const getOfficePreviewUrl = (url: string) => { + // 使用微软的Office 365在线预览服务 + return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`; +}; + +const FileUpload: React.FC = ({ + value = [], + onChange, + maxCount = 9, + accept = 'image/*' +}) => { + const [previewOpen, setPreviewOpen] = useState(false); + const [previewImage, setPreviewImage] = useState(''); + const [fileList, setFileList] = useState([]); + const [uploading, setUploading] = useState(false); + const { t, currentLanguage } = useLanguageStore(); + + // 当 value 变化时,更新 fileList + useEffect(() => { + // 只有当 value 是数组时才更新 fileList + // 这样可以避免在上传过程中被重置 + if (Array.isArray(value)) { + const newFileList = value.map((url, index) => ({ + uid: `-${index}`, + name: url.split('/').pop() || `file-${index}`, + status: 'done', + url, + thumbUrl: isImage(url) ? url : undefined + })); + + setFileList(newFileList); + } + }, [value]); + + const handlePreview = async (file: UploadFile) => { + const url = file.url || ''; + if (isImage(url)) { + setPreviewImage(url); + setPreviewOpen(true); + } else if (isOfficeFile(url)) { + // Office文件,使用微软的在线预览服务 + const previewUrl = getOfficePreviewUrl(url); + window.open(previewUrl, '_blank'); + } else { + // 其他文件,新窗口打开 + window.open(url, '_blank'); + } + }; + + const handleChange: UploadProps['onChange'] = (info) => { + const { fileList } = info; + setFileList(fileList); + + // 只有当文件状态发生变化时才调用 onChange + // 避免在初始化时触发无限循环 + if (info.file.status === 'done' || info.file.status === 'removed') { + // 提取已上传成功的URL + const urls = fileList + .filter(file => file.status === 'done') + .map(file => { + // 处理不同格式的文件对象 + if (file.url) { + return file.url; + } else if (file.response && file.response.url) { + return file.response.url; + } else if (file.response && typeof file.response === 'string') { + return file.response; + } + return ''; + }) + .filter(url => url); // 过滤空字符串 + + onChange?.(urls); + } + }; + + const customRequest = async (options: any) => { + const { file, onSuccess, onError, onProgress } = options; + + setUploading(true); + + const formData = new FormData(); + formData.append('file', file); + + try { + // 从 localStorage 读取认证 token + let authHeader = ''; + try { + const authStorage = localStorage.getItem('auth-storage'); + if (authStorage) { + const parsed = JSON.parse(authStorage); + const token = parsed?.state?.token; + if (token) { + authHeader = `Bearer ${token}`; + } + } + } catch (e) { + // 忽略解析错误 + } + + const res = await fetch('/api/upload/single', { + method: 'POST', + body: formData, + headers: authHeader ? { Authorization: authHeader } : {}, + }); + + const data = await res.json(); + + + if (data.success) { + onProgress({ percent: 100 }); + // 传递包含url属性的对象,这是Ant Design Upload组件在customRequest中期望的格式 + onSuccess({ url: data.data.url }, file); + message.success(t('fileUpload.uploadSuccess')); + } else { + onError(new Error(data.error)); + message.error(data.error || t('fileUpload.uploadFailed')); + } + } catch (error) { + console.error('上传错误:', error); + onError(error); + message.error(t('fileUpload.uploadFailed')); + } finally { + setUploading(false); + } + }; + + const uploadButton = ( +
+ +
{t('fileUpload.upload')}
+
+ ); + + return ( + <> + + {fileList.length >= maxCount ? null : uploadButton} + + + {/* 图片预览弹窗 */} + setPreviewOpen(false)} + width="80%" + centered + > +
+ +
+
+ + {uploading && ( +
+ {t('fileUpload.uploading')} +
+ )} + + ); +}; + +export default FileUpload; diff --git a/frontend/src/components/common/CompanyLogo.tsx b/frontend/src/components/common/CompanyLogo.tsx index 74454d3..e3c7ee9 100644 --- a/frontend/src/components/common/CompanyLogo.tsx +++ b/frontend/src/components/common/CompanyLogo.tsx @@ -1,62 +1,64 @@ -import React from 'react' -import { Space, Typography } from 'antd' -import { ThunderboltOutlined } from '@ant-design/icons' - -const { Text, Title } = Typography - -interface CompanyLogoProps { - showText?: boolean - size?: 'small' | 'medium' | 'large' -} - -const CompanyLogo: React.FC = ({ showText = true, size = 'medium' }) => { - const sizeMap = { - small: { fontSize: 14, iconSize: 20 }, - medium: { fontSize: 16, iconSize: 28 }, - large: { fontSize: 20, iconSize: 36 } - } - - const { fontSize, iconSize } = sizeMap[size] - - return ( - - {/* 图标 */} - - - {/* 公司名称 */} - {showText && ( -
- - 轻远电力老挝ERP - - - Qingyuan Power Laos - -
- )} -
- ) -} - -export default CompanyLogo +import React from 'react' +import { Space, Typography } from 'antd' +import { ThunderboltOutlined } from '@ant-design/icons' +import { useLanguageStore } from '../../store/languageStore' + +const { Text, Title } = Typography + +interface CompanyLogoProps { + showText?: boolean + size?: 'small' | 'medium' | 'large' +} + +const CompanyLogo: React.FC = ({ showText = true, size = 'medium' }) => { + const { t, currentLanguage } = useLanguageStore() + const sizeMap = { + small: { fontSize: 14, iconSize: 20 }, + medium: { fontSize: 16, iconSize: 28 }, + large: { fontSize: 20, iconSize: 36 } + } + + const { fontSize, iconSize } = sizeMap[size] + + return ( + + {/* 图标 */} + + + {/* 公司名称 */} + {showText && ( +
+ + {t('login.title')} + + + Qingyuan Power Laos + +
+ )} +
+ ) +} + +export default CompanyLogo diff --git a/frontend/src/components/layout/MainLayout.tsx b/frontend/src/components/layout/MainLayout.tsx index b231be8..8d3c7d4 100644 --- a/frontend/src/components/layout/MainLayout.tsx +++ b/frontend/src/components/layout/MainLayout.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { Outlet, useNavigate, useLocation } from 'react-router-dom' import { Layout, @@ -49,169 +49,6 @@ import LanguageSelector from '../common/LanguageSelector' const { Header, Sider, Content } = Layout const { Text } = Typography -const menuItems = [ - { - key: '/dashboard', - icon: , - label: '工作台' - }, - { - key: '/projects', - icon: , - label: '项目管理' - }, - { - key: '/budget-projects', - icon: , - label: '预算报价' - }, - { - key: '/construction', - icon: , - label: '施工管理', - children: [ - { - key: '/construction', - label: '施工总览' - } - ] - }, - { - key: 'approval', - icon: , - label: '审批管理', - children: [ - { - key: '/approval', - icon: , - label: '待审批' - }, - { - key: '/execution', - icon: , - label: '待执行' - } - ] - }, - { - key: 'finance-docs', - icon: , - label: '财务申请', - children: [ - { - key: '/advances', - icon: , - label: '预支申请' - }, - { - key: '/reimbursements', - icon: , - label: '报销申请' - }, - { - key: '/payment-requests', - icon: , - label: '付款申请' - }, - { - key: '/verification', - icon: , - label: '核销申请' - } - ] - }, - { - key: 'finance-group', - icon: , - label: '财务管理', - children: [ - { - key: '/finance', - label: '财务概览' - }, - { - key: '/exchange-rates', - icon: , - label: '汇率管理' - }, - { - key: '/project-cost', - icon: , - label: '项目成本' - }, - { - key: '/advances/verification-status', - icon: , - label: '预支核销状态' - } - ] - }, - { - key: '/reports', - icon: , - label: '报表分析' - }, - { - key: 'procurement', - icon: , - label: '采购管理', - children: [ - { - key: '/products', - icon: , - label: '商品管理' - }, - { - key: '/purchase-requests', - icon: , - label: '采购申请' - }, - { - key: '/purchase-orders', - icon: , - label: '采购订单' - }, - { - key: '/payment-plans', - icon: , - label: '付款计划' - }, - { - key: '/inventory', - icon: , - label: '库存管理' - } - ] - }, - { - key: 'partners', - icon: , - label: '合作伙伴', - children: [ - { - key: '/suppliers', - icon: , - label: '供应商管理' - }, - { - key: '/subcontractors', - icon: , - label: '分包商管理' - }, - { - key: '/customers', - icon: , - label: '客户管理' - }, - { - key: '/logistics-companies', - icon: , - label: '物流管理' - } - ] - } -] - const MainLayout: React.FC = () => { const navigate = useNavigate() const location = useLocation() @@ -221,11 +58,174 @@ const MainLayout: React.FC = () => { const [settingsVisible, setSettingsVisible] = useState(false) const [openKeys, setOpenKeys] = useState([]) const { user, logout } = useAuthStore() - const { t } = useLanguageStore() + const { t, currentLanguage } = useLanguageStore() const { token: { colorBgContainer, borderRadiusLG }, } = theme.useToken() + + const menuItems = useMemo(() => [ + { + key: '/dashboard', + icon: , + label: t('menu.dashboard') + }, + { + key: '/projects', + icon: , + label: t('menu.projects') + }, + { + key: '/budget-projects', + icon: , + label: t('menu.budgetQuotation') + }, + { + key: '/construction', + icon: , + label: t('menu.construction'), + children: [ + { + key: '/construction', + label: t('menu.constructionOverview') + } + ] + }, + { + key: 'approval', + icon: , + label: t('menu.approval'), + children: [ + { + key: '/approval', + icon: , + label: t('menu.pendingApproval') + }, + { + key: '/execution', + icon: , + label: t('menu.pendingExecution') + } + ] + }, + { + key: 'finance-docs', + icon: , + label: t('menu.financeDocs'), + children: [ + { + key: '/advances', + icon: , + label: t('menu.advanceApply') + }, + { + key: '/reimbursements', + icon: , + label: t('menu.reimburseApply') + }, + { + key: '/payment-requests', + icon: , + label: t('menu.paymentApply') + }, + { + key: '/verification', + icon: , + label: t('menu.verificationApply') + } + ] + }, + { + key: 'finance-group', + icon: , + label: t('menu.financeManagement'), + children: [ + { + key: '/finance', + label: t('menu.financeOverview') + }, + { + key: '/exchange-rates', + icon: , + label: t('menu.exchangeRate') + }, + { + key: '/project-cost', + icon: , + label: t('menu.projectCost') + }, + { + key: '/advances/verification-status', + icon: , + label: t('menu.advanceVerificationStatus') + } + ] + }, + { + key: '/reports', + icon: , + label: t('menu.reports') + }, + { + key: 'procurement', + icon: , + label: t('menu.procurement'), + children: [ + { + key: '/products', + icon: , + label: t('menu.productManagement') + }, + { + key: '/purchase-requests', + icon: , + label: t('menu.purchaseRequest') + }, + { + key: '/purchase-orders', + icon: , + label: t('menu.purchaseOrder') + }, + { + key: '/payment-plans', + icon: , + label: t('menu.paymentPlan') + }, + { + key: '/inventory', + icon: , + label: t('menu.inventory') + } + ] + }, + { + key: 'partners', + icon: , + label: t('menu.partners'), + children: [ + { + key: '/suppliers', + icon: , + label: t('menu.supplierManagement') + }, + { + key: '/subcontractors', + icon: , + label: t('menu.subcontractorManagement') + }, + { + key: '/customers', + icon: , + label: t('menu.customerManagement') + }, + { + key: '/logistics-companies', + icon: , + label: t('menu.logisticsManagement') + } + ] + } + ], [t, currentLanguage]) useEffect(() => { const checkMobile = () => { @@ -241,23 +241,23 @@ const MainLayout: React.FC = () => { return () => window.removeEventListener('resize', checkMobile) }, []) - const userMenuItems = [ + const userMenuItems = useMemo(() => [ { key: 'profile', icon: , - label: '个人信息' + label: t('menu.profile') }, { key: 'settings', icon: , - label: '系统设置' + label: t('menu.settings') }, ...(user?.role === 'admin' ? [{ type: 'divider' as const }, { key: '/admin', icon: , - label: '后台管理' + label: t('menu.admin') }] : []), { type: 'divider' as const @@ -265,9 +265,9 @@ const MainLayout: React.FC = () => { { key: 'logout', icon: , - label: '退出登录' + label: t('menu.logout') } - ] + ], [t, currentLanguage, user?.role]) const handleMenuClick = ({ key }: { key: string }) => { if (key === 'logout') { @@ -313,7 +313,9 @@ const MainLayout: React.FC = () => { path.startsWith('/inventory')) { return ['procurement'] } - if (path.startsWith('/project-cost')) { + if (path.startsWith('/project-cost') || + path.startsWith('/finance') || + path.startsWith('/exchange-rates')) { return ['finance-group'] } return [] @@ -389,7 +391,7 @@ const MainLayout: React.FC = () => { onClick={() => setCollapsed(!collapsed)} style={{ width: collapsed ? '100%' : 'auto' }} > - {!collapsed && '收起菜单'} + {!collapsed && t('menu.collapse')} @@ -446,7 +448,7 @@ const MainLayout: React.FC = () => { } style={{ backgroundColor: '#1890ff' }} /> - {!isMobile && {user?.name || user?.username || '用户'}} + {!isMobile && {user?.name || user?.username || t('user.userLabel')}} @@ -462,15 +464,15 @@ const MainLayout: React.FC = () => { setSettingsVisible(false)} footer={null} > -

系统设置功能开发中...

+

{t('menu.settings')}...

) } -export default MainLayout +export default MainLayout \ No newline at end of file diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index bb8da4e..de4d599 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -1,135 +1,137 @@ -import React from 'react'; -import { Outlet, Navigate, useLocation } from 'react-router-dom'; -import { Layout, Menu } from 'antd'; -import { - UserOutlined, - SafetyOutlined, - FileTextOutlined, - DatabaseOutlined, - InfoCircleOutlined, - ArrowLeftOutlined, - SettingOutlined, - AppstoreOutlined, - AccountBookOutlined, - ImportOutlined -} from '@ant-design/icons'; -import { useNavigate } from 'react-router-dom'; - -const { Sider, Content } = Layout; - -const AdminLayout: React.FC = () => { - const navigate = useNavigate(); - const location = useLocation(); - - const menuItems = [ - { - key: '/admin/users', - icon: , - label: '用户管理' - }, - { - key: '/admin/roles', - icon: , - label: '角色权限' - }, - { - key: '/admin/process', - icon: , - label: '流程管理' - }, - { - key: '/admin/process-templates', - icon: , - label: '工程模板管理' - }, - { - key: '/admin/expense-categories', - icon: , - label: '财务分类管理' - }, - { - key: '/admin/excel-import', - icon: , - label: 'Excel批量导入' - }, - { - key: '/admin/logs', - icon: , - label: '系统日志' - }, - { - key: '/admin/backup', - icon: , - label: '数据备份' - }, - { - key: '/admin/about', - icon: , - label: '关于系统' - } - ]; - - return ( - - -
- 系统后台管理 -
- navigate(key)} - style={{ borderRight: 0 }} - /> -
-
navigate('/dashboard')} - style={{ - cursor: 'pointer', - color: '#1890ff', - display: 'flex', - alignItems: 'center', - gap: 8 - }} - > - 返回前台 -
-
- - - - - - - - ); -}; - -export default AdminLayout; +import React from 'react'; +import { Outlet, Navigate, useLocation } from 'react-router-dom'; +import { Layout, Menu } from 'antd'; +import { + UserOutlined, + SafetyOutlined, + FileTextOutlined, + DatabaseOutlined, + InfoCircleOutlined, + ArrowLeftOutlined, + SettingOutlined, + AppstoreOutlined, + AccountBookOutlined, + ImportOutlined +} from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { useLanguageStore } from '../store/languageStore'; + +const { Sider, Content } = Layout; + +const AdminLayout: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { t, currentLanguage } = useLanguageStore(); + + const menuItems = [ + { + key: '/admin/users', + icon: , + label: t('menu.userManagement') + }, + { + key: '/admin/roles', + icon: , + label: t('menu.rolePermission') + }, + { + key: '/admin/process', + icon: , + label: t('menu.processManagement') + }, + { + key: '/admin/process-templates', + icon: , + label: t('menu.templateManagement') + }, + { + key: '/admin/expense-categories', + icon: , + label: t('menu.expenseCategory') + }, + { + key: '/admin/excel-import', + icon: , + label: t('menu.excelImport') + }, + { + key: '/admin/logs', + icon: , + label: t('menu.systemLogs') + }, + { + key: '/admin/backup', + icon: , + label: t('menu.dataBackup') + }, + { + key: '/admin/about', + icon: , + label: t('menu.aboutSystem') + } + ]; + + return ( + + +
+ {t('menu.admin')} +
+ navigate(key)} + style={{ borderRight: 0 }} + /> +
+
navigate('/dashboard')} + style={{ + cursor: 'pointer', + color: '#1890ff', + display: 'flex', + alignItems: 'center', + gap: 8 + }} + > + {t('menu.backToFront')} +
+
+ + + + + + + + ); +}; + +export default AdminLayout; diff --git a/frontend/src/locales/en-US.ts b/frontend/src/locales/en-US.ts index 5b8cc0a..2dcf685 100644 --- a/frontend/src/locales/en-US.ts +++ b/frontend/src/locales/en-US.ts @@ -1,69 +1,2551 @@ -export default { - // Common - common: { - confirm: 'Confirm', - cancel: 'Cancel', - save: 'Save', - delete: 'Delete', - edit: 'Edit', - add: 'Add', - search: 'Search', - reset: 'Reset', - submit: 'Submit', - back: 'Back', - loading: 'Loading...', - success: 'Operation successful', - failed: 'Operation failed', - required: 'This field is required' - }, - - // Login - login: { - title: 'Qingyuan Power Laos ERP', - subtitle: 'Project Management and Finance Platform', - username: 'Username', - password: 'Password', - loginButton: 'Login', - usernamePlaceholder: 'Please enter username', - passwordPlaceholder: 'Please enter password', - usernameRequired: 'Please enter username', - passwordRequired: 'Please enter password', - usernameMin: 'Username must be at least 3 characters', - passwordMin: 'Password must be at least 6 characters', - loginFailed: 'Login failed, please try again', - testAccounts: 'Test Accounts', - techSupport: 'Technical Support: OpenClaw AI + React + Node.js', - selectLanguage: 'Select Language' - }, - - // Menu - menu: { - dashboard: 'Dashboard', - projects: 'Project Management', - advances: 'Advance Management', - reimbursements: 'Reimbursement Management', - finance: 'Finance Management', - reports: 'Reports', - settings: 'System Settings' - }, - - // User - user: { - profile: 'Profile', - settings: 'System Settings', - logout: 'Logout', - admin: 'System Administrator', - finance: 'Finance Specialist', - manager: 'Project Manager', - employee: 'Employee' - }, - - // Features - features: { - projectManage: 'Project Management: Create, track, and analyze project progress', - advanceManage: 'Advance Management: Application and approval process', - reimburseManage: 'Reimbursement Management: Expense claim process', - financeReport: 'Financial Reports: Project cost and profit analysis', - mobileSupport: 'Mobile Support: PWA technology, add to home screen' - } -} +export default { + common: { + confirm: 'Confirm', + cancel: 'Cancel', + save: 'Save', + delete: 'Delete', + edit: 'Edit', + add: 'Add', + search: 'Search', + reset: 'Reset', + submit: 'Submit', + back: 'Back', + loading: 'Loading...', + success: 'Operation successful', + failed: 'Operation failed', + required: 'This field is required', + close: 'Close', + view: 'View', + refresh: 'Refresh', + create: 'Create', + upload: 'Upload', + download: 'Download', + export: 'Export', + import: 'Import', + copy: 'Copy', + detail: 'Details', + status: 'Status', + action: 'Action', + name: 'Name', + remark: 'Remark', + date: 'Date', + amount: 'Amount', + total: 'Total', + unit: 'pcs', + meter: 'meter', + currency: 'Currency', + country: 'Country', + phone: 'Phone', + email: 'Email', + address: 'Address', + position: 'Position', + is: 'Yes', + no: 'No', + days: 'days', + tenThousand: '10K', + yuan: 'yuan', + sheet: 'sheet(s)', + item: 'item(s)', + photo: 'Photo', + person: 'person(s)', + today: 'Today', + unknown: 'Unknown', + none: 'None', + all: 'All', + retry: 'Retry', + inputPassword: 'Please enter password', + deleteConfirm: 'Confirm Delete', + deleteWarning: 'This action cannot be undone', + draftFound: 'Unsaved draft found', + draftRestore: 'Detected previously unsaved data. Restore?', + restoreDraft: 'Restore Draft', + reFill: 'Start Over', + closeConfirm: 'Confirm Close', + closeConfirmMsg: 'Form data has not been saved. Closing will allow draft recovery later. Confirm close?', + continueEdit: 'Continue Editing', + noData: 'No data', + loadingData: 'Loading project data...', + noProjectData: 'No project data', + operationFailed: 'Operation failed', + saveFailed: 'Save failed', + deleteFailed: 'Delete failed', + networkError: 'Network error, operation failed', + totalCount: 'Total {total} records', + systemAdmin: 'System Admin', + currentUser: 'Current User', + unnamed: 'Unnamed', + notSet: 'Not set', + pleaseSelect: 'Please select', + inputPlaceholder: 'Please enter', + selectPlaceholder: 'Select', + confirmDelete: 'Confirm delete?', + confirmDeleteMsg: 'Are you sure you want to delete?', + saveSuccess: 'Saved successfully', + createSuccess: 'Created successfully', + deleteSuccess: 'Deleted successfully', + updateSuccess: 'Updated successfully', + }, + + login: { + title: 'Qingyuan Power Lao ERP', + subtitle: 'Integrated Project Management & Finance Reimbursement Platform', + username: 'Username', + password: 'Password', + loginButton: 'Login', + usernamePlaceholder: 'Please enter username', + passwordPlaceholder: 'Please enter password', + usernameRequired: 'Please enter username', + passwordRequired: 'Please enter password', + usernameMin: 'Username must be at least 3 characters', + passwordMin: 'Password must be at least 6 characters', + loginFailed: 'Login failed, please try again', + testAccounts: 'Test Accounts', + techSupport: 'Technical Support: OpenClaw AI Assistant + React + Node.js', + selectLanguage: 'Select Language', + passwordError: 'Incorrect username or password', + serverError: 'Internal server error, please try again later', + statusCodeError: 'Login failed (status code: {code})', + }, + + menu: { + dashboard: 'Dashboard', + projects: 'Project Management', + budgetQuotation: 'Budget & Quotation', + construction: 'Construction', + constructionOverview: 'Construction Overview', + approval: 'Approval', + pendingApproval: 'Pending Approval', + pendingExecution: 'Pending Execution', + financeDocs: 'Finance Applications', + advanceApply: 'Advance Application', + reimburseApply: 'Reimbursement Application', + paymentApply: 'Payment Application', + verificationApply: 'Verification Application', + financeManagement: 'Finance Management', + financeOverview: 'Finance Overview', + exchangeRate: 'Exchange Rate', + projectCost: 'Project Cost', + advanceVerificationStatus: 'Advance Verification Status', + reports: 'Reports & Analytics', + procurement: 'Procurement', + productManagement: 'Product Management', + purchaseRequest: 'Purchase Request', + purchaseOrder: 'Purchase Order', + paymentPlan: 'Payment Plan', + inventory: 'Inventory', + partners: 'Partners', + supplierManagement: 'Supplier Management', + subcontractorManagement: 'Subcontractor Management', + customerManagement: 'Customer Management', + logisticsManagement: 'Logistics Management', + admin: 'Admin', + userManagement: 'User Management', + rolePermission: 'Roles & Permissions', + processManagement: 'Process Management', + templateManagement: 'Project Templates', + expenseCategory: 'Expense Categories', + excelImport: 'Excel Import', + systemLogs: 'System Logs', + dataBackup: 'Data Backup', + aboutSystem: 'About', + profile: 'Profile', + settings: 'System Settings', + logout: 'Logout', + backToFront: 'Back to Frontend', + collapse: 'Collapse Menu', + }, + + user: { + profile: 'Profile', + settings: 'System Settings', + logout: 'Logout', + admin: 'Admin', + finance: 'Finance Specialist', + manager: 'Project Manager', + employee: 'Employee', + userLabel: 'User', + role: 'Role', + managingProfile: 'Manage account information', + name: 'Name', + phone: 'Phone', + email: 'Email', + username: 'Username', + clickToChangeAvatar: 'Click to change avatar', + idDocument: 'ID Documents', + idDocTip: 'Passport and driver license photos. Click image to zoom, click "Replace" to upload new photos.', + passport: 'Passport', + driverLicense: 'Driver License', + uploadPassport: 'Click to upload passport photo', + uploadDriverLicense: 'Click to upload driver license photo', + deletePassportConfirm: 'Confirm delete passport photo?', + deleteDriverLicenseConfirm: 'Confirm delete driver license photo?', + saveProfile: 'Save Changes', + changePassword: 'Change Password', + currentPassword: 'Current Password', + currentPasswordPlaceholder: 'Please enter current password', + newPassword: 'New Password', + newPasswordPlaceholder: 'Please enter new password', + confirmPassword: 'Confirm New Password', + confirmPasswordPlaceholder: 'Please confirm new password', + passwordMinLen: 'Password must be at least 6 characters', + passwordMismatch: 'Passwords do not match', + profileUpdated: 'Profile has been updated', + passwordUpdated: 'Password has been updated', + updateFailed: 'Update failed', + updateRetry: 'Update failed, please try again', + passwordUpdateFailed: 'Password update failed', + passwordUpdateRetry: 'Password update failed, please try again', + replace: 'Replace', + uploadFailed: 'Upload failed', + }, + + features: { + projectManage: 'Project Management: Create, track, and analyze project progress', + advanceManage: 'Advance Management: Employee advance application and approval process', + reimburseManage: 'Reimbursement Management: Expense reimbursement and settlement process', + financeReport: 'Financial Reports: Project cost and profit analysis', + mobileSupport: 'Mobile Support: PWA technology, add to home screen', + }, + + dashboard: { + title: '📊 Dashboard', + planning: 'Planning', + inProgress: 'In Progress', + completed: 'Completed', + projectName: 'Project Name', + budget: 'Budget', + spent: 'Spent', + project: 'Project', + budgetSpent: 'Budget / Spent', + budgetLabel: 'Budget: ', + spentLabel: 'Spent: ', + inProgressProjects: 'Projects In Progress', + monthlyReimburse: 'Monthly Reimbursement', + pendingApproval: 'Pending Approval', + teamMembers: 'Team Members', + recentProjects: 'Recent Projects', + }, + + project: { + title: 'Project Management', + description: 'Manage project information, progress, and budget', + list: 'Project List', + quickCreate: 'Quick Create Project', + newProject: 'New Project', + editProject: 'Edit Project', + deleteProject: 'Delete Project', + deleteConfirm: 'Delete Confirmation', + deleteConfirmMsg: 'Are you sure you want to delete this project? This action cannot be undone.', + deletePassMsg: 'Please enter admin password to confirm deletion:', + projectName: 'Project Name', + projectNamePlaceholder: 'e.g., Vientiane Xaythany District 22kV Line Project', + projectTemplate: 'Project Template', + selectTemplate: 'Select project template (optional)', + projectManager: 'Project Manager', + budget: 'Budget', + progress: 'Progress', + status: 'Status', + planning: 'Planning', + inProgress: 'In Progress', + completed: 'Completed', + paused: 'Paused', + plan: 'Plan', + complete: 'Complete', + pause: 'Pause', + customer: 'Customer', + selectCustomer: 'Select Customer', + selectManager: 'Select Project Manager', + contractAmount: 'Contract Amount', + projectStatus: 'Project Status', + completedHistory: 'Completed (Historical Record)', + startDate: 'Start Date', + endDate: 'End Date', + location: 'Project Location', + locationPlaceholder: 'e.g., Vientiane Province, Laos', + descriptionPlaceholder: 'Brief description of the project', + createSuccess: 'Project created successfully', + createFailed: 'Creation failed', + deleteSuccess: 'Project deleted successfully', + getListFailed: 'Failed to get project list', + unassigned: 'Unassigned', + passwordError: 'Incorrect password', + projectCode: 'Project Code', + basicInfo: 'Basic Information', + contractDetails: 'Contract & Receivables', + financeDetails: 'Financials', + editBasicInfo: 'Edit Project Basic Info', + basicInfoSaved: 'Basic info saved successfully', + selectStartDate: 'Please select start date', + selectEndDate: 'Please select end date', + durationDays: 'Duration (Days)', + durationDaysPlaceholder: 'Please enter duration in days', + overview: 'Project Overview', + overviewPlaceholder: 'Please enter project overview', + createdAt: 'Created At', + returnToList: 'Back to List', + unknownManager: 'Unknown Manager', + notFound: 'Project not found or has been deleted', + getInfoFailed: 'Failed to get project info', + enterConstruction: 'Enter Construction', + contractNo: 'Contract No.', + contractType: 'Contract Type', + unitPriceContract: 'Unit Price Contract', + includeTax: 'Tax Included', + settlementType: 'Settlement Type', + lumpSum: 'Lump Sum', + unitPrice: 'Unit Price Settlement', + contractTotal: 'Contract Total', + contractTotalPlaceholder: 'Please enter contract total', + contractTax: 'Contract Tax Included', + paymentMilestones: 'Payment Milestones', + milestoneName: 'Milestone Name', + milestoneCondition: 'Milestone Condition', + milestoneRatio: 'Ratio (%)', + milestoneAmount: 'Amount', + milestoneStatus: 'Completion Progress', + pendingMilestone: 'Pending', + noMilestone: 'No payment milestones', + noMilestoneRecord: 'No milestone records', + addMilestone: 'Add Payment Milestone', + milestoneNotReached: 'Milestone not reached', + contractAttachment: 'Contract Attachment', + contractFile: 'Contract File', + viewContract: 'View Contract File', + noContractAttachment: 'No contract attachment', + otherContractInfo: 'Other Contract Information', + otherInfo: 'Other Information', + otherInfoPlaceholder: 'Please enter other contract-related information', + warranty: 'Warranty Bond Settings', + hasWarranty: 'Has Warranty Bond', + warrantyRatio: 'Warranty Bond Ratio', + warrantyAmount: 'Warranty Bond Amount', + warrantyPeriod: 'Warranty Period', + warrantyExpiry: 'Expiry Date', + warrantyStatus: 'Warranty Bond Status', + warrantyReleased: 'Released', + warrantyPending: 'Pending Release', + contractSaveSuccess: 'Contract details saved successfully', + draft: 'Draft', + replaceFile: 'Replace File', + clickUpload: 'Click to Upload', + fileUploadFailed: 'File upload failed', + subcontract: 'Subcontract Management', + addSubcontract: 'Add Subcontract', + subcontractor: 'Subcontractor', + subcontractorName: 'Subcontractor Name', + subcontractorNamePlaceholder: 'Please enter subcontractor name', + paidAmount: 'Paid Amount', + noSubcontract: 'No subcontract records', + subcontractDetail: 'Subcontract Details', + startDateRequired: 'Please select start date', + endDateRequired: 'Please select end date', + otherTerms: 'Other Terms', + otherTermsPlaceholder: 'Please enter other terms', + paymentNote: 'Payment Note', + paymentNotePlaceholder: 'Please enter payment note', + addSubSuccess: 'Subcontract added successfully', + addSubFailed: 'Failed to add subcontract, please check form data', + projectItems: 'Project Line Items', + quantity: 'Quantity', + unitPriceLabel: 'Unit Price', + totalPrice: 'Total Price', + addItem: '+ Add Line Item', + noItems: 'No line items', + material: 'Material Management', + materialName: 'Material Name', + budgetQty: 'Budget Qty', + purchaseQty: 'Purchase Qty', + usedQty: 'Used Qty', + avgPrice: 'Avg Price', + noMaterial: 'No material records', + constructionNode: 'Construction Node', + contractMilestone: 'Contract Payment Milestone', + milestoneDesc: 'Key milestone completion status', + plannedDate: 'Planned Date', + actualDate: 'Actual Date', + uploadProof: 'Upload Proof', + noRecord: 'No records', + constructionLog: 'Construction Log', + addLog: 'Add Log', + weather: 'Weather', + recorder: 'Recorder', + todayWork: 'Today\'s Work', + viewPhoto: 'View Photo', + noLog: 'No log records', + finance: 'Financial Information', + received: 'Received', + totalExpense: 'Total Expense', + grossProfit: 'Gross Profit', + marginRate: 'Margin Rate', + addReceipt: 'Add Receipt', + receiptRecords: 'Receipt Records', + noReceipts: 'No receipt records', + receiptType: 'Receipt Type', + receiptTypeNode: 'Node Payment', + receiptTypeAdvance: 'Client Advance', + receiptTypeOther: 'Other Receipt', + receiptDate: 'Receipt Date', + receiptNode: 'Linked Node', + receiptAmount: 'Amount', + receiptAmountCNY: 'Amount (CNY)', + receiptDesc: 'Description', + receiptDescPlaceholder: 'Enter receipt description', + receiptAdded: 'Receipt added successfully', + selectMilestone: 'Payment Milestone', + selectMilestonePlaceholder: 'Select payment milestone', + selectMilestoneRequired: 'Please select a payment milestone', + payer: 'Payer', + payerPlaceholder: 'Enter payer name', + exchangeRate: 'Exchange Rate', + voucher: 'Voucher', + uploadVoucher: 'Upload Voucher', + viewVoucher: 'View', + expenseBreakdown: 'Expense Breakdown', + category: 'Category', + categoryAmount: 'Amount (¥)', + count: 'Count', + ratio: 'Ratio', + personnelExpense: 'Personnel Expense Detail', + personnel: 'Personnel', + warrantyManagement: 'Warranty Bond Management', + warrantyStartDate: 'Start Date', + markReleased: 'Mark Released', + extendWarranty: 'Extend', + currentLabel: 'Current: ', + progressLabel: 'Progress: ', + warrantyLabel: 'Warranty Bond: ', + currencyCNY: 'RMB', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + amountRequired: 'Please enter contract amount', + settlementRequired: 'Please select settlement type', + nodeNameRequired: 'Please enter node name', + nodeConditionRequired: 'Please enter node condition', + ratioRequired: 'Please enter ratio', + milestoneAmountRequired: 'Please enter amount', + statusRequired: 'Please select status', + }, + + construction: { + overview: 'Construction Overview', + noProjects: 'No construction projects', + underConstruction: 'Under Construction', + pendingStart: 'Pending Start', + completed: 'Completed', + paused: 'Paused', + currentPhase: 'Current Phase: ', + completedProjects: 'Completed Projects: ', + enter: 'Enter', + getListFailed: 'Failed to get project list', + progress: 'Construction Progress', + todayLog: 'Today\'s Log: ', + todayLogEmpty: 'Today\'s Log: Not filled', + writeLog: 'Write Today\'s Log', + constructionLog: 'Construction Log', + uploadPhoto: 'Upload Photo', + milestoneProgress: 'Milestone Progress', + management: 'Construction Management', + description: 'View and manage your construction projects', + noConstructionProjects: 'No construction projects', + contactAdmin: 'Please contact admin to assign construction projects', + myProjects: 'My Construction Projects', + customerLabel: 'Customer: ', + getInfoFailed: 'Failed to get project info', + getPhaseFailed: 'Failed to get phase info', + phaseComplete: 'Phase complete! Progress: ', + advancedTo: 'Advanced to: ', + reopenPhase: 'Reopen Phase', + reopenConfirm: 'Are you sure you want to reopen this phase? Project progress will be rolled back.', + phaseReopened: 'Phase has been reopened', + updateItemFailed: 'Failed to update item', + returnOverview: 'Back to Overview', + currentLabel: 'Current: ', + parallelPhase: 'Parallel Phase (can proceed simultaneously with other phases)', + completionStandard: 'Completion Standard: ', + remarkOptional: 'Remark (optional): ', + remarkPlaceholder: 'Enter completion remarks...', + confirmCompleteMsg: 'Confirm completion of this phase', + subsequentPhases: 'Subsequent Phases', + parallelLabel: 'Parallel', + phaseHistory: 'Phase History', + rollback: 'Rollback', + projectCompleted: 'Project Completed', + projectCompletedDesc: 'This project is completed. No further construction phase advancement needed.', + viewProjectDetail: 'View Project Details', + notInitialized: 'This project has not initialized construction phases', + notInitializedDesc: 'Please select a project template in project details to initialize construction phases', + goToProjectDetail: 'Go to Project Details', + confirmCompleteTitle: 'Confirm Phase Completion', + confirmCompleteDesc: 'Confirm this phase is completed? System will automatically advance to the next phase.', + remarkLabel: 'Remarks: ', + uploadProofLabel: 'Upload proof materials (photos/files): ', + selectFile: 'Select File', + supportFormats: 'Supports photos, PDF, Word, Excel and other formats', + sunny: 'Sunny', + cloudy: 'Cloudy', + rain: 'Rain', + thunderstorm: 'Thunderstorm', + windy: 'Windy', + getLogFailed: 'Failed to get log list', + logAddSuccess: 'Log added successfully', + logAddFailed: 'Failed to add log', + logDeleteSuccess: 'Log deleted successfully', + logDeleteFailed: 'Failed to delete log', + yearMonth: 'YYYY-MM', + monthDay: 'MM-DD', + recorderLabel: 'Recorder: ', + deleteLogConfirmTitle: 'Confirm delete this log?', + deleteLogConfirmDesc: 'This cannot be undone', + todayWorkLabel: 'Today\'s Work:', + tomorrowPlanLabel: 'Tomorrow\'s Plan:', + issueRecordLabel: 'Issue Record:', + constructionPhoto: 'Construction Photo', + noLog: 'No construction logs', + addFirstLog: 'Add first log entry', + newLog: 'New Log', + newConstructionLog: 'New Construction Log', + selectDate: 'Please select date', + selectWeather: 'Please select weather', + inputTodayWork: 'Please enter today\'s work content', + todayWorkPlaceholder: 'Describe the construction work completed today...', + tomorrowPlanPlaceholder: 'Tomorrow\'s work plan...', + issuePlaceholder: 'Issues encountered or items needing coordination...', + addPhoto: 'Add Photo', + multiPhotoSupport: 'Supports multiple photos, up to 9', + plannedComplete: 'Planned Completion: ', + overallProgress: 'Overall Progress', + totalNodes: 'Total Nodes', + noMilestones: 'No construction nodes', + milestoneConfigured: 'Nodes are configured by project manager in project settings', + inProgress: 'In Progress', + cancelled: 'Cancelled', + progressTab: 'Progress', + documentsTab: 'Documents', + logsTab: 'Construction Log', + markComplete: 'Mark Complete', + phasesCompleted: 'phases completed', + initPhases: 'Initialize Phases', + selectTemplateInit: 'Select Template & Initialize', + selectTemplate: 'Please select a template', + initSuccess: 'Phases initialized successfully', + completedAt: 'Completed at', + completionTime: 'Completion Time', + proofPhotos: 'Proof Photos', + optional: 'optional', + uploadImage: 'Upload Image', + uploadDocument: 'Upload Document', + imageDocs: 'Images', + fileDocs: 'Documents', + noImages: 'No images yet', + noDocuments: 'No documents yet', + fileName: 'File Name', + uploader: 'Uploader', + uploadTime: 'Upload Time', + descriptionPlaceholder: 'Enter description', + clickUpload: 'Click to upload', + uploadSuccess: 'Upload successful', + addLog: 'Add Log', + logAdded: 'Log added successfully', + logDate: 'Log Date', + weather: 'Weather', + weatherSunny: 'Sunny', + weatherCloudy: 'Cloudy', + weatherRainy: 'Rainy', + weatherStormy: 'Stormy', + weatherWindy: 'Windy', + workContent: 'Work Content', + workContentPlaceholder: 'Enter today\'s work content', + nextPlan: 'Next Plan', + nextPlanPlaceholder: 'Enter tomorrow\'s plan', + issues: 'Issues', + issuesPlaceholder: 'Enter any issues encountered', + sitePhotos: 'Site Photos', + noLogs: 'No construction logs yet', + customer: 'Customer', + manager: 'Project Manager', + }, + + budget: { + title: 'Budget & Quotation Management', + description: 'Manage negotiation projects and quotation versions', + newProject: 'New Negotiation Project', + statusFilter: 'Status Filter:', + inNegotiation: 'In Negotiation', + signed: 'Signed', + unsigned: 'Unsigned', + draft: 'Draft', + sent: 'Sent', + approved: 'Passed', + rejected: 'Rejected', + deleteConfirm: 'Delete Confirmation', + deleteConfirmMsg: 'Are you sure you want to delete this budget project? This action cannot be undone.', + deletePassMsg: 'Please enter admin password to confirm deletion:', + deletePass: 'Please enter admin password', + getDataFailed: 'Failed to get data', + deleteSuccess: 'Deleted successfully', + passwordError: 'Incorrect password', + customerLabel: 'Customer: ', + managerLabel: 'Business Manager: ', + intermediaryLabel: 'Intermediary: ', + intermediaryFee: 'Intermediary Fee: ', + versionDeleteConfirm: 'Are you sure you want to delete this quotation version? This action cannot be undone.', + createTitle: 'New Negotiation Project', + createDesc: 'Create a new negotiation project and add basic information', + basicInfo: 'Basic Information', + projectName: 'Project Name', + projectNamePlaceholder: 'Please enter project name', + customer: 'Customer', + selectCustomer: 'Please select customer', + businessManager: 'Business Manager', + selectManager: 'Please select business manager', + unknownDept: 'Unknown Department', + projectLocation: 'Project Location', + locationPlaceholder: 'Please enter project location', + surveyDate: 'Survey Date', + intermediary: 'Intermediary Information', + intermediaryName: 'Intermediary Name', + intermediaryNamePlaceholder: 'Please enter intermediary name', + intermediaryType: 'Intermediary Fee Type', + fixedAmount: 'Fixed Amount', + percentage: 'Percentage', + intermediaryRatio: 'Intermediary Fee Ratio (%)', + intermediaryRatioPlaceholder: 'Enter ratio, e.g., 5', + intermediaryAmount: 'Intermediary Fee Amount', + intermediaryAmountPlaceholder: 'Enter amount', + projectDetail: 'Project Details', + customerRequirement: 'Customer Requirements', + requirementPlaceholder: 'Please enter customer specific requirements', + overview: 'Project Overview', + overviewPlaceholder: 'Please enter project overview description', + attachment: 'Attachment', + attachmentUpload: 'Attachment Upload', + surveyPhoto: 'Survey Photos', + noAccess: 'You do not have permission to access this page', + createSuccess: 'Created successfully', + createFailed: 'Creation failed', + leaveConfirm: 'Confirm Leave', + leaveConfirmMsg: 'Form data has not been saved. Leaving will allow draft recovery later. Confirm leave?', + leave: 'Leave', + continueEdit: 'Continue Editing', + return: 'Back', + signedSuccess: 'Marked as unsigned successfully', + notFound: 'Project not found', + quotationVersions: 'Quotation Versions', + addVersion: 'Add Quotation Version', + versionDate: 'Quotation Date: ', + versionAmount: 'Quotation Amount: ', + versionRemark: 'Remark: ', + noVersion: 'No quotation versions', + markSigned: 'Mark Signed', + markUnsigned: 'Mark Unsigned', + enterProject: 'Enter Project Management', + deleteProject: 'Delete Project', + detailTitle: 'Budget Project Details', + detailDesc: 'View project details and quotation versions', + projectInfo: 'Project Information', + photos: 'Photos ', + noPhotos: 'No survey photos', + noAttachment: 'No attachments', + quickSign: 'Quick Sign', + quickSignSuccess: 'Contract signed successfully, project auto-created', + contractNo: 'Contract No.', + contractNoPlaceholder: 'Please enter contract number', + contractType: 'Contract Type', + selectContractType: 'Please select contract type', + totalPrice: 'Total Price', + totalPricePlaceholder: 'Please enter total price', + durationDays: 'Duration (Days)', + durationPlaceholder: 'Please enter duration', + durationDaysPlaceholder: 'Please enter duration (days)', + quickSignNote: 'Note: This is a quick signing process. Only basic info is recorded. Detailed contract info can be added in project management.', + newQuotation: 'New Quotation Version', + quotationDate: 'Quotation Date', + selectDate: 'Please select quotation date', + quotationAmount: 'Quotation Amount', + amountPlaceholder: 'Please enter quotation amount', + quotationFile: 'Quotation File', + viewFile: 'View File', + uploadFile: 'Upload File', + remarkPlaceholder: 'Please enter remarks', + uploadSuccess: 'Upload successful', + version: 'Current Version: ', + projectLabel: 'Project Name: ', + }, + + finance: { + title: 'Finance Management', + addRecord: 'Add Finance Record', + addRecordBtn: 'Add Record', + exportExcel: 'Export Excel', + totalIncome: 'Total Income', + totalExpense: 'Total Expense', + netProfit: 'Net Profit', + expenseSummary: 'Expense Category Summary', + detail: 'Finance Details', + filterType: 'Filter Type', + date: 'Date', + incomeType: 'Income/Expense Type', + level1Category: 'Level 1 Category', + level2Category: 'Level 2 Category', + projectName: 'Project Name', + amount: 'Amount', + currency: 'Currency', + exchangeRate: 'Exchange Rate', + equivalentCNY: 'Equivalent RMB', + counterpartyName: 'Counterparty Name', + counterpartyType: 'Counterparty Type', + personName: 'Person Name', + desc: 'Description', + voucherNo: 'Voucher No.', + selectDate: 'Please select date', + selectIncomeType: 'Please select', + selectLevel1: 'Please select', + selectLevel2: 'Please select level 1 category first', + selectProject: 'Select Project', + amountPlaceholder: '0', + counterpartyPlaceholder: 'Payee/Payer name', + selectType: 'Select Type', + personNamePlaceholder: 'Related employee name', + descPlaceholder: 'Additional notes', + voucherPlaceholder: 'Invoice/Receipt number', + income: 'Income', + expense: 'Expense', + projectExpense: 'Project Expense', + companyExpense: 'Company Expense', + incomeCategory: 'Income', + manual: 'Manual', + advance: 'Advance', + reimbursement: 'Reimbursement', + payment: 'Payment', + material: 'Material', + freight: 'Freight', + source: 'Source', + recordSuccess: 'Recorded successfully', + exporting: 'Exporting...', + exportSuccess: 'Export successful', + exportFailed: 'Export failed', + sheetName: 'Finance Ledger', + totalRecords: 'Total {total} records', + currencyCNY: 'CNY (RMB)', + currencyLAK: 'LAK (Lao Kip)', + currencyUSD: 'USD (US Dollar)', + currencyTHB: 'THB (Thai Baht)', + counterpartySupplier: 'Supplier', + counterpartySubcontractor: 'Subcontractor', + counterpartyCustomer: 'Customer', + counterpartyEmployee: 'Employee', + counterpartyLogistics: 'Logistics Company', + counterpartyShareholder: 'Shareholder', + counterpartyOther: 'Other', + projectRevenue: 'Project Contract Receipts', + warrantyReturn: 'Warranty Bond Return', + shareholderInvestment: 'Shareholder Investment', + otherIncome: 'Other Income', + materialPurchase: 'Material Purchase', + equipmentPurchase: 'Equipment Purchase', + constructionSubcontract: 'Construction Subcontract', + laborWage: 'Labor Wages', + travelTransport: 'Travel & Transport', + accommodationFood: 'Accommodation & Food', + transportLogistics: 'Transport Logistics', + surveyDesign: 'Survey & Design', + smallTools: 'Small Tools', + customerEDLRelation: 'Customer/EDL Relations', + otherProjectExpense: 'Other Project Expenses', + salaryWelfare: 'Salary & Benefits', + rentProperty: 'Rent & Property', + officeExpense: 'Office Expenses', + commute: 'Commute', + vehicleMaintenance: 'Vehicle Maintenance', + fixedAsset: 'Fixed Assets', + marketing: 'Marketing', + entertainment: 'Entertainment', + employeeBenefit: 'Employee Benefits', + expressLogistics: 'Express Logistics', + otherCompanyExpense: 'Other Company Expenses', + }, + + cash: { + tabOverview: 'Overview', + tabIncome: 'Income Entry', + tabExpense: 'Expense Entry', + addIncome: 'Add Income', + addExpense: 'Add Expense', + financeExpense: 'Finance Expense', + customerAdvance: 'Customer Advance/Loan', + bankLoan: 'Bank Loan', + otherLoan: 'Other Loan', + dividendIncome: 'Dividend Income', + interestIncome: 'Interest Income', + assetDisposal: 'Asset Disposal', + taxRefund: 'Tax Refund', + governmentSubsidy: 'Government Subsidy', + loanRepayment: 'Loan Repayment', + interestExpense: 'Interest Expense', + dividendPayment: 'Dividend Payment', + taxPayment: 'Tax Payment', + depositPayment: 'Deposit/Pledge', + ownerExpense: 'Owner Expense', + otherFinance: 'Other Finance Expense', + sourceLabel: 'Cash Management', + receiptSource: 'Project Receipt', + counterpartyBank: 'Bank', + counterpartySelect: 'Select Counterparty', + counterpartySelectPlaceholder: 'Select counterparty', + voucherUpload: 'Voucher Upload', + uploadVoucher: 'Upload Voucher', + uploadSuccess: 'Upload success', + uploadFailed: 'Upload failed', + }, + + reports: { + title: 'Reports & Analytics', + description: 'View project financial reports and statistical analysis data', + totalIncome: 'Total Income', + totalExpense: 'Total Expense', + netProfit: 'Net Profit', + monthlyReport: 'Monthly Financial Report', + selectMonth: 'Select Month', + month: 'Month', + incomeCategory: 'Income Categories', + projectExpenseCategory: 'Project Expense Categories', + companyExpenseCategory: 'Company Expense Categories', + categoryTag: 'Income', + projectTag: 'Project', + companyTag: 'Company', + }, + + paymentRequest: { + title: 'Payment Request', + description: 'Manage external payment requests', + newRequest: 'New Payment Request', + activeApplications: 'Active Applications', + completed: 'Completed', + subject: 'Subject', + applicant: 'Applicant', + payee: 'Payee', + amount: 'Amount', + applicationDate: 'Application Date', + status: 'Status', + code: 'Code', + action: 'Action', + edit: 'Edit', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + delete: 'Delete', + deleteSuccess: 'Deleted successfully', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + editPayment: 'Edit Payment Request', + newPayment: 'New Payment Request', + expenseType: 'Expense Type', + selectExpenseType: 'Select Expense Type', + relatedProject: 'Related Project', + selectProject: 'Select Project', + expenseCategory: 'Expense Category', + selectCategory: 'Select Expense Category', + payeeType: 'Payee Type', + selectPayeeType: 'Select Payee Type', + selectSubcontractor: 'Select Subcontractor', + selectSupplier: 'Select Supplier', + selectCustomer: 'Select Customer', + payeeName: 'Payee Name', + payeeNamePlaceholder: 'Manually enter payee name', + accountName: 'Account Name', + accountNamePlaceholder: 'Account name (auto-filled when selecting subcontractor/supplier/customer)', + bankAccount: 'Bank Account', + bankAccountPlaceholder: 'Bank account number (auto-filled when selecting subcontractor/supplier/customer)', + bankName: 'Bank Name', + bankNamePlaceholder: 'Bank name (auto-filled when selecting subcontractor/supplier/customer)', + qrCode: 'QR Code', + paymentAmount: 'Payment Amount', + paymentAmountPlaceholder: 'Enter payment amount', + paymentReason: 'Payment Reason', + paymentReasonPlaceholder: 'Reason for payment', + uploadProof: 'Upload Proof Attachment', + proofAttachment: 'Proof Attachment', + equivalentCNY: 'Equivalent RMB: ¥ ', + getListFailed: 'Failed to get payment request list', + deleteFailed: 'Delete failed', + operationSuccess: 'Operation successful', + detailTitle: 'Payment Request Details', + applicationCode: 'Application Code', + pendingApproval: 'Pending Approval', + approved: 'Approved', + rejected: 'Rejected', + withdrawn: 'Withdrawn', + paid: 'Paid', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + counterpartySubcontractor: 'Subcontractor', + counterpartySupplier: 'Supplier', + counterpartyCustomer: 'Customer', + counterpartyOther: 'Other', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + deleteConfirmMsg: 'Confirm delete this payment request?', + }, + + paymentPlan: { + title: 'Payment Plan', + description: 'Manage payment plans for purchase orders', + newPlan: 'New Payment Plan', + editPlan: 'Edit Payment Plan', + planCode: 'Plan Code', + purchaseOrder: 'Purchase Order', + paymentDate: 'Payment Date', + amount: 'Amount', + paymentType: 'Payment Type', + status: 'Status', + creator: 'Creator', + action: 'Action', + pending: 'Pending', + approved: 'Approved', + executed: 'Executed', + cancelled: 'Cancelled', + partialPayment: 'Partial Payment', + fullPayment: 'Full Payment', + selectOrder: 'Please select purchase order', + selectDate: 'Please select payment date', + inputAmount: 'Please enter payment amount', + selectCurrency: 'Please select currency', + selectType: 'Please select payment type', + selectStatus: 'Please select status', + inputCreator: 'Please enter creator', + amountPlaceholder: 'Payment amount', + descPlaceholder: 'Please enter payment plan description', + detailTitle: 'Payment Plan Details', + detailCode: 'Plan Code', + currencyCNY: 'RMB', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + getListFailed: 'Failed to get payment plan list', + getDetailFailed: 'Failed to get payment plan details', + }, + + verification: { + title: 'Verification Application', + description: 'Advance fund verification/settlement', + newVerification: 'New Verification', + editVerification: 'Edit Verification', + subject: 'Subject', + applicant: 'Applicant', + relatedAdvance: 'Related Advance', + amount: 'Amount', + verificationDate: 'Verification Date', + status: 'Status', + code: 'Code', + action: 'Action', + detail: 'Details', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + delete: 'Delete', + addDetail: 'Add Detail', + advanceAmount: 'Advance Amount', + settlementOptions: 'Settlement Options', + settlementAmount: 'Settlement Amount', + expenseType: 'Expense Type', + selectProject: 'Select Project', + subjectPlaceholder: 'Verification reason description', + detailLabel: 'Verification Details', + expenseDescription: 'Expense Description', + expenseCategory: 'Expense Category', + detailAmount: 'Amount', + attachment: 'Proof Attachment', + mainAttachment: 'Main Attachment', + refundProof: 'Refund Proof (Required)', + overallAttachment: 'Overall Proof Attachment', + selectAdvance: 'Please select related advance', + selectAdvanceOrInput: 'Select or enter advance code', + selectProjectRequired: 'Please select project', + uploadRefundRequired: 'Please upload refund proof', + finalSettlement: 'Final Settlement', + verifiedAmount: 'Verified Amount: ', + remainingAmount: 'Remaining Amount: ', + refundLabel: 'Refund ¥{amount}', + supplementLabel: 'Supplement ¥{amount}', + totalLabel: 'Total: ', + refundNote: '* Refund-type settlement verification must upload refund proof', + unknownProject: 'Unknown Project', + advanceInfo: 'Advance Information', + advanceCode: 'Advance Code', + advanceTotalAmount: 'Advance Amount', + advanceVerified: 'Verified Amount', + advanceRemaining: 'Remaining Amount', + detailTitle: 'Verification Details', + attachmentCount: '{count} sheets', + isSettlement: 'Yes', + notSettlement: 'No', + refundText: 'Refund ', + supplementText: 'Supplement ', + pendingApproval: 'Pending Approval', + approved: 'Approved', + rejected: 'Rejected', + withdrawn: 'Withdrawn', + paid: 'Paid', + pendingEdit: 'Pending Edit', + deleteSuccess: 'Deleted successfully', + deleteFailed: 'Delete failed', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + saveSuccess: 'Saved successfully', + createSuccess: 'Created successfully', + submitSuccess: 'Submitted successfully', + getListFailed: 'Failed to get verification list', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + deleteConfirmMsg: 'Are you sure you want to delete this verification record?', + project: 'Related Project', + projectPlaceholder: 'Select project', + category: 'Expense Category', + categoryPlaceholder: 'Select expense category', + categoryLabel: 'Category: ', + categoryRequired: 'Please select expense category', + verificationAmount: 'Verification Amount', + settlementType: 'Settlement Type', + settlement: 'Settlement', + settlementInfo: 'Settlement Info', + nonSettlement: 'Non-Settlement', + expenseDetail: 'Expense Details', + totalItems: 'Total {count} items', + paymentProof: 'Payment Proof', + receipt: 'Receipt', + addExpense: 'Add Expense', + editExpense: 'Edit Expense', + edit: 'Edit', + viewDetail: 'View Details', + refund: 'Refund', + supplement: 'Supplement', + inputAmount: 'Please enter amount', + amountPlaceholder: 'Enter amount', + equivalentCNY: 'Equivalent RMB: ¥', + totalAmount: 'Total: ¥{amount}', + descriptionPlaceholder: 'Enter expense description', + advanceCodePlaceholder: 'Select or enter advance code', + deleteConfirm: 'Confirm Delete', + getDetailFailed: 'Failed to get details', + }, + + advance: { + title: 'Advance Application', + description: 'Manage employee advance applications', + newAdvance: 'New Advance', + editAdvance: 'Edit Advance', + subject: 'Subject', + applicant: 'Applicant', + amount: 'Amount', + advanceDate: 'Advance Date', + status: 'Status', + code: 'Code', + action: 'Action', + detail: 'Details', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + delete: 'Delete', + activeApplications: 'Active Applications', + completed: 'Completed', + advanceCode: 'Advance Code', + subjectPlaceholder: 'Please enter advance reason', + amountPlaceholder: 'Enter amount', + inputAmount: 'Please enter amount', + equivalentCNY: 'Equivalent RMB: ¥ ', + attachment: 'Proof Attachment', + pendingApproval: 'Pending Approval', + approved: 'Approved', + rejected: 'Rejected', + withdrawn: 'Withdrawn', + verified: 'Verified', + pendingEdit: 'Pending Edit', + deleteSuccess: 'Deleted successfully', + deleteFailed: 'Delete failed', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + saveSuccess: 'Saved successfully', + createSuccess: 'Created successfully', + submitSuccess: 'Submitted successfully', + getListFailed: 'Failed to get advance list', + getDetailFailed: 'Failed to get details', + deletePassInput: 'Please enter password to confirm deletion', + deletePassPlaceholder: 'Enter password', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + detailTitle: 'Advance Details', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + }, + + advanceVerification: { + title: 'Advance Verification Status', + description: 'Manage advance verification status and progress', + subject: 'Subject', + applicant: 'Applicant', + advanceAmount: 'Advance Amount', + verifiedAmount: 'Verified Amount', + remainingAmount: 'Remaining Amount', + advanceDate: 'Advance Date', + status: 'Status', + code: 'Code', + action: 'Action', + searchByName: 'Search by applicant name', + startDate: 'Start Date', + endDate: 'End Date', + search: 'Search', + unverified: 'Not Fully Verified', + completed: 'Completed', + detail: 'Details', + noAccess: 'Access Denied', + noAccessMsg: 'You do not have permission to access this page. Only admin and finance personnel can view advance verification status.', + pendingApproval: 'Pending Approval', + approved: 'Approved', + rejected: 'Rejected', + withdrawn: 'Withdrawn', + verified: 'Verified', + pendingEdit: 'Pending Edit', + partialVerified: 'Partially Verified', + completedStatus: 'Completed', + detailTitle: 'Advance Details: {code}', + advanceCode: 'Advance Code', + amount: 'Amount', + verifiedLabel: 'Verified Amount', + remaining: 'Remaining Amount', + currency: 'Currency', + relatedVerifications: 'Related Verifications', + verificationCode: 'Verification Code', + relatedAdvance: 'Related Advance', + verificationAmount: 'Verification Amount', + verificationDate: 'Verification Date', + isSettlement: 'Is Settlement', + getListFailed: 'Failed to get advance list', + getDetailFailed: 'Failed to get advance details', + }, + + reimbursement: { + title: 'Reimbursement Application', + description: 'Manage expense reimbursement applications', + newReimbursement: 'New Reimbursement', + editReimbursement: 'Edit Reimbursement', + subject: 'Subject', + applicant: 'Applicant', + amount: 'Amount', + reimbursementDate: 'Reimbursement Date', + status: 'Status', + code: 'Code', + action: 'Action', + detail: 'Details', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + delete: 'Delete', + addDetail: 'Add Detail', + activeApplications: 'Active Applications', + completed: 'Completed', + expenseType: 'Expense Type', + selectProject: 'Select Project', + subjectPlaceholder: 'Please enter reimbursement reason', + detailLabel: 'Reimbursement Details', + expenseDescription: 'Expense Description', + expenseCategory: 'Expense Category', + detailAmount: 'Amount', + attachment: 'Proof Attachment', + mainAttachment: 'Main Attachment', + overallAttachment: 'Overall Proof Attachment', + totalLabel: 'Total: ', + unknownProject: 'Unknown Project', + reimbursementCode: 'Reimbursement Code', + attachmentCount: '{count} sheets', + pendingApproval: 'Pending Approval', + approved: 'Approved', + rejected: 'Rejected', + withdrawn: 'Withdrawn', + paid: 'Paid', + pendingEdit: 'Pending Edit', + deleteSuccess: 'Deleted successfully', + deleteFailed: 'Delete failed', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + saveSuccess: 'Saved successfully', + createSuccess: 'Created successfully', + submitSuccess: 'Submitted successfully', + getListFailed: 'Failed to get reimbursement list', + deletePassInput: 'Please enter password to confirm deletion', + deletePassPlaceholder: 'Enter password', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + detailTitle: 'Reimbursement Details', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + expenseDetail: 'Expense Details', + totalItems: 'Total {count} items', + receipt: 'Receipt', + addExpense: 'Add Expense', + editExpense: 'Edit Expense', + category: 'Expense Category', + categoryLabel: 'Category: ', + categoryPlaceholder: 'Select expense category', + categoryRequired: 'Please select expense category', + descriptionPlaceholder: 'Enter expense description', + project: 'Related Project', + projectPlaceholder: 'Select project', + inputAmount: 'Please enter amount', + amountPlaceholder: 'Enter amount', + equivalentCNY: 'Equivalent RMB: ¥', + totalAmount: 'Total: ¥{amount}', + edit: 'Edit', + viewDetail: 'View Details', + getDetailFailed: 'Failed to get details', + deleteConfirmMsg: 'Confirm delete this reimbursement record?', + }, + + approval: { + title: 'Approval Management', + description: 'Approve advance, reimbursement, payment and other applications', + approve: 'Approve', + refresh: 'Refresh Data', + pass: 'Approve', + reject: 'Reject', + close: 'Close', + pendingTab: 'Pending Approval', + historyTab: 'Approval History', + subject: 'Subject', + type: 'Type', + applicant: 'Applicant', + amount: 'Amount', + applicationDate: 'Application Date', + status: 'Status', + code: 'Code', + action: 'Action', + time: 'Time', + operation: 'Operation', + operator: 'Operator', + note: 'Note/Reason', + applicationType: 'Application Type', + applicationCode: 'Application Code', + payeeType: 'Payee Type', + payee: 'Payee', + bankName: 'Bank Name', + bankAccount: 'Bank Account', + expenseType: 'Expense Type', + relatedProject: 'Related Project', + expenseCategory: 'Expense Category', + relatedAdvance: 'Related Advance', + advanceAmount: 'Advance Amount', + settlement: 'Settlement Verification', + verifiedAmount: 'Verified Amount', + remainingAmount: 'Remaining Settlement Amount', + settlementAmount: 'Settlement Amount', + purchaseType: 'Purchase Type', + supplier: 'Supplier', + currency: 'Currency', + remark: 'Remark', + approvalNote: 'Approval Note', + approvalNotePlaceholder: 'Optional: enter approval note', + rejectReason: 'Rejection Reason', + rejectReasonPlaceholder: 'Please enter rejection reason', + productDetail: 'Product Details', + detailList: 'Detail List', + refundProof: 'Refund Proof', + proofAttachment: 'Proof Attachment', + approvalOpinion: 'Approval Opinion', + detailLabel: 'Detail {index}:', + categoryLabel: 'Expense Category: ', + detailAttachment: 'Detail Attachment: ', + specLabel: 'Spec: ', + unitLabel: 'Unit: ', + qtyLabel: 'Qty: ', + priceLabel: 'Unit Price: ', + advanceApply: 'Advance Application', + reimburseApply: 'Reimbursement Application', + paymentApply: 'Payment Application', + verificationApply: 'Verification Application', + purchaseApply: 'Purchase Application', + pendingApproval: 'Pending Approval', + approved: 'Approved', + rejected: 'Rejected', + withdrawn: 'Withdrawn', + executed: 'Executed', + partialVerified: 'Partially Verified', + completed: 'Completed', + getPendingFailed: 'Failed to get pending approval data', + getHistoryFailed: 'Failed to get approval history', + approveSuccess: 'Approved: {code}', + rejectSuccess: 'Rejected: {code}', + withdrawSuccess: 'Application withdrawn', + editResubmit: 'Edited successfully, resubmitted for approval', + withdrawConfirm: 'Withdraw Application', + withdrawConfirmMsg: 'Confirm withdrawal of application {code}?', + withdrawConfirmBtn: 'Confirm Withdrawal', + projectPurchase: 'Project Purchase', + stockPurchase: 'Stock Purchase', + refundText: 'Refund ', + supplementText: 'Supplement ', + editTitle: 'Edit Application: {code}', + detailTitle: '{type} Details: {code}', + advanceDetailTitle: '{type} Details: {code}', + counterpartySubcontractor: 'Subcontractor', + counterpartySupplier: 'Supplier', + counterpartyCustomer: 'Customer', + counterpartyOther: 'Other', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + other: 'Other', + accommodation: 'Accommodation', + catering: 'Catering', + fuel: 'Fuel', + scatteredMaterial: 'Scattered Materials', + customerRelation: 'Customer Relations', + subcontractorRelation: 'Subcontractor Relations', + EDLRelation: 'EDL Relations', + extraConstruction: 'Extra Construction', + generalOperation: 'General Operations (Rent/Consumables)', + commute: 'Commute', + marketing: 'Marketing', + powerSystem: 'Power System Relations', + employeeBenefit: 'Employee Benefits', + expressLogistics: 'Express Logistics', + }, + + execution: { + title: 'Execution Management', + description: 'Execute approved payment requests', + execute: 'Execute', + edit: 'Edit', + cancel: 'Cancel', + pass: 'Reject', + close: 'Close', + uploadProof: 'Upload Payment Proof', + pendingTab: 'Pending Execution', + executedTab: 'Executed', + subject: 'Subject', + type: 'Type', + applicant: 'Applicant', + amount: 'Amount', + payee: 'Payee', + approvalDate: 'Approval Date', + code: 'Code', + action: 'Action', + executionDate: 'Execution Date', + executionMethod: 'Execution Method', + status: 'Status', + searchPlaceholder: 'Search subject, code, or applicant', + filterType: 'Filter Type', + sortBy: 'Sort By', + sortDateNew: 'Execution Date (Newest)', + sortDateOld: 'Execution Date (Oldest)', + sortAmountHigh: 'Amount (High to Low)', + sortAmountLow: 'Amount (Low to High)', + confirmationDate: 'Confirmation Date', + paymentMethod: 'Payment Method', + executionMethodLabel: 'Execution Method', + proofOfPayment: 'Payment Proof', + paymentConfirmation: 'Payment Confirmation', + remark: 'Remark', + returnReason: 'Rejection Reason', + confirmRequired: 'Please enter payment confirmation', + rejectReasonRequired: 'Please enter rejection reason', + confirmationPlaceholder: 'Enter payment confirmation info, such as account, time, etc.', + remarkPlaceholder: 'Optional: enter execution remarks', + bankTransfer: 'Bank Transfer', + cash: 'Cash', + wechat: 'WeChat', + other: 'Other', + proofUploadTip: 'Please upload payment proof (bank transfer receipt, cash receipt, etc.), supports images and PDF', + noProofRefund: 'This verification is a refund type, no payment proof required', + noProofNonSettlement: 'This verification is non-settlement, no payment proof required', + pendingExecution: 'Pending Execution', + executed: 'Executed', + rejected: 'Rejected', + approved: 'Approved', + executeSuccess: 'Executed successfully: {code}', + executeFailed: 'Execution failed, please try again', + proofRequired: 'Please upload payment proof', + rejectSuccess: 'Rejected: {code}, applicant can edit and resubmit', + rejectFailed: 'Rejection failed, please try again', + editSuccess: 'Edited successfully, resubmitted for approval', + uploadSuccess: '{name} uploaded successfully', + uploadFailed: '{name} upload failed', + getPendingFailedFormat: 'Failed to get pending execution data: format error', + getPendingFailed: 'Failed to get pending execution data: ', + getPendingNetworkError: 'Network error, failed to get pending execution data', + getExecutedFailedFormat: 'Failed to get executed data: format error', + getExecutedFailed: 'Failed to get executed data: ', + getExecutedNetworkError: 'Network error, failed to get executed data', + supplierPaymentInfo: 'Supplier Payment Info', + accountName: 'Account Name', + bankAccount: 'Bank Account', + bankName: 'Bank Name', + qrCode: 'QR Code', + purchaseDetail: 'Purchase Details', + detailList: 'Detail List', + approvalOpinion: 'Approval Opinion', + refundProof: 'Refund Proof', + applicationAttachment: 'Application Proof Attachment', + executionInfo: 'Execution Information', + applicationType: 'Application Type', + applicationCode: 'Application Code', + payeeType: 'Payee Type', + expenseType: 'Expense Type', + relatedProject: 'Related Project', + expenseCategory: 'Expense Category', + relatedAdvance: 'Related Advance', + advanceAmount: 'Advance Amount', + settlement: 'Settlement Verification', + verifiedAmount: 'Verified Amount', + remainingAmount: 'Remaining Settlement Amount', + settlementAmount: 'Settlement Amount', + purchaseType: 'Purchase Type', + supplier: 'Supplier', + currency: 'Currency', + detailLabel: 'Detail {index}:', + categoryLabel: 'Expense Category: ', + detailAttachment: 'Detail Attachment: ', + specLabel: 'Spec: ', + unitLabel: 'Unit: ', + qtyLabel: 'Qty: ', + advanceApply: 'Advance Application', + reimburseApply: 'Reimbursement Application', + paymentApply: 'Payment Application', + verificationApply: 'Verification Application', + purchaseApply: 'Purchase Application', + projectPurchase: 'Project Purchase', + stockPurchase: 'Stock Purchase', + refundText: 'Refund ', + supplementText: 'Supplement ', + counterpartySubcontractor: 'Subcontractor', + counterpartySupplier: 'Supplier', + counterpartyCustomer: 'Customer', + counterpartyOther: 'Other', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + otherCategory: 'Other', + accommodation: 'Accommodation', + catering: 'Catering', + fuel: 'Fuel', + scatteredMaterial: 'Scattered Materials', + customerRelation: 'Customer Relations', + subcontractorRelation: 'Subcontractor Relations', + EDLRelation: 'EDL Relations', + extraConstruction: 'Extra Construction', + generalOperation: 'General Operations (Rent/Consumables)', + commute: 'Commute', + marketing: 'Marketing', + powerSystem: 'Power System Relations', + employeeBenefit: 'Employee Benefits', + expressLogistics: 'Express Logistics', + }, + + procurement: { + title: 'Procurement Management', + description: 'Manage procurement orders and material receipt', + newProcurement: 'New Procurement', + orderCode: 'Order Code', + purchaseDate: 'Purchase Date', + supplier: 'Supplier', + materialName: 'Material Name', + quantity: 'Quantity', + unitPrice: 'Unit Price', + totalAmount: 'Total Amount', + status: 'Status', + action: 'Action', + view: 'View', + approve: 'Approve', + pendingApproval: 'Pending Approval', + approved: 'Approved', + stocked: 'Stocked', + rejected: 'Rejected', + startDate: 'Start Date', + endDate: 'End Date', + searchOrder: 'Search order code', + monthPurchase: 'Monthly Purchase Amount', + newApplication: 'New Purchase Application', + selectSupplier: 'Select Supplier', + inputMaterialName: 'Please enter material name', + remark: 'Remark', + remarkPlaceholder: 'Please enter remarks', + submitSuccess: 'Purchase application submitted', + }, + + purchaseRequest: { + title: 'Purchase Request', + description: 'Manage company purchase requests (simplified: description and estimated amount only)', + newRequest: 'New Purchase Request', + activeApplications: 'Active Applications', + completed: 'Completed', + subject: 'Subject', + project: 'Project', + category: 'Category', + estimatedAmount: 'Estimated Amount', + demandDate: 'Required Date', + status: 'Status', + applicationDate: 'Application Date', + applicant: 'Applicant', + code: 'Code', + action: 'Action', + approve: 'Approve', + reject: 'Reject', + withdraw: 'Withdraw', + edit: 'Edit', + confirmDelete: 'Confirm delete?', + selectProjectFilter: 'Filter by project', + selectStatusFilter: 'Filter by status', + editRequest: 'Edit Purchase Request', + submitApproval: 'Submit for Approval', + purchaseType: 'Purchase Type', + selectPurchaseType: 'Please select purchase type', + purchaseTypeRequired: 'Please select purchase type', + stockPurchase: 'Stock Purchase', + projectPurchase: 'Project Purchase', + relatedProject: 'Related Project', + selectProject: 'Please select project', + projectRequired: 'Project purchase must be linked to a project', + applicantLabel: 'Applicant', + applicantPlaceholder: 'Please enter applicant', + applicantRequired: 'Please enter applicant', + applicationDateLabel: 'Application Date', + dateRequired: 'Please select application date', + subjectDescription: 'Subject Description', + subjectRequired: 'Please enter subject description', + subjectMaxLength: 'Subject description cannot exceed 100 characters', + subjectPlaceholder: 'Briefly describe purchase needs (e.g., cables, poles needed for XX project)', + expenseCategory: 'Expense Category', + selectCategory: 'Please select expense category', + categoryRequired: 'Please select expense category', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + other: 'Other', + estimatedAmountLabel: 'Estimated Amount', + amountRequired: 'Please enter estimated amount', + estimatedAmountPlaceholder: 'Estimated amount', + currency: 'Currency', + selectCurrency: 'Please select currency', + currencyRequired: 'Please select currency', + demandDateLabel: 'Required Date', + demandDatePlaceholder: 'Expected delivery date', + remarkLabel: 'Remark', + remarkPlaceholder: 'Please enter remarks (optional)', + attachment: 'Attachment', + selectFile: 'Select File', + detailTitle: 'Purchase Request Details', + applicationCode: 'Application Code', + createdAt: 'Created At', + getListFailed: 'Failed to get purchase request list', + getDetailFailed: 'Failed to get purchase request details', + deleteSuccess: 'Deleted successfully', + deleteFailed: 'Delete failed', + saveSuccess: 'Saved successfully', + createSuccess: 'Created successfully', + submitSuccess: 'Created and submitted successfully', + submitFailed: 'Submit failed', + withdrawSuccess: 'Withdrawn successfully', + withdrawFailed: 'Withdrawal failed', + approveSuccess: 'Approved successfully', + approveFailed: 'Approval failed', + rejectSuccess: 'Rejected successfully', + rejectFailed: 'Rejection failed', + pendingEdit: 'Pending Edit', + pendingApproval: 'Pending Approval', + approved: 'Approved', + executed: 'Executed', + withdrawn: 'Withdrawn', + currencyCNY: 'RMB', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + }, + + purchaseOrder: { + title: 'Purchase Order', + description: 'Manage purchase orders (multi-tab: Basic Info, Product Details, Payment Info, Logistics, Acceptance)', + draft: 'Draft', + confirmed: 'Confirmed', + partialPayment: 'Partial Payment', + paidOff: 'Paid Off', + inTransit: 'In Transit', + accepted: 'Accepted', + closed: 'Closed', + cancelled: 'Cancelled', + orderCode: 'Order Code', + supplier: 'Supplier', + relatedProject: 'Project', + amount: 'Amount', + paid: 'Paid', + status: 'Status', + createDate: 'Create Date', + action: 'Action', + confirm: 'Confirm', + cancel: 'Cancel', + delete: 'Delete', + confirmCancel: 'Confirm cancel?', + confirmDelete: 'Confirm delete?', + confirmDeleteShort: 'Confirm delete?', + productName: 'Product Name', + spec: 'Spec', + unit: 'Unit', + quantity: 'Quantity', + unitPrice: 'Unit Price', + subtotal: 'Subtotal', + phase: 'Phase', + plannedDate: 'Planned Date', + plannedAmount: 'Planned Amount', + ratioPercent: 'Ratio %', + actualAmount: 'Actual Amount', + pendingPayment: 'Pending Payment', + applied: 'Applied', + paidStatus: 'Paid', + trackingNumber: 'Tracking No.', + origin: 'Origin', + china: 'China', + laos: 'Laos', + logisticsCompany: 'Logistics Company', + deliveryDate: 'Delivery Date', + freight1: 'Freight 1', + freight2: 'Freight 2', + acceptanceCode: 'Acceptance Code', + acceptanceDate: 'Acceptance Date', + acceptor: 'Acceptor', + acceptedQty: 'Accepted Qty', + selectProject: 'Filter by project', + selectStatus: 'Filter by status', + detailTitle: 'Purchase Order Details - {code}', + basicInfo: 'Basic Information', + supplierCountry: 'Supplier Country', + estimatedAmount: 'Estimated Amount', + orderAmount: 'Order Amount', + paidAmount: 'Paid Amount', + createdAt: 'Created At', + remark: 'Remark', + productDetails: 'Product Details', + addProduct: 'Add Product', + productTotal: 'Product Total: ', + paymentInfo: 'Payment Info', + addPaymentPlan: 'Add Payment Plan', + logisticsInfo: 'Logistics Info', + noLogistics: 'No logistics info', + acceptanceRecord: 'Acceptance Record', + noAcceptance: 'No acceptance records', + editProduct: 'Edit Product', + addProductTitle: 'Add Product', + selectProduct: 'Select Product', + productNameInput: 'Product Name', + specInput: 'Spec', + unitInput: 'Unit', + quantityInput: 'Quantity', + unitPriceInput: 'Unit Price', + editPaymentPlan: 'Edit Payment Plan', + addPaymentPlanTitle: 'Add Payment Plan', + selectPhase: 'Select Phase', + advancePayment: 'Advance Payment', + deliveryPayment: 'Delivery Payment', + acceptancePayment: 'Acceptance Payment', + finalPayment: 'Final Payment', + orderConfirmSuccess: 'Order confirmed successfully', + orderConfirmFailed: 'Order confirmation failed', + orderCancelSuccess: 'Order cancelled', + orderCancelFailed: 'Order cancellation failed', + orderDeleteSuccess: 'Order deleted successfully', + orderDeleteFailed: 'Order deletion failed', + productUpdateSuccess: 'Product updated successfully', + productAddSuccess: 'Product added successfully', + productDeleteSuccess: 'Product deleted successfully', + paymentPlanUpdateSuccess: 'Payment plan updated successfully', + paymentPlanAddSuccess: 'Payment plan added successfully', + paymentPlanDeleteSuccess: 'Payment plan deleted successfully', + getListFailed: 'Failed to get purchase order list', + getDetailFailed: 'Failed to get order details', + }, + + product: { + thumbnail: 'Thumbnail', + productName: 'Product Name', + model: 'Model', + level1Category: 'Level 1 Category', + level2Category: 'Level 2 Category', + unit: 'Unit', + quantity: 'Quantity', + costPrice: 'Cost Price', + brand: 'Brand', + action: 'Action', + edit: 'Edit', + delete: 'Delete', + confirmDelete: 'Confirm delete this product?', + confirmDeleteCategory: 'Confirm delete this category?', + addLevel2: 'Add Level 2 Category', + addLevel1: 'Add Level 1 Category', + totalProducts: 'Total Products', + totalCategories: 'Total Categories', + searchPlaceholder: 'Search product name / model / brand', + downloadTemplate: 'Download Template', + batchUpload: 'Batch Upload', + addProduct: 'Add Product', + productList: 'Product List', + filterByCategory: 'Filter by Category: ', + selectCategory: 'Select Category', + clearFilter: 'Clear Filter', + totalRecords: 'Total {total} records', + categoryManagement: 'Category Management', + addCategory: 'Add Category', + noCategory: 'No categories', + editProduct: 'Edit Product', + addProductTitle: 'Add Product', + nameRequired: 'Please enter product name', + namePlaceholder: 'e.g., JKLYJ-120-22kV HV Insulated Wire', + modelPlaceholder: 'e.g., JKLYJ-120-22kV', + selectLevel1: 'Select level 1 category', + level1Required: 'Please select level 1 category', + selectLevel2: 'Select level 2 category (optional)', + level2Extra: 'Optional, defaults to level 1 if not selected', + selectUnit: 'Select unit', + costPricePlaceholder: 'Default 0', + source: 'Source', + selectSource: 'Select source', + china: 'China', + laos: 'Laos', + brandPlaceholder: 'Brand name', + specs: 'Specifications', + specsPlaceholder: 'e.g., 120mm², 22kV', + remarkPlaceholder: 'Other notes', + batchUploadTitle: 'Batch Upload Products', + uploadInstructions: 'Upload Instructions:', + uploadStep1: 'Please download the template file first and fill in product information according to the template format', + uploadStep2: 'Supports .xlsx and .xls Excel file formats', + uploadStep3: 'Product name and level 1 category are required fields', + uploadStep4: 'Other fields are optional, fill in as appropriate', + uploadStep5: 'Source field defaults to Laos, can be China/Laos', + uploading: 'Uploading...', + selectExcel: 'Select Excel File', + downloadImportTemplate: 'Download Import Template', + editCategory: 'Edit Category', + addCategoryTitle: 'Add Category', + categoryName: 'Category Name', + categoryNameRequired: 'Please enter category name', + categoryNamePlaceholder: 'e.g., Wire & Cable', + categoryLevel: 'Category Level', + selectLevel: 'Select category level', + levelRequired: 'Please select category level', + parentCategory: 'Parent Category', + selectParent: 'Select parent category (optional)', + parentCategoryExtra: 'When selecting level 2 category, parent category is required', + getListFailed: 'Failed to get product list', + categoryUpdateSuccess: 'Category updated successfully', + categoryCreateSuccess: 'Category created successfully', + categoryDeleteSuccess: 'Category deleted successfully', + title: 'Product Management', + code: 'Product Code', + codePlaceholder: 'Enter product code', + codeRequired: 'Please enter product code', + name: 'Product Name', + category: 'Category', + categoryRequired: 'Please select category', + spec: 'Specification', + specPlaceholder: 'Enter specification', + unitRequired: 'Please select unit', + safetyStock: 'Safety Stock', + safetyStockPlaceholder: 'Enter safety stock', + safetyStockRequired: 'Please enter safety stock', + remark: 'Remark', + stock: 'Stock', + description: 'Description', + createdAt: 'Created At', + piece: 'piece', + meter: 'meter', + kilometer: 'kilometer', + ton: 'ton', + pole2: 'pole', + set: 'set', + unit2: 'unit', + detailTitle: 'Product Details', + getDetailFailed: 'Failed to get product details', + deleteSuccess: 'Deleted successfully', + deleteFailed: 'Delete failed', + }, + + inventory: { + stockIn: 'Stock In', + stockOut: 'Stock Out', + recordType: 'Record Type', + product: 'Product', + project: 'Project', + quantity: 'Quantity', + unitPrice: 'Unit Price', + totalAmount: 'Total Amount', + recordDate: 'Record Date', + operator: 'Operator', + remark: 'Remark', + unitLabel: 'Unit', + totalStockIn: 'Total Stock In', + totalStockOut: 'Total Stock Out', + currentStock: 'Current Stock', + stockRecord: 'Stock Records', + stockSummary: 'Stock Summary', + selectProduct: 'Filter by product', + selectProject: 'Filter by project', + selectRecordType: 'Select record type', + stockOutBtn: 'Stock Out', + stockOutTitle: 'Stock Out', + relatedProject: 'Related Project', + selectProjectRequired: 'Please select project', + selectProductRequired: 'Please select product', + stockOutQuantity: 'Stock Out Quantity', + quantityRequired: 'Please enter stock out quantity', + inputUnitPrice: 'Please enter unit price', + inputTotalAmount: 'Please enter total amount', + inputRemark: 'Please enter remarks', + stockOutSuccess: 'Stock out successful', + stockOutFailed: 'Stock out failed', + getListFailed: 'Failed to get inventory records', + title: 'Inventory Management', + description: 'Manage product inventory and stock in/out records', + productCode: 'Product Code', + productName: 'Product Name', + spec: 'Specification', + unit: 'Unit', + stock: 'Stock', + safetyStock: 'Safety Stock', + locked: 'Locked', + lastIn: 'Last Stock In', + lastOut: 'Last Stock Out', + status: 'Status', + normal: 'Normal', + lowStock: 'Low Stock', + outOfStock: 'Out of Stock', + tabInventory: 'Inventory', + tabLog: 'Stock In/Out Log', + categoryFilter: 'Filter by category', + stockFilter: 'Stock Filter', + all: 'All', + time: 'Time', + type: 'Type', + in: 'In', + out: 'Out', + before: 'Before', + after: 'After', + orderCode: 'Related Order', + relatedOrder: 'Related Order', + relatedOrderPlaceholder: 'Enter related order code', + date: 'Date', + dateRequired: 'Please select date', + remarkPlaceholder: 'Enter remarks', + quantityPlaceholder: 'Enter quantity', + stockInTitle: 'Stock In', + stockInSuccess: 'Stock in successful', + stockInFailed: 'Stock in failed', + getLogFailed: 'Failed to get log records', + }, + + supplier: { + title: 'Supplier List', + totalCount: 'Total Suppliers', + totalPurchase: 'Total Purchase Amount', + totalPayable: 'Total Payable', + searchPlaceholder: 'Search supplier code, name, or supply category', + newSupplier: 'New Supplier', + editSupplier: 'Edit Supplier', + name: 'Name', + supplyCategory: 'Supply Category', + country: 'Country', + purchaseAmount: 'Purchase Amount', + payableAmount: 'Payable Amount', + action: 'Action', + contact: 'Contacts', + mainContact: 'Primary Contact', + paymentInfo: 'Payment Info', + addContact: '+ Add Contact', + addPaymentInfo: '+ Add Payment Info', + accountName: 'Account Name', + bankName: 'Bank Name', + bankAccount: 'Bank Account', + qrCode: 'QR Code', + mainAccount: 'Primary Account', + deleteContact: 'Delete', + deletePaymentInfo: 'Delete this payment info', + nameRequired: 'Please enter name', + namePlaceholder: 'Supplier name', + categoryPlaceholder: 'Manual entry: e.g., Electrical Equipment, Building Materials', + china: 'China', + laos: 'Laos', + remarkPlaceholder: 'Remarks', + getListFailed: 'Failed to get supplier list', + confirmDeleteMsg: 'Are you sure you want to delete this supplier?', + notFound: 'Supplier not found', + basicInfo: 'Basic Information', + code: 'Code', + remarkLabel: 'Remark: ', + returnToList: 'Back to List', + ledger: 'Business Ledger', + phoneLabel: 'Phone: ', + positionLabel: 'Position: ', + qrCodeLabel: 'QR Code: ', + accountNameLabel: 'Account Name: ', + accountNumLabel: 'Account No.: ', + bankLabel: 'Bank: ', + notFoundTitle: 'Supplier Not Found', + noContact: 'No contacts', + noPayment: 'No payment info', + mainContactTag: 'Primary Contact', + mainAccountTag: 'Primary Account', + }, + + subcontractor: { + title: 'Subcontractor List', + totalCount: 'Total Subcontractors', + totalContract: 'Total Contract Amount', + totalPayable: 'Total Payable', + searchPlaceholder: 'Search subcontractor code, name, or scope', + newSubcontractor: 'New Subcontractor', + editSubcontractor: 'Edit Subcontractor', + name: 'Name', + scope: 'Scope', + country: 'Country', + contractAmount: 'Contract Amount', + payableAmount: 'Payable Amount', + action: 'Action', + contact: 'Contacts', + mainContact: 'Primary Contact', + paymentInfo: 'Payment Info', + addContact: '+ Add Contact', + addPaymentInfo: '+ Add Payment Info', + accountName: 'Account Name', + bankName: 'Bank Name', + bankAccount: 'Bank Account', + qrCode: 'QR Code', + mainAccount: 'Primary Account', + deleteContact: 'Delete', + deletePaymentInfo: 'Delete this payment info', + nameRequired: 'Please enter name', + namePlaceholder: 'Subcontractor name', + scopePlaceholder: 'Manual entry: e.g., Electrical Installation, Civil Engineering', + china: 'China', + laos: 'Laos', + feature: 'Features', + featurePlaceholder: 'Manual entry: e.g., Professional team, Fully equipped, Reasonable pricing', + remarkPlaceholder: 'Remarks', + getListFailed: 'Failed to get subcontractor list', + confirmDeleteMsg: 'Are you sure you want to delete this subcontractor?', + notFound: 'Subcontractor not found', + basicInfo: 'Basic Information', + code: 'Code', + featureLabel: 'Features: ', + remarkLabel: 'Remark: ', + returnToList: 'Back to List', + ledger: 'Business Ledger', + phoneLabel: 'Phone: ', + positionLabel: 'Position: ', + bankLabel: 'Bank: ', + accountLabel: 'Account: ', + qrCodeLabel: 'QR Code: ', + defaultAccount: 'Default Account', + notFoundTitle: 'Subcontractor Not Found', + noContact: 'No contacts', + noPayment: 'No payment info', + mainContactTag: 'Primary Contact', + }, + + customer: { + title: 'Customer List', + totalCount: 'Total Customers', + totalContract: 'Total Contract Amount', + totalReceivable: 'Total Receivable', + searchPlaceholder: 'Search customer code, name, or address', + newCustomer: 'New Customer', + editCustomer: 'Edit Customer', + name: 'Name', + address: 'Address', + mainContact: 'Primary Contact', + paymentInfo: 'Payment Info', + contractAmount: 'Contract Amount', + receivableAmount: 'Receivable Amount', + action: 'Action', + contact: 'Contacts', + addContact: '+ Add Contact', + addPaymentInfo: '+ Add Payment Info', + accountName: 'Account Name', + bankName: 'Bank Name', + bankAccount: 'Bank Account', + qrCode: 'QR Code', + mainAccount: 'Primary Account', + deleteContact: 'Delete', + deletePaymentInfo: 'Delete this payment info', + nameRequired: 'Please enter name', + namePlaceholder: 'Customer name', + addressPlaceholder: 'Customer address', + remarkPlaceholder: 'Remarks', + getListFailed: 'Failed to get customer list', + confirmDeleteMsg: 'Are you sure you want to delete this customer?', + notFound: 'Customer not found', + basicInfo: 'Basic Information', + code: 'Code', + remarkLabel: 'Remark: ', + returnToList: 'Back to List', + ledger: 'Business Ledger', + relatedBudget: 'Related Budget', + projectName: 'Project Name', + businessManager: 'Business Manager', + inNegotiation: 'In Negotiation', + signed: 'Signed', + unsigned: 'Unsigned', + quotationCount: 'Quotation Versions', + createdAt: 'Created At', + noBudget: 'No related budget projects', + phoneLabel: 'Phone: ', + positionLabel: 'Position: ', + accountNameLabel: 'Account Name:', + accountNumLabel: 'Account No.:', + mainContactTag: 'Primary Contact', + noContact: 'No contacts', + noPayment: 'No payment info', + }, + + logistics: { + title: 'Logistics Management', + description: 'Manage logistics partners (unified partner interface)', + newCompany: 'New Logistics Company', + editCompany: 'Edit Logistics Company', + companyName: 'Company Name', + phone: 'Phone', + quoteDescription: 'Quote Description', + createdAt: 'Created At', + action: 'Action', + confirmDelete: 'Confirm delete?', + contact: 'Contacts', + name: 'Name', + position: 'Position', + phoneLabel: 'Phone', + mainContact: 'Primary Contact', + confirmDeleteShort: 'Confirm delete?', + paymentInfo: 'Payment Info', + accountName: 'Account Name', + bankAccount: 'Bank Account', + bankName: 'Bank Name', + defaultAccount: 'Default', + defaultLabel: 'Default', + orders: 'Orders', + trackingNumber: 'Tracking No.', + purchaseOrder: 'Purchase Order', + deliveryDate: 'Delivery Date', + freight1: 'Freight 1', + freight1Status: 'Freight 1 Status', + freight2: 'Freight 2', + freight2Status: 'Freight 2 Status', + status: 'Status', + pendingPayment: 'Pending Payment', + applied: 'Applied', + paid: 'Paid', + addContact: 'Add Contact', + addPaymentInfo: 'Add Payment Info', + basicInfo: 'Basic Information', + email: 'Email', + remark: 'Remark', + paymentTab: 'Payment Info', + ledger: 'Business Ledger', + editContact: 'Edit Contact', + addContactTitle: 'Add Contact', + editPaymentInfo: 'Edit Payment Info', + addPaymentInfoTitle: 'Add Payment Info', + nameRequired: 'Please enter name', + positionRequired: 'Please enter position', + phoneRequired: 'Please enter phone', + accountNameRequired: 'Please enter account name', + bankAccountRequired: 'Please enter bank account', + bankNameRequired: 'Please enter bank name', + qrCodeRequired: 'Please enter QR code image URL', + isMainContact: 'Is Primary Contact', + isDefaultAccount: 'Is Default Account', + address: 'Address', + addressPlaceholder: 'Please enter address', + quotePlaceholder: 'Please enter quote description (e.g., China-Laos land freight rates, transit time, etc.)', + remarkPlaceholder: 'Please enter remarks', + getListFailed: 'Failed to get logistics company list', + getDetailFailed: 'Failed to get logistics company details', + contactUpdateSuccess: 'Contact updated successfully', + contactAddSuccess: 'Contact added successfully', + contactDeleteSuccess: 'Contact deleted successfully', + paymentInfoUpdateSuccess: 'Payment info updated successfully', + paymentInfoAddSuccess: 'Payment info added successfully', + paymentInfoDeleteSuccess: 'Payment info deleted successfully', + detailTitle: 'Logistics Company Details - {name}', + qrCode: 'QR Code', + }, + + businessLedger: { + contractTotal: 'Contract Total', + totalPaid: 'Total Paid', + totalUnpaid: 'Total Unpaid', + projectCount: 'Project Count', + purchaseTotal: 'Purchase Total', + totalReceived: 'Total Received', + totalReceivable: 'Total Receivable', + orderCount: 'Order Count', + freight1Total: 'Freight 1 Total', + logisticsCount: 'Logistics Count', + code: 'Code', + name: 'Name', + contractAmount: 'Contract Amount', + status: 'Status', + purchaseAmount: 'Purchase Amount', + project: 'Project', + freight1: 'Freight 1', + freight1Status: 'Freight 1 Status', + completed: 'Completed', + inProgress: 'In Progress', + planning: 'Planning', + pending: 'Pending', + approved: 'Approved', + paid: 'Paid', + applied: 'Applied', + noRecord: 'No business records', + }, + + exchangeRate: { + title: 'Exchange Rate', + description: 'Set currency exchange rates, auto-calculate when entering either side', + CNYLAK: 'CNY-LAK Rate', + CNY: 'RMB', + LAK: 'LAK', + CNYUSD: 'CNY-USD Rate', + USD: 'USD', + CNYTHB: 'CNY-THB Rate', + THB: 'THB', + USDLAK: 'USD-LAK Rate', + THBLAK: 'THB-LAK Rate', + ratePair: 'Rate Pair', + rate: 'Rate', + effectiveDate: 'Effective Date', + setTime: 'Set Time', + setBy: 'Set By', + lastUpdated: 'Last Updated: ', + actualRate: 'Actual Rate: ', + confirmSave: 'Confirm Save Rate', + historyRate: 'Historical Rate Records', + tipText: 'Tip: Enter either side value and the other side will auto-calculate. Actual rate displays as 1 source currency = X target currency. Click "Confirm Save Rate" to save current settings to database.', + getRateFailed: 'Failed to get exchange rate', + noChange: 'No exchange rate changes detected', + saveSuccess: 'Exchange rate saved successfully', + saveFailed: 'Failed to save exchange rate', + inputFrom: 'Enter {from} amount', + inputTo: 'Enter {to} amount', + }, + + projectCost: { + title: 'Project Cost', + selectProject: 'Please select a project to view cost statistics', + contractAmount: 'Contract Amount', + purchaseCost: 'Purchase Cost', + paymentExpense: 'Payment Expense', + totalIncome: 'Total Income', + totalExpense: 'Total Expense', + profit: 'Profit', + profitRate: 'Profit Rate', + totalCostBreakdown: 'Total Cost Breakdown', + totalCost: 'Total Cost', + costProgress: 'Cost Progress', + costRatio: 'Cost / Contract Ratio', + purchaseCategoryBreakdown: 'Purchase Cost Categories', + incomeBreakdown: 'Income Breakdown', + expenseBreakdown: 'Expense Breakdown', + expenseByLevel1: 'Expense by Category', + overviewTab: 'Overview', + detailsTab: 'Transactions', + detail: 'Details', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + other: 'Other', + noData: 'No data', + getDataFailed: 'Failed to get cost statistics', + }, + + systemLogs: { + title: 'System Logs', + description: 'View system operation records and audit logs', + logId: 'Log ID', + time: 'Time', + level: 'Level', + module: 'Module', + operator: 'Operator', + operation: 'Operation', + ipAddress: 'IP Address', + detail: 'Details', + logLevel: 'Log Level', + selectModule: 'Module', + export: 'Export', + clear: 'Clear', + searchPlaceholder: 'Search log content', + moduleUser: 'User Management', + moduleProject: 'Project Management', + moduleFinance: 'Finance Management', + moduleSystem: 'System', + login: 'User Login', + createProject: 'Create Project', + approveAdvance: 'Approve Advance', + dataBackup: 'Data Backup', + }, + + about: { + title: 'About', + description: 'System information and version', + systemInfo: 'System Information', + systemName: 'System Name', + systemNameValue: 'Qingyuan Power Lao ERP', + version: 'System Version', + versionValue: 'V1.0.0', + devTeam: 'Development Team', + devTeamValue: 'Qingyuan Power IT Department', + onlineDate: 'Launch Date', + onlineDateValue: 'March 2026', + techArchitecture: 'Technical Architecture', + deployEnv: 'Deployment Environment', + deployEnvValue: 'Tencent Cloud Server', + frontend: 'Frontend Framework', + frontendValue: 'Vite + React + TypeScript', + backend: 'Backend Framework', + backendValue: 'Express.js + PostgreSQL', + modules: 'Feature Modules', + serverStatus: 'Server Status', + databaseStatus: 'Database Status', + cpuUsage: 'CPU Usage', + memoryUsage: 'Memory Usage', + diskSpace: 'Disk Space', + serverIp: 'Server IP', + os: 'Operating System', + osValue: 'OpenCloudOS 9', + nodeVersion: 'Node Version', + running: 'Running Normally', + dbName: 'Database Name', + connectionStatus: 'Connection Status', + normal: 'Normal', + lastBackup: 'Last Backup', + footer: '© 2026 Qingyuan Power Lao ERP System - Version V1.0.0', + }, + + backup: { + title: 'Data Backup', + description: 'Manage system data backup and recovery', + backupName: 'Backup Name', + backupTime: 'Backup Time', + fileSize: 'File Size', + backupType: 'Backup Type', + auto: 'Auto', + manual: 'Manual', + status: 'Status', + success: 'Success', + failed: 'Failed', + action: 'Action', + download: 'Download', + restore: 'Restore', + delete: 'Delete', + totalBackups: 'Total Backups', + totalSize: 'Total Size', + lastBackup: 'Last Backup', + storageSpace: 'Storage Space', + backupList: 'Backup List', + autoBackupSetting: 'Auto Backup Settings', + immediateBackup: 'Backup Now', + backupCreated: 'Backup created successfully', + }, + + processManagement: { + title: 'Process Management', + description: 'Configure approval process nodes for finance applications, with custom execution roles', + flowchart: 'Current Flowchart', + nodeConfig: 'Node Configuration', + applicableProcess: 'Applicable Process', + processType: 'Process Type', + desc: 'Description', + status: 'Status', + enabled: 'Enabled', + disabled: 'Disabled', + sequence: 'Sequence', + nodeName: 'Node Name', + executeRole: 'Execution Role', + action: 'Action', + edit: 'Edit', + editNode: 'Edit Node: {name}', + roleApplicant: 'Applicant (Any Role)', + roleAdmin: 'Admin', + roleFinance: 'Finance Specialist', + roleManager: 'Project Manager', + submitApplication: 'Submit Application', + approvalNode: 'Approval', + executePayment: 'Execute Payment', + advanceProcess: 'Advance Application', + advanceProcessDesc: 'Employee advance application process', + reimburseProcess: 'Reimbursement Application', + reimburseProcessDesc: 'Expense reimbursement application process', + paymentProcess: 'Payment Application', + paymentProcessDesc: 'Supplier payment application process', + verificationProcess: 'Verification Application', + verificationProcessDesc: 'Document verification application process', + nodeSaved: 'Node configuration saved', + selectRole: 'Select execution role', + selectRolePlaceholder: 'Please select execution role', + warning: '⚠️ Modifying execution roles will affect all applications using this process. It is recommended to change execution nodes to finance role after having a finance specialist.', + tipTitle: 'Note:', + tipContent: 'Current process is "Applicant → Admin Approval → Admin Execution". You can change the execution role to finance specialist below.', + }, + + processTemplate: { + title: 'Project Template Management', + basicInfo: 'Basic Info', + designPhase: 'Design Phases', + preview: 'Preview & Confirm', + templateName: 'Template Name', + templateDescription: 'Template Description', + namePlaceholder: 'e.g., Distribution Installation Project', + descPlaceholder: 'Describe the type of project this template applies to', + newTemplate: 'New Template', + editTemplate: 'Edit Project Template', + phaseCount: 'Phase Count', + desc: 'Description', + action: 'Action', + copy: 'Copy', + delete: 'Delete', + confirmDelete: 'Confirm delete?', + phaseName: 'Phase Name', + phaseNamePlaceholder: 'e.g., Material Procurement', + phaseType: 'Phase Type', + serial: 'Serial (must wait for dependencies)', + parallel: 'Parallel (can proceed with adjacent phases)', + dependency: 'Dependencies (which phases must complete first)', + subItems: 'Sub-items (one per line)', + subItemsPlaceholder: 'Pole procurement\nTransformer procurement\nCable procurement', + dependencyLabel: 'Dependencies: ', + emptySubItems: 'No sub-items', + noPhase: 'No phases. Click below to add.', + addPhase: 'Add Phase', + saveEdit: 'Save Changes', + confirmCreate: 'Confirm Create', + save: 'Save', + cancel: 'Cancel', + prev: 'Previous', + next: 'Next', + getListFailed: 'Failed to get template list', + copySuccess: 'Copied successfully', + copyFailed: 'Copy failed', + deleteSuccess: 'Deleted successfully', + deleteFailed: 'Delete failed', + nameRequired: 'Please enter template name', + phaseRequired: 'Please add at least one phase', + updateSuccess: 'Template updated successfully', + createSuccess: 'Template created successfully', + phaseNameEmpty: 'Phase name cannot be empty', + systemPreset: 'System Preset', + serialLabel: 'Serial', + parallelLabel: 'Parallel', + dependencyLabelShort: 'Dep: ', + phaseEdit: 'Phase Edit', + }, + + expenseCategory: { + title: 'Expense Category Management', + editCategory: 'Edit Category', + addCategory: 'Add Category', + id: 'ID', + level1: 'Level 1 Category', + level2Code: 'Level 2 Code', + displayName: 'Display Name', + desc: 'Description', + order: 'Order', + status: 'Status', + action: 'Action', + refresh: 'Refresh', + add: 'Add Category', + income: 'Income', + projectExpense: 'Project Expense', + companyExpense: 'Company Expense', + getFailed: 'Failed to get categories', + enabled: 'Enabled', + disabled: 'Disabled', + selectLevel1: 'Please select', + inputLevel2: 'Please enter', + codePlaceholder: 'e.g., material, salary', + namePlaceholder: 'e.g., Material Purchase', + }, + + excelImport: { + title: 'Excel Batch Import Finance Records', + templateRequirements: 'Excel Template Format Requirements', + columnOrder: 'Column Order: Date | Income/Expense Type | Level 1 Category | Level 2 Category | Project Name | Amount | Currency | Exchange Rate | Equivalent RMB | Counterparty Name | Counterparty Type | Person Name | Description | Voucher No.', + formatRequirements: 'Income/Expense Type: Income / Expense | Level 1 Category: Income / Project Expense / Company Expense | Currency: CNY / USD / LAK / THB', + categoryRequirements: 'Level 2 categories must use existing system category names (e.g., Material Purchase, Salary & Benefits, etc.)', + selectFile: 'Select Excel File', + downloadTemplate: 'Download Template File', + templateFileName: 'Finance Ledger Import Template.xlsx', + noData: 'Excel file has no data rows', + invalidDate: 'Date is empty', + invalidType: 'Invalid income/expense type: {type}', + invalidLevel1: 'Invalid level 1 category: {level1}', + invalidLevel2: 'Invalid level 2 category: {level2}', + amountPositive: 'Amount must be greater than 0', + projectRequired: 'Project expense/income must have a project name', + parseComplete: 'Parsing complete, total {count} records', + parseFailed: 'Excel parsing failed: ', + noValidData: 'No valid data to import', + importComplete: 'Import complete: {success} succeeded, {fail} failed', + importFailed: 'Import failed: ', + rowNum: 'Row', + date: 'Date', + incomeExpense: 'I/E', + income: 'Income', + expense: 'Expense', + level1: 'L1', + projectLabel: 'Project', + companyLabel: 'Company', + level2: 'L2', + amount: 'Amount', + currency: 'Currency', + rate: 'Rate', + equivalentCNY: 'Eqv. RMB', + desc: 'Desc', + validation: 'Validation', + countPrefix: 'Total ', + countSuffix: ' records', + validPrefix: 'Valid ', + errorPrefix: 'Error ', + importPrefix: 'Importing ', + importSuffix: ' valid records', + rowPrefix: 'Row ', + rowSuffix: ': ', + }, + + errorBoundary: { + title: 'Page Load Error', + description: 'Sorry, an error occurred while rendering the page. Please try refreshing or contact the administrator.', + errorInfo: 'Error Info:', + stackTrace: 'Stack Trace:', + refresh: 'Refresh Page', + }, + + fileUpload: { + upload: 'Upload', + uploading: 'Uploading...', + preview: 'Image Preview', + uploadSuccess: 'Upload successful', + uploadFailed: 'Upload failed', + }, + + component: { + phonePrefix: 'Phone: ', + wechatPrefix: 'WeChat: ', + whatsappLabel: 'WhatsApp', + whatsappPlaceholder: 'Enter WhatsApp number', + }, + + roles: { + title: 'Roles & Permissions', + description: 'Manage system roles and permission assignments', + searchRole: 'Search Roles', + newRole: 'New Role', + roleId: 'Role ID', + roleName: 'Role Name', + roleDesc: 'Role Description', + permCount: 'Permission Count', + createdAt: 'Created At', + creator: 'Creator', + action: 'Action', + viewPerm: 'View Permissions', + edit: 'Edit', + delete: 'Delete', + superAdmin: 'Super Admin', + superAdminDesc: 'Has all system permissions', + admin: 'System', + adminDesc: 'Project management, construction management permissions', + financeManager: 'Finance Manager', + financeManagerDesc: 'Finance management, approval permissions', + employee: 'Employee', + employeeDesc: 'View and application permissions', + roleNameRequired: 'Please enter role name', + roleDescRequired: 'Please enter role description', + roleCreated: 'Role created', + permConfig: 'Permission Configuration', + permProject: 'Project Management', + permViewProject: 'View Projects', + permCreateProject: 'Create Projects', + permEditProject: 'Edit Projects', + permDeleteProject: 'Delete Projects', + permFinance: 'Finance Management', + permViewFinance: 'View Finance', + permApproveAdvance: 'Approve Advances', + permApproveReimburse: 'Approve Reimbursements', + permApprovePayment: 'Approve Payments', + permProcurement: 'Procurement Management', + permViewProcurement: 'View Procurement', + permCreateProcurement: 'Create Procurement', + permApproveProcurement: 'Approve Procurement', + permSystem: 'System Settings', + permUserManagement: 'User Management', + permRoleManagement: 'Role Management', + permSystemConfig: 'System Configuration', + }, + + users: { + title: 'User Management', + newUser: 'New User', + editUser: 'Edit User', + id: 'ID', + avatar: 'Avatar', + username: 'Username', + name: 'Name', + email: 'Email', + phone: 'Phone', + role: 'Role', + user: 'User', + action: 'Action', + edit: 'Edit', + resetPassword: 'Reset Password', + confirmDelete: 'Confirm Delete', + confirmDeleteMsg: 'Are you sure you want to delete user {name}?', + usernamePlaceholder: 'Please enter username', + namePlaceholder: 'Please enter name', + selectRole: 'Select Role', + selectRolePlaceholder: 'Please select role', + initialPassword: 'Initial Password', + initialPasswordPlaceholder: 'Please enter initial password', + passwordMinLen: 'Password must be at least 6 characters', + newPasswordPlaceholder: 'Please enter new password', + confirmPassword: 'Confirm Password', + confirmPasswordPlaceholder: 'Please confirm new password', + reEnterPassword: 'Please re-enter new password', + passwordMismatch: 'Passwords do not match', + getListFailed: 'Failed to get user list', + notAdmin: 'Insufficient permissions, admin only', + getUserFailed: 'Failed to get user list', + addSuccess: 'User added', + getListFailedLog: 'Failed to get user list: ', + unknownError: 'Unknown error', + }, + + userManagement: { + title: 'User Management', + testPage: 'This is a test page to check if API calls are working correctly.', + refreshList: 'Refresh User List', + errorPrefix: 'Error: ', + apiResult: 'API returned data:', + loadStatus: 'Load Status:', + loadComplete: 'Load Complete', + apiFailed: 'API returned failure: ', + unknownError: 'Unknown error', + }, +} \ No newline at end of file diff --git a/frontend/src/locales/lo-LA.ts b/frontend/src/locales/lo-LA.ts index ce31607..af9008d 100644 --- a/frontend/src/locales/lo-LA.ts +++ b/frontend/src/locales/lo-LA.ts @@ -1,69 +1,3549 @@ -export default { - // ທົ່ວໄປ - common: { - confirm: 'ຢືນຢັນ', - cancel: 'ຍົກເລີກ', - save: 'ບັນທຶກ', - delete: 'ລຶບ', - edit: 'ແກ້ໄຂ', - add: 'ເພີ່ມ', - search: 'ຄົ້ນຫາ', - reset: 'ຣີເຊັດ', - submit: 'ສົ່ງ', - back: 'ກັບຄືນ', - loading: 'ກຳລັງໂຫລດ...', - success: 'ດຳເນີນການສຳເລັດ', - failed: 'ດຳເນີນການລົ້ມເຫລວ', - required: 'ຈຳເປັນຕ້ອງປ້ອນ' - }, - - // ໜ້າລັອກອິນ - login: { - title: 'Qingyuan Power Laos ERP', - subtitle: 'ແພລດຟອມຈັດການໂຄງການ ແລະ ການເງິນ', - username: 'ຊື່ຜູ້ໃຊ້', - password: 'ລະຫັດຜ່ານ', - loginButton: 'ເຂົ້າສູ່ລະບົບ', - usernamePlaceholder: 'ກະລຸນາປ້ອນຊື່ຜູ້ໃຊ້', - passwordPlaceholder: 'ກະລຸນາປ້ອນລະຫັດຜ່ານ', - usernameRequired: 'ກະລຸນາປ້ອນຊື່ຜູ້ໃຊ້', - passwordRequired: 'ກະລຸນາປ້ອນລະຫັດຜ່ານ', - usernameMin: 'ຊື່ຜູ້ໃຊ້ຕ້ອງມີຢ່າງໜ້ອຍ 3 ຕົວອັກສອນ', - passwordMin: 'ລະຫັດຜ່ານຕ້ອງມີຢ່າງໜ້ອຍ 6 ຕົວອັກສອນ', - loginFailed: 'ການເຂົ້າສູ່ລະບົບລົ້ມເຫລວ ກະລຸນາລອງອີກຄັ້ງ', - testAccounts: 'ບັນຊີທົດສອບ', - techSupport: 'ການສະໜັບສະໜູນເຕັກນິກ: OpenClaw AI + React + Node.js', - selectLanguage: 'ເລືອກພາສາ' - }, - - // ເມນູ - menu: { - dashboard: 'ແດຊບອດ', - projects: 'ຈັດການໂຄງການ', - advances: 'ຈັດການເງິນທືນ', - reimbursements: 'ຈັດການເບີກຈ່າຍ', - finance: 'ຈັດການການເງິນ', - reports: 'ລາຍງານ', - settings: 'ຕັ້ງຄ່າລະບົບ' - }, - - // ຜູ້ໃຊ້ - user: { - profile: 'ຂໍ້ມູນສ່ວນຕົວ', - settings: 'ຕັ້ງຄ່າລະບົບ', - logout: 'ອອກຈາກລະບົບ', - admin: 'ຜູ້ບໍລິຫານລະບົບ', - finance: 'ເຈົ້າໜ້າທີ່ການເງິນ', - manager: 'ຜູ້ຈັດການໂຄງການ', - employee: 'ພະນັກງານ' - }, - - // ຄຸນສົມບັດລະບົບ - features: { - projectManage: 'ຈັດການໂຄງການ: ສ້າງ ຕິດຕາມ ແລະ ວິເຄາະຄວາມຄືບໜ້າ', - advanceManage: 'ຈັດການເງິນທືນ: ຂະບວນການຂໍ ແລະ ອະນຸມັດ', - reimburseManage: 'ຈັດການເບີກຈ່າຍ: ຂະບວນການເບີກຄ່າໃຊ້ຈ່າຍ', - financeReport: 'ລາຍງານການເງິນ: ວິເຄາະຕົ້ນທຶນ ແລະ ກຳໄລໂຄງການ', - mobileSupport: 'ຮອງຮັບມືຖື: ເຕັກໂນໂລຊີ PWA ສາມາດເພີ່ມໃສ່ໜ້າຈໍຫຼັກ' - } -} +export default { + + common: { + confirm: 'ຢືນຢັນ', + cancel: 'ຍົກເລີກ', + save: 'ບັນທຶກ', + delete: 'ລຶບ', + edit: 'ແກ້ໄຂ', + add: 'ເພີ່ມ', + search: 'ຄົ້ນຫາ', + reset: 'ຣີເຊັດ', + submit: 'ສົ່ງ', + back: 'ກັບຄືນ', + loading: 'ກຳລັງໂຫຼດ...', + success: 'ດຳເນີນການສຳເລັດ', + failed: 'ດຳເນີນການລົ້ມເຫຼວ', + required: 'ລາຍການນີ້ຈຳເປັນຕ້ອງປ້ອນ', + close: 'ປິດ', + view: 'ເບິ່ງ', + refresh: 'ໂຫຼດໃໝ່', + create: 'ສ້າງ', + upload: 'ອັບໂຫຼດ', + download: 'ດາວໂຫຼດ', + export: 'ສົ່ງອອກ', + import: 'ນຳເຂົ້າ', + copy: 'ສຳເນົາ', + detail: 'ລາຍລະອຽດ', + status: 'ສະຖານະ', + action: 'ດຳເນີນການ', + name: 'ຊື່', + remark: 'ໝາຍເຫດ', + date: 'ວັນທີ', + amount: 'ຈຳນວນເງິນ', + total: 'ລວມ', + unit: 'ອັນ', + meter: 'ແມັດ', + currency: 'ສະກຸນເງິນ', + country: 'ປະເທດ', + phone: 'ໂທລະສັບ', + email: 'ອີເມວ', + address: 'ທີ່ຢູ່', + position: 'ຕຳແໜ່ງ', + is: 'ແມ່ນ', + no: 'ບໍ່', + days: 'ມື້', + tenThousand: 'ໝື່ນ', + yuan: 'ຢວນ', + sheet: 'ແຜ່ນ', + item: 'ລາຍການ', + photo: 'ຮູບພາບ', + person: 'ຄົນ', + today: 'ມື້ນີ້', + unknown: 'ບໍ່ຮູ້ຈັກ', + none: 'ບໍ່ມີ', + all: 'ທັງໝົດ', + retry: 'ລອງໃໝ່', + inputPassword: 'ກະລຸນາປ້ອນລະຫັດຜ່ານ', + deleteConfirm: 'ຢືນຢັນການລຶບ', + deleteWarning: 'ການດຳເນີນການນີ້ບໍ່ສາມາດກູ້ຄືນໄດ້', + draftFound: 'ພົບຮ່າງທີ່ຍັງບໍ່ສຳເລັດ', + draftRestore: 'ກວດພົບຂໍ້ມູນທີ່ຍັງບໍ່ໄດ້ສົ່ງ, ຕ້ອງການກູ້ຄືນບໍ?', + restoreDraft: 'ກູ້ຄືນຮ່າງ', + reFill: 'ປ້ອນໃໝ່', + closeConfirm: 'ຢືນຢັນການປິດ', + closeConfirmMsg: 'ຂໍ້ມູນຟອມຍັງບໍ່ໄດ້ບັນທຶກ, ຫຼັງປິດສາມາດກູ້ຄືນຜ່ານຮ່າງ. ຢືນຢັນການປິດ?', + continueEdit: 'ສືບຕໍ່ແກ້ໄຂ', + noData: 'ບໍ່ມີຂໍ້ມູນ', + loadingData: 'ກຳລັງໂຫຼດຂໍ້ມູນໂຄງການ...', + noProjectData: 'ບໍ່ມີຂໍ້ມູນໂຄງການ', + operationFailed: 'ດຳເນີນການລົ້ມເຫຼວ', + saveFailed: 'ບັນທຶກລົ້ມເຫຼວ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + networkError: 'ເຄືອຂ່າຍຜິດພາດ', + totalCount: 'ທັງໝົດ {total} ລາຍການ', + systemAdmin: 'ຜູ້ເບິ່ງແຍງລະບົບ', + currentUser: 'ຜູ້ໃຊ້ປັດຈຸບັນ', + unnamed: 'ບໍ່ມີຊື່', + notSet: 'ບໍ່ໄດ້ຕັ້ງຄ່າ', + pleaseSelect: 'ກະລຸນາເລືອກ', + inputPlaceholder: 'ກະລຸນາປ້ອນ', + selectPlaceholder: 'ເລືອກ', + confirmDelete: 'ຢືນຢັນການລຶບ?', + confirmDeleteMsg: 'ຢືນຢັນວ່າຕ້ອງການລຶບບໍ?', + saveSuccess: 'ບັນທຶກສຳເລັດ', + createSuccess: 'ສ້າງສຳເລັດ', + deleteSuccess: 'ລຶບສຳເລັດ', + updateSuccess: 'ອັບເດດສຳເລັດ', + }, + + login: { + title: 'ຊິງຢວນ ໄຟຟ້າລາວ ERP', + subtitle: 'ແພລດຟອມຄຸ້ມຄອງໂຄງການ ແລະ ການເງິນ', + username: 'ຊື່ຜູ້ໃຊ້', + password: 'ລະຫັດຜ່ານ', + loginButton: 'ເຂົ້າສູ່ລະບົບ', + usernamePlaceholder: 'ກະລຸນາປ້ອນຊື່ຜູ້ໃຊ້', + passwordPlaceholder: 'ກະລຸນາປ້ອນລະຫັດຜ່ານ', + usernameRequired: 'ກະລຸນາປ້ອນຊື່ຜູ້ໃຊ້', + passwordRequired: 'ກະລຸນາປ້ອນລະຫັດຜ່ານ', + usernameMin: 'ຕ້ອງມີຢ່າງໜ້ອຍ 3 ຕົວ', + passwordMin: 'ຕ້ອງມີຢ່າງໜ້ອຍ 6 ຕົວ', + loginFailed: 'ເຂົ້າສູ່ລະບົບລົ້ມເຫຼວ', + testAccounts: 'ບັນຊີທົດສອບ', + techSupport: 'ສະໜັບສະໜູນ: OpenClaw AI + React + Node.js', + selectLanguage: 'ເລືອກພາສາ', + passwordError: 'ຊື່ຜູ້ໃຊ້ ຫຼື ລະຫັດຜ່ານບໍ່ຖືກ', + serverError: 'ເຊີບເວີຜິດພາດ, ລອງໃໝ່ພາຍຫຼັງ', + statusCodeError: 'ເຂົ້າສູ່ລະບົບລົ້ມເຫຼວ (ລະຫັດ: {code})', + }, + + menu: { + dashboard: 'ໜ້າຫຼັກ', + projects: 'ຈັດການໂຄງການ', + budgetQuotation: 'ງົບປະມານ/ໃບສະເໜີລາຄາ', + construction: 'ຈັດການກໍ່ສ້າງ', + constructionOverview: 'ພາບລວມການກໍ່ສ້າງ', + approval: 'ຈັດການອະນຸມັດ', + pendingApproval: 'ລໍອະນຸມັດ', + pendingExecution: 'ລໍດຳເນີນການ', + financeDocs: 'ເອກະສານການເງິນ', + advanceApply: 'ຄຳຂໍເງິນລ່ວງໜ້າ', + reimburseApply: 'ຄຳຂໍເບີກຄ່າໃຊ້ຈ່າຍ', + paymentApply: 'ຄຳຂໍຈ່າຍເງິນ', + verificationApply: 'ຄຳຂໍກວດສອບ', + financeManagement: 'ຈັດການການເງິນ', + financeOverview: 'ພາບລວມການເງິນ', + exchangeRate: 'ຈັດການອັດຕາແລກປ່ຽນ', + projectCost: 'ຕົ້ນທຶນໂຄງການ', + advanceVerificationStatus: 'ສະຖານະກວດສອບເງິນລ່ວງໜ້າ', + reports: 'ລາຍງານວິເຄາະ', + procurement: 'ຈັດການຈັດຊື້', + productManagement: 'ຈັດການສິນຄ້າ', + purchaseRequest: 'ຄຳຂໍຈັດຊື້', + purchaseOrder: 'ໃບສັ່ງຊື້', + paymentPlan: 'ແຜນຈ່າຍເງິນ', + inventory: 'ຈັດການສາງ', + partners: 'ຄູ່ຮ່ວມງານ', + supplierManagement: 'ຈັດການຜູ້ສະໜອງ', + subcontractorManagement: 'ຈັດການຜູ້ຮັບເໝົາຊ່ວງ', + customerManagement: 'ຈັດການລູກຄ້າ', + logisticsManagement: 'ຈັດການໂລຈິສຕິກ', + admin: 'ຈັດການລະບົບ', + userManagement: 'ຈັດການຜູ້ໃຊ້', + rolePermission: 'ສິດບົດບາດ', + processManagement: 'ຈັດການຂະບວນການ', + templateManagement: 'ຈັດການແມ່ແບບ', + expenseCategory: 'ຈັດການໝວດການເງິນ', + excelImport: 'ນຳເຂົ້າ Excel', + systemLogs: 'ບັນທຶກລະບົບ', + dataBackup: 'ສຳຮອງຂໍ້ມູນ', + aboutSystem: 'ກ່ຽວກັບລະບົບ', + profile: 'ຂໍ້ມູນສ່ວນຕົວ', + settings: 'ຕັ້ງຄ່າລະບົບ', + logout: 'ອອກຈາກລະບົບ', + backToFront: 'ກັບໜ້າຫຼັກ', + collapse: 'ຫຍໍ້ເມນູ', + }, + + user: { + profile: 'ຂໍ້ມູນສ່ວນຕົວ', + settings: 'ຕັ້ງຄ່າລະບົບ', + logout: 'ອອກຈາກລະບົບ', + admin: 'ຜູ້ເບິ່ງແຍງ', + finance: 'ເຈົ້າໜ້າທີ່ການເງິນ', + manager: 'ຜູ້ຈັດການໂຄງການ', + employee: 'ພະນັກງານ', + userLabel: 'ຜູ້ໃຊ້', + role: 'ບົດບາດ', + managingProfile: 'ຈັດການຂໍ້ມູນບັນຊີ', + name: 'ຊື່', + phone: 'ໂທລະສັບ', + email: 'ອີເມວ', + username: 'ຊື່ຜູ້ໃຊ້', + clickToChangeAvatar: 'ກົດເພື່ອປ່ຽນຮູບ', + idDocument: 'ເອກະສານປະຈຳຕົວ', + idDocTip: 'ຮູບໜັງສືຜ່ານແດນ ແລະ ໃບຂັບຂີ່, ກົດ "ປ່ຽນ" ເພື່ອອັບໂຫຼດ', + passport: 'ໜັງສືຜ່ານແດນ', + driverLicense: 'ໃບຂັບຂີ່', + uploadPassport: 'ກົດເພື່ອອັບໂຫຼດຮູບໜັງສືຜ່ານແດນ', + uploadDriverLicense: 'ກົດເພື່ອອັບໂຫຼດຮູບໃບຂັບຂີ່', + deletePassportConfirm: 'ຢືນຢັນລຶບຮູບໜັງສືຜ່ານແດນ?', + deleteDriverLicenseConfirm: 'ຢືນຢັນລຶບຮູບໃບຂັບຂີ່?', + saveProfile: 'ບັນທຶກ', + changePassword: 'ປ່ຽນລະຫັດຜ່ານ', + currentPassword: 'ລະຫັດຜ່ານປັດຈຸບັນ', + currentPasswordPlaceholder: 'ປ້ອນລະຫັດຜ່ານປັດຈຸບັນ', + newPassword: 'ລະຫັດຜ່ານໃໝ່', + newPasswordPlaceholder: 'ປ້ອນລະຫັດຜ່ານໃໝ່', + confirmPassword: 'ຢືນຢັນລະຫັດຜ່ານ', + confirmPasswordPlaceholder: 'ຢືນຢັນລະຫັດຜ່ານໃໝ່', + passwordMinLen: 'ຕ້ອງມີ 6 ຕົວຂຶ້ນໄປ', + passwordMismatch: 'ລະຫັດຜ່ານບໍ່ກົງກັນ', + profileUpdated: 'ອັບເດດຂໍ້ມູນແລ້ວ', + passwordUpdated: 'ປ່ຽນລະຫັດຜ່ານແລ້ວ', + updateFailed: 'ອັບເດດລົ້ມເຫຼວ', + updateRetry: 'ອັບເດດລົ້ມເຫຼວ, ລອງໃໝ່', + passwordUpdateFailed: 'ປ່ຽນລະຫັດລົ້ມເຫຼວ', + passwordUpdateRetry: 'ປ່ຽນລະຫັດລົ້ມເຫຼວ, ລອງໃໝ່', + replace: 'ປ່ຽນ', + uploadFailed: 'ອັບໂຫຼດລົ້ມເຫຼວ', + }, + + features: { + projectManage: 'ຈັດການໂຄງການ: ສ້າງ, ຕິດຕາມ, ວິເຄາະຄວາມຄືບໜ້າ', + advanceManage: 'ຈັດການເງິນລ່ວງໜ້າ: ຂໍ ແລະ ອະນຸມັດ', + reimburseManage: 'ຈັດການເບີກຈ່າຍ: ເບີກຄ່າໃຊ້ຈ່າຍ ແລະ ກວດສອບ', + financeReport: 'ລາຍງານການເງິນ: ວິເຄາະຕົ້ນທຶນ ແລະ ກຳໄລ', + mobileSupport: 'ຮອງຮັບມືຖື: ເທັກໂນໂລຢີ PWA, ເພີ່ມໃສ່ໜ້າຈໍຫຼັກໄດ້', + }, + + dashboard: { + title: '📊 ໜ້າຫຼັກ', + planning: 'ກຳລັງວາງແຜນ', + inProgress: 'ກຳລັງດຳເນີນ', + completed: 'ສຳເລັດແລ້ວ', + projectName: 'ຊື່ໂຄງການ', + budget: 'ງົບປະມານ', + spent: 'ໃຊ້ຈ່າຍແລ້ວ', + project: 'ໂຄງການ', + budgetSpent: 'ງົບປະມານ/ໃຊ້ຈ່າຍ', + budgetLabel: 'ງົບປະມານ: ', + spentLabel: 'ໃຊ້ຈ່າຍແລ້ວ: ', + inProgressProjects: 'ໂຄງການທີ່ກຳລັງດຳເນີນ', + monthlyReimburse: 'ເບີກຈ່າຍເດືອນນີ້', + pendingApproval: 'ລໍອະນຸມັດ', + teamMembers: 'ສະມາຊິກທີມ', + recentProjects: 'ໂຄງການຫຼ້າສຸດ', + }, + + project: { + title: 'ຈັດການໂຄງການ', + description: 'ຈັດການຂໍ້ມູນໂຄງການ, ຄວາມຄືບໜ້າ ແລະ ງົບປະມານ', + list: 'ລາຍການໂຄງການ', + quickCreate: 'ສ້າງໂຄງການດ່ວນ', + newProject: 'ສ້າງໂຄງການໃໝ່', + editProject: 'ແກ້ໄຂໂຄງການ', + deleteProject: 'ລຶບໂຄງການ', + deleteConfirm: 'ຢືນຢັນລຶບ', + deleteConfirmMsg: 'ຢືນຢັນລຶບໂຄງການນີ້? ບໍ່ສາມາດກູ້ຄືນໄດ້.', + deletePassMsg: 'ກະລຸນາປ້ອນລະຫັດຜ່ານຜູ້ເບິ່ງແຍງ:', + projectName: 'ຊື່ໂຄງການ', + projectNamePlaceholder: 'ເຊັ່ນ: ໂຄງການສາຍສົ່ງ 22kV', + projectTemplate: 'ແມ່ແບບໂຄງການ', + selectTemplate: 'ເລືອກແມ່ແບບໂຄງການ (ບໍ່ບັງຄັບ)', + projectManager: 'ຜູ້ຈັດການ', + budget: 'ງົບປະມານ', + progress: 'ຄວາມຄືບໜ້າ', + status: 'ສະຖານະ', + planning: 'ວາງແຜນ', + inProgress: 'ດຳເນີນການ', + completed: 'ສຳເລັດ', + paused: 'ຢຸດຊົ່ວຄາວ', + plan: 'ວາງແຜນ', + complete: 'ສຳເລັດ', + pause: 'ຢຸດ', + customer: 'ລູກຄ້າ', + selectCustomer: 'ເລືອກລູກຄ້າ', + selectManager: 'ເລືອກຜູ້ຈັດການ', + contractAmount: 'ມູນຄ່າສັນຍາ', + projectStatus: 'ສະຖານະໂຄງການ', + completedHistory: 'ສຳເລັດ (ບັນທຶກຍ້ອນຫຼັງ)', + startDate: 'ວັນທີເລີ່ມ', + endDate: 'ວັນທີສິ້ນສຸດ', + location: 'ສະຖານທີ່', + locationPlaceholder: 'ເຊັ່ນ: ແຂວງວຽງຈັນ', + descriptionPlaceholder: 'ອະທິບາຍເນື້ອໃນ', + createSuccess: 'ສ້າງສຳເລັດ', + createFailed: 'ສ້າງລົ້ມເຫຼວ', + deleteSuccess: 'ລຶບສຳເລັດ', + getListFailed: 'ດຶງລາຍການລົ້ມເຫຼວ', + unassigned: 'ບໍ່ໄດ້ມອບໝາຍ', + passwordError: 'ລະຫັດບໍ່ຖືກ', + projectCode: 'ລະຫັດໂຄງການ', + basicInfo: 'ຂໍ້ມູນພື້ນຖານ', + contractDetails: 'ສັນຍາ/ຮັບເງິນ', + financeDetails: 'ລາຍຮັບ-ລາຍຈ່າຍ', + editBasicInfo: 'ແກ້ໄຂຂໍ້ມູນພື້ນຖານ', + basicInfoSaved: 'ບັນທຶກຂໍ້ມູນພື້ນຖານແລ້ວ', + selectStartDate: 'ເລືອກວັນທີເລີ່ມ', + selectEndDate: 'ເລືອກວັນທີສິ້ນສຸດ', + durationDays: 'ມື້ກໍ່ສ້າງ', + durationDaysPlaceholder: 'ປ້ອນຈຳນວນມື້', + overview: 'ພາບລວມວິສະວະກຳ', + overviewPlaceholder: 'ປ້ອນພາບລວມ', + createdAt: 'ເວລາສ້າງ', + returnToList: 'ກັບໄປລາຍການ', + unknownManager: 'ບໍ່ຮູ້ຜູ້ຈັດການ', + notFound: 'ໂຄງການບໍ່ພົບ', + getInfoFailed: 'ດຶງຂໍ້ມູນລົ້ມເຫຼວ', + enterConstruction: 'ເຂົ້າສູ່ຈັດການກໍ່ສ້າງ', + contractNo: 'ເລກທີສັນຍາ', + contractType: 'ປະເພດສັນຍາ', + unitPriceContract: 'ສັນຍາລາຄາຕໍ່ໜ່ວຍ', + includeTax: 'ລວມອາກອນ', + settlementType: 'ວິທີຊຳລະ', + lumpSum: 'ລາຄາລວມເໝົາ', + unitPrice: 'ຊຳລະຕາມລາຄາໜ່ວຍ', + contractTotal: 'ມູນຄ່າສັນຍາລວມ', + contractTotalPlaceholder: 'ປ້ອນມູນຄ່າສັນຍາ', + contractTax: 'ສັນຍາລວມອາກອນ', + paymentMilestones: 'ຈຸດຈ່າຍເງິນ', + milestoneName: 'ຊື່ຈຸດ', + milestoneCondition: 'ເງື່ອນໄຂ', + milestoneRatio: 'ອັດຕາສ່ວນ(%)', + milestoneAmount: 'ຈຳນວນເງິນ', + milestoneStatus: 'ຄວາມຄືບໜ້າ', + pendingMilestone: 'ລໍເລີ່ມ', + noMilestone: 'ບໍ່ມີຈຸດ', + noMilestoneRecord: 'ບໍ່ມີບັນທຶກ', + addMilestone: 'ເພີ່ມຈຸດ', + milestoneNotReached: 'ຍັງບໍ່ຮອດຈຸດ', + contractAttachment: 'ໄຟລ໌ແນບສັນຍາ', + contractFile: 'ໄຟລ໌ສັນຍາ', + viewContract: 'ເບິ່ງໄຟລ໌', + noContractAttachment: 'ບໍ່ມີໄຟລ໌ແນບ', + otherContractInfo: 'ຂໍ້ມູນສັນຍາອື່ນ', + otherInfo: 'ຂໍ້ມູນອື່ນ', + otherInfoPlaceholder: 'ປ້ອນຂໍ້ມູນເພີ່ມ', + warranty: 'ຕັ້ງຄ່າເງິນປະກັນ', + hasWarranty: 'ມີເງິນປະກັນ', + warrantyRatio: 'ອັດຕາເງິນປະກັນ(%)', + warrantyAmount: 'ຈຳນວນເງິນປະກັນ', + warrantyPeriod: 'ໄລຍະປະກັນ', + warrantyExpiry: 'ວັນໝົດອາຍຸ', + warrantyStatus: 'ສະຖານະປະກັນ', + warrantyReleased: 'ປ່ອຍແລ້ວ', + warrantyPending: 'ລໍປ່ອຍ', + contractSaveSuccess: 'ບັນທຶກສັນຍາສຳເລັດ', + draft: 'ຮ່າງ', + replaceFile: 'ປ່ຽນໄຟລ໌', + clickUpload: 'ກົດອັບໂຫຼດ', + fileUploadFailed: 'ອັບໂຫຼດລົ້ມເຫຼວ', + subcontract: 'ຈັດການຮັບເໝົາຊ່ວງ', + addSubcontract: 'ເພີ່ມຮັບເໝົາຊ່ວງ', + subcontractor: 'ຜູ້ຮັບເໝົາຊ່ວງ', + subcontractorName: 'ຊື່ຮັບເໝົາຊ່ວງ', + subcontractorNamePlaceholder: 'ປ້ອນຊື່', + paidAmount: 'ຈ່າຍແລ້ວ', + noSubcontract: 'ບໍ່ມີບັນທຶກ', + subcontractDetail: 'ລາຍລະອຽດ', + startDateRequired: 'ເລືອກວັນເລີ່ມ', + endDateRequired: 'ເລືອກວັນສິ້ນສຸດ', + otherTerms: 'ຂໍ້ຕົກລົງອື່ນ', + otherTermsPlaceholder: 'ປ້ອນຂໍ້ຕົກລົງ', + paymentNote: 'ໝາຍເຫດຈ່າຍ', + paymentNotePlaceholder: 'ປ້ອນໝາຍເຫດ', + addSubSuccess: 'ເພີ່ມສຳເລັດ', + addSubFailed: 'ເພີ່ມລົ້ມເຫຼວ', + projectItems: 'ລາຄາລາຍການ', + quantity: 'ຈຳນວນ', + unitPriceLabel: 'ລາຄາຕໍ່ໜ່ວຍ', + totalPrice: 'ລາຄາລວມ', + addItem: '+ ເພີ່ມລາຍການ', + noItems: 'ບໍ່ມີລາຍການ', + material: 'ວັດສະດຸ', + materialName: 'ຊື່ວັດສະດຸ', + budgetQty: 'ຈຳນວນຕາມແຜນ', + purchaseQty: 'ຈຳນວນຊື້', + usedQty: 'ຈຳນວນໃຊ້', + avgPrice: 'ລາຄາສະເລ່ຍ', + noMaterial: 'ບໍ່ມີບັນທຶກ', + constructionNode: 'ຈຸດກໍ່ສ້າງ', + contractMilestone: 'ຈຸດຈ່າຍຕາມສັນຍາ', + milestoneDesc: 'ສະຖານະຈຸດ', + plannedDate: 'ວັນທີແຜນ', + actualDate: 'ວັນທີຕົວຈິງ', + uploadProof: 'ອັບຫຼັກຖານ', + noRecord: 'ບໍ່ມີບັນທຶກ', + constructionLog: 'ບັນທຶກກໍ່ສ້າງ', + addLog: 'ເພີ່ມບັນທຶກ', + weather: 'ສະພາບອາກາດ', + recorder: 'ຜູ້ບັນທຶກ', + todayWork: 'ວຽກມື້ນີ້', + viewPhoto: 'ເບິ່ງຮູບ', + noLog: 'ບໍ່ມີບັນທຶກ', + finance: 'ການເງິນ', + received: 'ຮັບແລ້ວ', + totalExpense: 'ລາຍຈ່າຍລວມ', + grossProfit: 'ກຳໄລລວມ', + marginRate: 'ອັດຕາກຳໄລ', + addReceipt: 'ເພີ່ມລາຍຮັບ', + receiptRecords: 'ບັນທຶກລາຍຮັບ', + noReceipts: 'ຍັງບໍ່ມີລາຍຮັບ', + receiptType: 'ປະເພດລາຍຮັບ', + receiptTypeNode: 'ຮັບຕາມຂັ້ນຕອນ', + receiptTypeAdvance: 'ເງິນລ່ວງໜ້າຈາກລູກຄ້າ', + receiptTypeOther: 'ລາຍຮັບອື່ນໆ', + receiptDate: 'ວັນທີຮັບ', + receiptNode: 'ຂັ້ນຕອນທີ່ກ່ຽວຂ້ອງ', + receiptAmount: 'ຈຳນວນເງິນ', + receiptAmountCNY: 'ຈຳນວນເງິນ (CNY)', + receiptDesc: 'ລາຍລະອຽດ', + receiptDescPlaceholder: 'ປ້ອນລາຍລະອຽດ', + receiptAdded: 'ເພີ່ມລາຍຮັບສຳເລັດ', + selectMilestone: 'ຂັ້ນຕອນການຊຳລະ', + selectMilestonePlaceholder: 'ເລືອກຂັ້ນຕອນການຊຳລະ', + selectMilestoneRequired: 'ກະລຸນາເລືອກຂັ້ນຕອນການຊຳລະ', + payer: 'ຜູ້ຊຳລະ', + payerPlaceholder: 'ປ້ອນຊື່ຜູ້ຊຳລະ', + exchangeRate: 'ອັດຕາແລກປ່ຽນ', + voucher: 'ໃບຮັບເງິນ', + uploadVoucher: 'ອັບໂຫຼດໃບຮັບ', + viewVoucher: 'ເບິ່ງ', + expenseBreakdown: 'ລາຍລະອຽດລາຍຈ່າຍ', + category: 'ໝວດ', + categoryAmount: 'ຈຳນວນ(¥)', + count: 'ຈຳນວນລາຍການ', + ratio: 'ອັດຕາ', + personnelExpense: 'ລາຍຈ່າຍບຸກຄະລາກອນ', + personnel: 'ບຸກຄະລາກອນ', + warrantyManagement: 'ຈັດການເງິນປະກັນ', + warrantyStartDate: 'ວັນເລີ່ມຄິດໄລຍະ', + markReleased: 'ໝາຍປ່ອຍ', + extendWarranty: 'ຂະຫຍາຍເວລາ', + currentLabel: 'ປັດຈຸບັນ: ', + progressLabel: 'ຄວາມຄືບໜ້າ: ', + warrantyLabel: 'ປະກັນ: ', + currencyCNY: 'ຢວນ(CNY)', + currencyUSD: 'ໂດລາ(USD)', + currencyLAK: 'ກີບ(LAK)', + currencyTHB: 'ບາດ(THB)', + amountRequired: 'ປ້ອນມູນຄ່າ', + settlementRequired: 'ເລືອກວິທີຊຳລະ', + nodeNameRequired: 'ປ້ອນຊື່ຈຸດ', + nodeConditionRequired: 'ປ້ອນເງື່ອນໄຂ', + ratioRequired: 'ປ້ອນອັດຕາ', + milestoneAmountRequired: 'ປ້ອນຈຳນວນ', + statusRequired: 'ເລືອກສະຖານະ', + }, + + construction: { + overview: 'ພາບລວມກໍ່ສ້າງ', + noProjects: 'ບໍ່ມີໂຄງການ', + underConstruction: 'ກຳລັງກໍ່ສ້າງ', + pendingStart: 'ລໍເລີ່ມ', + completed: 'ສຳເລັດ', + paused: 'ຢຸດ', + currentPhase: 'ໄລຍະ: ', + completedProjects: 'ສຳເລັດ: ', + enter: 'ເຂົ້າ', + getListFailed: 'ດຶງລາຍການລົ້ມເຫຼວ', + progress: 'ຄວາມຄືບໜ້າ', + todayLog: 'ບັນທຶກມື້ນີ້: ', + todayLogEmpty: 'ຍັງບໍ່ໄດ້ຂຽນ', + writeLog: 'ຂຽນບັນທຶກ', + constructionLog: 'ບັນທຶກກໍ່ສ້າງ', + uploadPhoto: 'ອັບຮູບ', + milestoneProgress: 'ຄວາມຄືບໜ້າຈຸດ', + management: 'ຈັດການກໍ່ສ້າງ', + description: 'ເບິ່ງ ແລະ ຈັດການໂຄງການກໍ່ສ້າງ', + noConstructionProjects: 'ບໍ່ມີໂຄງການກໍ່ສ້າງ', + contactAdmin: 'ຕິດຕໍ່ຜູ້ເບິ່ງແຍງ', + myProjects: 'ໂຄງການຂອງຂ້ອຍ', + customerLabel: 'ລູກຄ້າ: ', + getInfoFailed: 'ດຶງຂໍ້ມູນລົ້ມເຫຼວ', + getPhaseFailed: 'ດຶງຂໍ້ມູນໄລຍະລົ້ມເຫຼວ', + phaseComplete: 'ສຳເລັດໄລຍະ! ', + advancedTo: 'ກ້າວໄປສູ່: ', + reopenPhase: 'ເປີດໄລຍະຄືນ', + reopenConfirm: 'ຢືນຢັນເປີດຄືນ? ຄວາມຄືບໜ້າຈະຖອຍກັບ.', + phaseReopened: 'ເປີດໄລຍະຄືນແລ້ວ', + updateItemFailed: 'ອັບເດດລາຍການລົ້ມເຫຼວ', + returnOverview: 'ກັບໄປພາບລວມ', + currentLabel: 'ປັດຈຸບັນ: ', + parallelPhase: 'ໄລຍະຂະໜານ', + completionStandard: 'ມາດຕະຖານ: ', + remarkOptional: 'ໝາຍເຫດ: ', + remarkPlaceholder: 'ຂຽນໝາຍເຫດ...', + confirmCompleteMsg: 'ຢືນຢັນສຳເລັດໄລຍະນີ້', + subsequentPhases: 'ໄລຍະຕໍ່ໄປ', + parallelLabel: 'ຂະໜານ', + phaseHistory: 'ປະຫວັດ', + rollback: 'ຖອຍກັບ', + projectCompleted: 'ໂຄງການສຳເລັດແລ້ວ', + projectCompletedDesc: 'ໂຄງການສຳເລັດ, ບໍ່ຕ້ອງກ້າວໄປຕໍ່', + viewProjectDetail: 'ເບິ່ງລາຍລະອຽດ', + notInitialized: 'ຍັງບໍ່ໄດ້ຕັ້ງຄ່າໄລຍະ', + notInitializedDesc: 'ເລືອກແມ່ແບບເພື່ອຕັ້ງຄ່າ', + goToProjectDetail: 'ໄປລາຍລະອຽດ', + confirmCompleteTitle: 'ຢືນຢັນສຳເລັດໄລຍະ', + confirmCompleteDesc: 'ຢືນຢັນສຳເລັດ? ລະບົບຈະກ້າວໄປຕໍ່.', + remarkLabel: 'ໝາຍເຫດ: ', + uploadProofLabel: 'ອັບຫຼັກຖານ: ', + selectFile: 'ເລືອກໄຟລ໌', + supportFormats: 'ຮອງຮັບຮູບ, PDF, Word, Excel', + sunny: 'ແດດ', + cloudy: 'ມີເມກ', + rain: 'ຝົນ', + thunderstorm: 'ຟ້າຮ້ອງ', + windy: 'ລົມແຮງ', + getLogFailed: 'ດຶງບັນທຶກລົ້ມເຫຼວ', + logAddSuccess: 'ເພີ່ມບັນທຶກສຳເລັດ', + logAddFailed: 'ເພີ່ມບັນທຶກລົ້ມເຫຼວ', + logDeleteSuccess: 'ລຶບບັນທຶກສຳເລັດ', + logDeleteFailed: 'ລຶບບັນທຶກລົ້ມເຫຼວ', + yearMonth: 'YYYY ປີ MM ເດືອນ', + monthDay: 'MM ເດືອນ DD ວັນ', + recorderLabel: 'ຜູ້ບັນທຶກ: ', + deleteLogConfirmTitle: 'ຢືນຢັນລຶບບັນທຶກ?', + deleteLogConfirmDesc: 'ລຶບແລ້ວບໍ່ສາມາດກູ້ຄືນ', + todayWorkLabel: 'ວຽກມື້ນີ້:', + tomorrowPlanLabel: 'ແຜນມື້ອື່ນ:', + issueRecordLabel: 'ບັນຫາ:', + constructionPhoto: 'ຮູບກໍ່ສ້າງ', + noLog: 'ບໍ່ມີບັນທຶກ', + addFirstLog: 'ເພີ່ມບັນທຶກທຳອິດ', + newLog: 'ບັນທຶກໃໝ່', + newConstructionLog: 'ບັນທຶກກໍ່ສ້າງໃໝ່', + selectDate: 'ເລືອກວັນທີ', + selectWeather: 'ເລືອກສະພາບອາກາດ', + inputTodayWork: 'ປ້ອນເນື້ອໃນວຽກ', + todayWorkPlaceholder: 'ວຽກທີ່ສຳເລັດມື້ນີ້...', + tomorrowPlanPlaceholder: 'ແຜນມື້ອື່ນ...', + issuePlaceholder: 'ບັນຫາທີ່ພົບ...', + addPhoto: 'ເພີ່ມຮູບ', + multiPhotoSupport: 'ສູງສຸດ 9 ຮູບ', + plannedComplete: 'ແຜນສຳເລັດ: ', + overallProgress: 'ຄວາມຄືບໜ້າລວມ', + totalNodes: 'ຈຸດທັງໝົດ', + noMilestones: 'ບໍ່ມີຈຸດ', + milestoneConfigured: 'ຕັ້ງຄ່າໂດຍຜູ້ຈັດການ', + inProgress: 'ດຳເນີນ', + cancelled: 'ຍົກເລີກ', + progressTab: 'ຄວາມຄືບໜ້າ', + documentsTab: 'ເອກະສານ', + logsTab: 'ບັນທຶກກໍ່ສ້າງ', + markComplete: 'ໝາຍວ່າແລ້ວ', + phasesCompleted: 'ຂັ້ນຕອນສຳເລັດ', + initPhases: 'ເລີ່ມຕົ້ນຂັ້ນຕອນ', + selectTemplateInit: 'ເລືອກແມ່ແບບແລະເລີ່ມຕົ້ນ', + selectTemplate: 'ກະລຸນາເລືອກແມ່ແບບ', + initSuccess: 'ເລີ່ມຕົ້ນຂັ້ນຕອນສຳເລັດ', + completedAt: 'ສຳເລັດເມື່ອ', + completionTime: 'ເວລາສຳເລັດ', + proofPhotos: 'ຮູບພາບຫຼັກຖານ', + optional: 'ບໍ່ບັງຄັບ', + uploadImage: 'ອັບໂຫຼດຮູບ', + uploadDocument: 'ອັບໂຫຼດເອກະສານ', + imageDocs: 'ຮູບພາບ', + fileDocs: 'ເອກະສານ', + noImages: 'ຍັງບໍ່ມີຮູບພາບ', + noDocuments: 'ຍັງບໍ່ມີເອກະສານ', + fileName: 'ຊື່ໄຟລ໌', + uploader: 'ຜູ້ອັບໂຫຼດ', + uploadTime: 'ເວລາອັບໂຫຼດ', + descriptionPlaceholder: 'ປ້ອນຄຳອະທິບາຍ', + clickUpload: 'ຄລິກເພື່ອອັບໂຫຼດ', + uploadSuccess: 'ອັບໂຫຼດສຳເລັດ', + addLog: 'ເພີ່ມບັນທຶກ', + logAdded: 'ເພີ່ມບັນທຶກສຳເລັດ', + logDate: 'ວັນທີບັນທຶກ', + weather: 'ອາກາດ', + weatherSunny: 'ແດດ', + weatherCloudy: 'ຄືນເມກ', + weatherRainy: 'ຝົນ', + weatherStormy: 'ພະຍຸ', + weatherWindy: 'ລົມແຮງ', + workContent: 'ເນື້ອໃນວຽກ', + workContentPlaceholder: 'ປ້ອນເນື້ອໃນວຽກມື້ນີ້', + nextPlan: 'ແຜນມື້ອື່ນ', + nextPlanPlaceholder: 'ປ້ອນແຜນວຽກມື້ອື່ນ', + issues: 'ບັນຫາ', + issuesPlaceholder: 'ປ້ອນບັນຫາທີ່ພົບ', + sitePhotos: 'ຮູບຖ່າຍສະຖານທີ່', + noLogs: 'ຍັງບໍ່ມີບັນທຶກກໍ່ສ້າງ', + customer: 'ລູກຄ້າ', + manager: 'ຜູ້ຈັດການໂຄງການ', + }, + + budget: { + title: 'ຈັດການງົບປະມານ/ໃບສະເໜີລາຄາ', + description: 'ຈັດການໂຄງການເຈລະຈາ', + newProject: 'ສ້າງໂຄງການໃໝ່', + statusFilter: 'ກັ່ນຕອງ: ', + inNegotiation: 'ເຈລະຈາ', + signed: 'ເຊັນແລ້ວ', + unsigned: 'ຍັງບໍ່ເຊັນ', + draft: 'ຮ່າງ', + sent: 'ສົ່ງແລ້ວ', + approved: 'ຜ່ານແລ້ວ', + rejected: 'ປະຕິເສດ', + deleteConfirm: 'ຢືນຢັນລຶບ', + deleteConfirmMsg: 'ຢືນຢັນລຶບ? ບໍ່ສາມາດກູ້ຄືນ.', + deletePassMsg: 'ປ້ອນລະຫັດຜູ້ເບິ່ງແຍງ:', + deletePass: 'ປ້ອນລະຫັດ', + getDataFailed: 'ດຶງຂໍ້ມູນລົ້ມເຫຼວ', + deleteSuccess: 'ລຶບສຳເລັດ', + passwordError: 'ລະຫັດບໍ່ຖືກ', + customerLabel: 'ລູກຄ້າ: ', + managerLabel: 'ຜູ້ຈັດການ: ', + intermediaryLabel: 'ຄົນກາງ: ', + intermediaryFee: 'ຄ່ານາຍໜ້າ: ', + versionDeleteConfirm: 'ຢືນຢັນລຶບສະບັບນີ້?', + createTitle: 'ສ້າງໂຄງການໃໝ່', + createDesc: 'ສ້າງໂຄງການເຈລະຈາ', + basicInfo: 'ຂໍ້ມູນພື້ນຖານ', + projectName: 'ຊື່ໂຄງການ', + projectNamePlaceholder: 'ປ້ອນຊື່', + customer: 'ລູກຄ້າ', + selectCustomer: 'ເລືອກລູກຄ້າ', + businessManager: 'ຜູ້ຈັດການ', + selectManager: 'ເລືອກຜູ້ຈັດການ', + unknownDept: 'ພະແນກບໍ່ຮູ້', + projectLocation: 'ສະຖານທີ່', + locationPlaceholder: 'ປ້ອນສະຖານທີ່', + surveyDate: 'ວັນສຳຫຼວດ', + intermediary: 'ຂໍ້ມູນຄົນກາງ', + intermediaryName: 'ຊື່ຄົນກາງ', + intermediaryNamePlaceholder: 'ປ້ອນຊື່', + intermediaryType: 'ປະເພດຄ່ານາຍໜ້າ', + fixedAmount: 'ຈຳນວນຄົງທີ່', + percentage: 'ເປີເຊັນ', + intermediaryRatio: 'ອັດຕາ(%)', + intermediaryRatioPlaceholder: 'ປ້ອນອັດຕາ', + intermediaryAmount: 'ຈຳນວນເງິນ', + intermediaryAmountPlaceholder: 'ປ້ອນຈຳນວນ', + projectDetail: 'ລາຍລະອຽດ', + customerRequirement: 'ຄວາມຕ້ອງການ', + requirementPlaceholder: 'ປ້ອນຄວາມຕ້ອງການ', + overview: 'ພາບລວມ', + overviewPlaceholder: 'ປ້ອນຄຳອະທິບາຍ', + attachment: 'ເອກະສານແນບ', + attachmentUpload: 'ອັບເອກະສານ', + surveyPhoto: 'ຮູບສຳຫຼວດ', + noAccess: 'ບໍ່ມີສິດ', + createSuccess: 'ສ້າງສຳເລັດ', + createFailed: 'ສ້າງລົ້ມເຫຼວ', + leaveConfirm: 'ຢືນຢັນອອກ', + leaveConfirmMsg: 'ຂໍ້ມູນຍັງບໍ່ບັນທຶກ, ຢືນຢັນອອກ?', + leave: 'ອອກ', + continueEdit: 'ສືບຕໍ່ແກ້ໄຂ', + return: 'ກັບ', + signedSuccess: 'ໝາຍເຊັນສຳເລັດ', + notFound: 'ບໍ່ພົບ', + quotationVersions: 'ສະບັບໃບສະເໜີລາຄາ', + addVersion: 'ເພີ່ມສະບັບ', + versionDate: 'ວັນທີ: ', + versionAmount: 'ຈຳນວນ: ', + versionRemark: 'ໝາຍເຫດ: ', + noVersion: 'ບໍ່ມີສະບັບ', + markSigned: 'ໝາຍເຊັນ', + markUnsigned: 'ໝາຍບໍ່ເຊັນ', + enterProject: 'ເຂົ້າສູ່ຈັດການ', + deleteProject: 'ລຶບໂຄງການ', + detailTitle: 'ລາຍລະອຽດ', + detailDesc: 'ເບິ່ງລາຍລະອຽດ', + projectInfo: 'ຂໍ້ມູນໂຄງການ', + photos: 'ຮູບ ', + noPhotos: 'ບໍ່ມີຮູບ', + noAttachment: 'ບໍ່ມີເອກະສານ', + quickSign: 'ເຊັນດ່ວນ', + quickSignSuccess: 'ເຊັນສຳເລັດ, ສ້າງໂຄງການອັດຕະໂນມັດ', + contractNo: 'ເລກສັນຍາ', + contractNoPlaceholder: 'ປ້ອນເລກສັນຍາ', + contractType: 'ຮູບແບບຮັບເໝົາ', + selectContractType: 'ເລືອກຮູບແບບ', + totalPrice: 'ລາຄາລວມ', + totalPricePlaceholder: 'ປ້ອນລາຄາ', + durationDays: 'ມື້ກໍ່ສ້າງ', + durationPlaceholder: 'ປ້ອນຈຳນວນມື້', + durationDaysPlaceholder: 'ປ້ອນມື້', + quickSignNote: 'ໝາຍເຫດ: ເຊັນດ່ວນ, ຂໍ້ມູນລະອຽດເພີ່ມໄດ້ພາຍຫຼັງ', + newQuotation: 'ເພີ່ມສະບັບໃໝ່', + quotationDate: 'ວັນທີໃບສະເໜີ', + selectDate: 'ເລືອກວັນທີ', + quotationAmount: 'ຈຳນວນເງິນ', + amountPlaceholder: 'ປ້ອນຈຳນວນ', + quotationFile: 'ໄຟລ໌', + viewFile: 'ເບິ່ງໄຟລ໌', + uploadFile: 'ອັບໄຟລ໌', + remarkPlaceholder: 'ປ້ອນໝາຍເຫດ', + uploadSuccess: 'ອັບສຳເລັດ', + version: 'ສະບັບ', + projectLabel: 'ຊື່: ', + }, + + finance: { + title: 'ການເງິນ', + description: 'ຈັດການການເງິນໂຄງການຄົບວົງຈອນ', + initialSetup: 'ຕັ້ງຄ່າເບື້ອງຕົ້ນ', + profitModel: 'ຮູບແບບກຳໄລ', + profitModelDesc: 'ຕັ້ງຄ່າອັດຕາກຳໄລມາດຕະຖານ', + addCompany: 'ເພີ່ມບໍລິສັດ', + companyName: 'ຊື່ບໍລິສັດ', + companyNamePlaceholder: 'ປ້ອນຊື່ບໍລິສັດ', + profitRate: 'ອັດຕາກຳໄລ(%)', + financeOverview: 'ພາບລວມການເງິນ', + financeOverviewDesc: 'ພາບລວມສະຖານະການເງິນ', + exportReport: 'ສົ່ງອອກ', + incomeExpense: 'ລາຍຮັບ/ລາຍຈ່າຍ', + receivablePayable: 'ໜີ້ຮັບ/ໜີ້ຈ່າຍ', + profit: 'ກຳໄລ', + pendingApprovalCount: 'ລໍອະນຸມັດ: {count}', + pendingExecutionCount: 'ລໍດຳເນີນການ: {count}', + pendingPaymentCount: 'ລໍຈ່າຍ: {count}', + totalIncome: 'ລາຍຮັບລວມ', + totalExpense: 'ລາຍຈ່າຍລວມ', + totalReceivable: 'ໜີ້ຮັບລວມ', + totalPayable: 'ໜີ້ຈ່າຍລວມ', + netProfit: 'ກຳໄລສຸດທິ', + incomeSources: 'ລາຍຮັບ', + expenseCategories: 'ລາຍຈ່າຍ', + noData: 'ບໍ່ມີຂໍ້ມູນ', + currencyCNY: 'CNY', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + originalCurrency: 'ສະກຸນເງິນຕົ້ນທາງ', + convertedAmount: 'ຈຳນວນແປງແລ້ວ({currency})', + projectName: 'ຊື່ໂຄງການ', + amount: 'ຈຳນວນ', + type: 'ປະເພດ', + date: 'ວັນທີ', + category: 'ໝວດ', + ratio: 'ອັດຕາ', + financeRecord: 'ບັນທຶກການເງິນ', + companyList: 'ລາຍການບໍລິສັດ', + noCompanies: 'ບໍ່ມີບໍລິສັດ', + addFirstCompany: 'ເພີ່ມບໍລິສັດທຳອິດ', + companyAdded: 'ເພີ່ມບໍລິສັດແລ້ວ', + companyDeleted: 'ລຶບບໍລິສັດແລ້ວ', + profitRateSaved: 'ບັນທຶກອັດຕາກຳໄລແລ້ວ', + standard: 'ມາດຕະຖານ', + constructionTeam: 'ທີມກໍ່ສ້າງ', + businessTeam: 'ທີມທຸລະກິດ', + subsidiary: 'ບໍລິສັດຍ່ອຍ', + getListFailed: 'ດຶງບັນຊີລົ້ມເຫຼວ', + loadCompanyFailed: 'ໂຫຼດບໍລິສັດລົ້ມເຫຼວ', + saveCompanyFailed: 'ບັນທຶກບໍລິສັດລົ້ມເຫຼວ', + deleteCompanyFailed: 'ລຶບບໍລິສັດລົ້ມເຫຼວ', + profitRateUpdateFailed: 'ອັບເດດອັດຕາກຳໄລລົ້ມເຫຼວ', + noAccess: 'ບໍ່ມີສິດ', + pendingDocs: 'ເອກະສານລໍດຳເນີນການ', + pendingApproval: 'ລໍອະນຸມັດ', + pendingExecution: 'ລໍດຳເນີນການ', + pendingPayment: 'ລໍຈ່າຍ', + financialReport: 'ລາຍງານການເງິນ', + totalLabel: 'ລວມ: ', + noRecord: 'ບໍ່ມີບັນທຶກ', + loadFinanceFailed: 'ໂຫຼດຂໍ້ມູນການເງິນລົ້ມເຫຼວ', + loadOverviewFailed: 'ໂຫຼດພາບລວມລົ້ມເຫຼວ', + projectRevenue: 'ລາຍຮັບສັນຍາໂຄງການ', + warrantyReturn: 'ສົ່ງຄືນເງິນຄໍ້າປະກັນ', + shareholderInvestment: 'ການລົງທຶນຂອງຜູ້ຖືຫຸ້ນ', + otherIncome: 'ລາຍຮັບອື່ນໆ', + materialPurchase: 'ຈັດຊື້ວັດສະດຸ', + equipmentPurchase: 'ຈັດຊື້ອຸປະກອນ', + constructionSubcontract: 'ຮັບເໝົາຊ່ວຍກໍ່ສ້າງ', + laborWage: 'ຄ່າແຮງງານ', + travelTransport: 'ການເດີນທາງ ແລະ ຂົນສົ່ງ', + accommodationFood: 'ທີ່ພັກ ແລະ ອາຫານ', + transportLogistics: 'ຂົນສົ່ງ ແລະ ໂລຈິສຕິກ', + surveyDesign: 'ສຳຫຼວດ ແລະ ອອກແບບ', + smallTools: 'ເຄື່ອງມືນ້ອຍ', + customerEDLRelation: 'ຄວາມສຳພັນລູກຄ້າ/EDL', + otherProjectExpense: 'ຄ່າໃຊ້ຈ່າຍໂຄງການອື່ນໆ', + salaryWelfare: 'ເງິນເດືອນ ແລະ ສະວັດດິກ', + rentProperty: 'ຄ່າເຊົ່າ ແລະ ອະສັງຫາລິມະຊັບ', + officeExpense: 'ຄ່າໃຊ້ຈ່າຍຫ້ອງການ', + commute: 'ການເດີນທາງ', + vehicleMaintenance: 'ບຳລຸງຮັກສາພາຫະນະ', + fixedAsset: 'ຊັບສິນຖາວອນ', + marketing: 'ການຕະຫຼາດ', + entertainment: 'ຄ່າຕ້ອນຮັບ', + employeeBenefit: 'ສະວັດດິກພະນັກງານ', + expressLogistics: 'ຂົນສົ່ງດ່ວນ', + otherCompanyExpense: 'ຄ່າໃຊ້ຈ່າຍບໍລິສັດອື່ນໆ', + counterpartySupplier: 'ຜູ້ສະໜອງ', + counterpartySubcontractor: 'ຜູ້ຮັບເໝົາຊ່ວຍ', + counterpartyCustomer: 'ລູກຄ້າ', + counterpartyEmployee: 'ພະນັກງານ', + counterpartyLogistics: 'ບໍລິສັດໂລຈິສຕິກ', + counterpartyShareholder: 'ຜູ້ຖືຫຸ້ນ', + counterpartyOther: 'ອື່ນໆ', + addRecord: 'Add Finance Record', + addRecordBtn: 'Add Record', + exportExcel: 'Export Excel', + expenseSummary: 'Expense Category Summary', + detail: 'Finance Details', + filterType: 'Filter Type', + incomeType: 'Income/Expense Type', + level1Category: 'Level 1 Category', + level2Category: 'Level 2 Category', + currency: 'Currency', + exchangeRate: 'Exchange Rate', + equivalentCNY: 'Equivalent RMB', + counterpartyName: 'Counterparty Name', + counterpartyType: 'Counterparty Type', + personName: 'Person Name', + desc: 'Description', + voucherNo: 'Voucher No.', + selectDate: 'Please select date', + selectIncomeType: 'Please select', + selectLevel1: 'Please select', + selectLevel2: 'Please select level 1 category first', + selectProject: 'Select Project', + amountPlaceholder: '0', + counterpartyPlaceholder: 'Payee/Payer name', + selectType: 'Select Type', + personNamePlaceholder: 'Related employee name', + descPlaceholder: 'Additional notes', + voucherPlaceholder: 'Invoice/Receipt number', + income: 'Income', + expense: 'Expense', + projectExpense: 'Project Expense', + companyExpense: 'Company Expense', + incomeCategory: 'Income', + manual: 'Manual', + advance: 'Advance', + reimbursement: 'Reimbursement', + payment: 'Payment', + material: 'Material', + freight: 'Freight', + source: 'Source', + recordSuccess: 'Recorded successfully', + exporting: 'Exporting...', + exportSuccess: 'Export successful', + exportFailed: 'Export failed', + sheetName: 'Finance Ledger', + totalRecords: 'Total {total} records', + }, + + cash: { + tabOverview: 'ພາບລວມ', + tabIncome: 'ບັນທຶກລາຍຮັບ', + tabExpense: 'ບັນທຶກລາຍຈ່າຍ', + addIncome: 'ເພີ່ມລາຍຮັບ', + addExpense: 'ເພີ່ມລາຍຈ່າຍ', + financeExpense: 'ຄ່າໃຊ້ຈ່າຍທາງການເງິນ', + customerAdvance: 'ລູກຄ້າຈ່າຍລ່ວງໜ້າ/ກູ້ຢືມ', + bankLoan: 'ກູ້ຢືມທະນາຄານ', + otherLoan: 'ກູ້ຢືມອື່ນໆ', + dividendIncome: 'ເງິນປັນຜົນ', + interestIncome: 'ດອກເບ້ຍຮັບ', + assetDisposal: 'ຂາຍຊັບສິນ', + taxRefund: 'ຄືນພາສີ', + governmentSubsidy: 'ເງິນອຸດໜູນລັດຖະບານ', + loanRepayment: 'ຊຳລະຄືນເງິນກູ້', + interestExpense: 'ດອກເບ້ຍຈ່າຍ', + dividendPayment: 'ຈ່າຍເງິນປັນຜົນ', + taxPayment: 'ຊຳລະພາສີ', + depositPayment: 'ມັດຈຳ/ຄໍ້າປະກັນ', + ownerExpense: 'ຄ່າໃຊ້ຈ່າຍເຈົ້າຂອງ', + otherFinance: 'ຄ່າໃຊ້ຈ່າຍທາງການເງິນອື່ນໆ', + sourceLabel: 'ການຈັດການເງິນສົດ', + receiptSource: 'ຮັບເງິນໂຄງການ', + counterpartyBank: 'ທະນາຄານ', + counterpartySelect: 'ເລືອກຄູ່ສັນຍາ', + counterpartySelectPlaceholder: 'ເລືອກຄູ່ສັນຍາ', + voucherUpload: 'ອັບໂຫຼດໃບຮັບ', + uploadVoucher: 'ອັບໂຫຼດໃບຮັບ', + uploadSuccess: 'ອັບໂຫຼດສຳເລັດ', + uploadFailed: 'ອັບໂຫຼດລົ້ມເຫຼວ', + }, + + reports: { + title: 'ລາຍງານວິເຄາະ', + description: 'ວິເຄາະສະຖິຕິການດຳເນີນງານ', + projectAnalysis: 'ວິເຄາະໂຄງການ', + financialAnalysis: 'ວິເຄາະການເງິນ', + personnelAnalysis: 'ວິເຄາະບຸກຄະລາກອນ', + procurementAnalysis: 'ວິເຄາະຈັດຊື້', + noData: 'ບໍ່ມີຂໍ້ມູນ', + reportExport: 'ສົ່ງອອກລາຍງານ', + monthly: 'ລາຍເດືອນ', + quarterly: 'ລາຍໄຕຣມາດ', + yearly: 'ລາຍປີ', + custom: 'ກຳນົດເອງ', + fromDate: 'ຈາກວັນທີ', + toDate: 'ຮອດວັນທີ', + generate: 'ສ້າງລາຍງານ', + noReport: 'ບໍ່ມີຂໍ້ມູນລາຍງານ', + selectReportType: 'ເລືອກປະເພດລາຍງານ', + totalIncome: 'Total Income', + totalExpense: 'Total Expense', + netProfit: 'Net Profit', + monthlyReport: 'Monthly Financial Report', + selectMonth: 'Select Month', + month: 'Month', + incomeCategory: 'Income Categories', + projectExpenseCategory: 'Project Expense Categories', + companyExpenseCategory: 'Company Expense Categories', + categoryTag: 'Income', + projectTag: 'Project', + companyTag: 'Company', + }, + + paymentRequest: { + title: 'ຄຳຂໍຈ່າຍເງິນ', + description: 'ຂະບວນການຄຳຂໍຈ່າຍເງິນ', + newRequest: 'ສ້າງຄຳຂໍໃໝ່', + editRequest: 'ແກ້ໄຂ', + deleteRequest: 'ລຶບ', + requestNo: 'ເລກທີ', + project: 'ໂຄງການ', + applicant: 'ຜູ້ຂໍ', + payee: 'ຜູ້ຮັບເງິນ', + amount: 'ຈຳນວນ', + status: 'ສະຖານະ', + remark: 'ໝາຍເຫດ', + actions: 'ດຳເນີນການ', + pendingSubmit: 'ຮ່າງ', + pendingApproval: 'ລໍອະນຸມັດ', + approved: 'ຜ່ານແລ້ວ', + rejected: 'ປະຕິເສດ', + processing: 'ດຳເນີນ', + paid: 'ຈ່າຍແລ້ວ', + cancelled: 'ຍົກເລີກ', + selectProject: 'ເລືອກໂຄງການ', + selectPayee: 'ເລືອກຜູ້ຮັບເງິນ', + payeeType: 'ປະເພດຜູ້ຮັບເງິນ', + supplier: 'ຜູ້ສະໜອງ', + subcontractor: 'ຜູ້ຮັບເໝົາຊ່ວງ', + employee: 'ພະນັກງານ', + other: 'ອື່ນໆ', + paymentType: 'ປະເພດຈ່າຍ', + materialPayment: 'ຈ່າຍຄ່າວັດສະດຸ', + laborPayment: 'ຈ່າຍຄ່າແຮງງານ', + subcontractPayment: 'ຈ່າຍຄ່າເໝົາ', + otherPayment: 'ຈ່າຍອື່ນໆ', + paymentDate: 'ວັນທີຈ່າຍ', + paymentMethod: 'ວິທີຈ່າຍ', + bankTransfer: 'ໂອນຜ່ານທະນາຄານ', + cash: 'ເງິນສົດ', + check: 'ເຊັກ', + accountInfo: 'ຂໍ້ມູນບັນຊີ', + bankName: 'ຊື່ທະນາຄານ', + accountNo: 'ເລກບັນຊີ', + accountName: 'ຊື່ບັນຊີ', + attachment: 'ເອກະສານແນບ', + submitSuccess: 'ສົ່ງສຳເລັດ', + submitFailed: 'ສົ່ງລົ້ມເຫຼວ', + getListFailed: 'ດຶງລາຍການລົ້ມເຫຼວ', + noRequests: 'ບໍ່ມີຄຳຂໍ', + noPermission: 'ບໍ່ມີສິດ', + filterByProject: 'ແຍກຕາມໂຄງການ', + filterByStatus: 'ແຍກຕາມສະຖານະ', + all: 'ທັງໝົດ', + relatedPo: 'ໃບສັ່ງຊື້ທີ່ກ່ຽວຂ້ອງ', + poNo: 'ເລກສັ່ງຊື້', + currency: 'ສະກຸນເງິນ', + exchangeRate: 'ອັດຕາແລກປ່ຽນ', + payeeCompany: 'ບໍລິສັດຮັບເງິນ', + payeeCompanyPlaceholder: 'ປ້ອນຊື່ບໍລິສັດ', + payeePerson: 'ຊື່ຜູ້ຮັບເງິນ', + payeePersonPlaceholder: 'ປ້ອນຊື່', + amountPlaceholder: 'ປ້ອນຈຳນວນ', + datePlaceholder: 'ເລືອກວັນທີ', + remarkPlaceholder: 'ປ້ອນໝາຍເຫດ', + confirmDeleteTitle: 'ຢືນຢັນລຶບຄຳຂໍ', + confirmDeleteMsg: 'ລຶບແລ້ວກູ້ບໍ່ໄດ້', + getDetailFailed: 'ດຶງລາຍລະອຽດລົ້ມເຫຼວ', + notFound: 'ບໍ່ພົບ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + rollbackSuccess: 'ຖອນສຳເລັດ', + rollbackFailed: 'ຖອນລົ້ມເຫຼວ', + deleteConfirmMsg: 'ຢືນຢັນລຶບຄຳຂໍຊຳລະນີ້?', + activeApplications: 'Active Applications', + completed: 'Completed', + subject: 'Subject', + applicationDate: 'Application Date', + code: 'Code', + action: 'Action', + edit: 'Edit', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + delete: 'Delete', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + editPayment: 'Edit Payment Request', + newPayment: 'New Payment Request', + expenseType: 'Expense Type', + selectExpenseType: 'Select Expense Type', + relatedProject: 'Related Project', + expenseCategory: 'Expense Category', + selectCategory: 'Select Expense Category', + selectPayeeType: 'Select Payee Type', + selectSubcontractor: 'Select Subcontractor', + selectSupplier: 'Select Supplier', + selectCustomer: 'Select Customer', + payeeName: 'Payee Name', + payeeNamePlaceholder: 'Manually enter payee name', + accountNamePlaceholder: 'Account name (auto-filled when selecting subcontractor/supplier/customer)', + bankAccount: 'Bank Account', + bankAccountPlaceholder: 'Bank account number (auto-filled when selecting subcontractor/supplier/customer)', + bankNamePlaceholder: 'Bank name (auto-filled when selecting subcontractor/supplier/customer)', + qrCode: 'QR Code', + paymentAmount: 'Payment Amount', + paymentAmountPlaceholder: 'Enter payment amount', + paymentReason: 'Payment Reason', + paymentReasonPlaceholder: 'Reason for payment', + uploadProof: 'Upload Proof Attachment', + proofAttachment: 'Proof Attachment', + equivalentCNY: 'Equivalent RMB: ¥ ', + operationSuccess: 'Operation successful', + detailTitle: 'Payment Request Details', + applicationCode: 'Application Code', + withdrawn: 'Withdrawn', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + counterpartySubcontractor: 'Subcontractor', + counterpartySupplier: 'Supplier', + counterpartyCustomer: 'Customer', + counterpartyOther: 'Other', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + }, + + paymentPlan: { + title: 'ແຜນຈ່າຍເງິນ', + description: 'ກຳນົດແຜນຈ່າຍເງິນ', + project: 'ໂຄງການ', + selectProject: 'ເລືອກ', + totalAmount: 'ລວມ', + paidAmount: 'ຈ່າຍແລ້ວ', + remainingAmount: 'ຍັງເຫຼືອ', + addPlan: 'ເພີ່ມແຜນ', + planName: 'ຊື່ແຜນ', + planDate: 'ວັນທີ', + planAmount: 'ຈຳນວນ', + planRatio: 'ອັດຕາ(%)', + noPlan: 'ບໍ່ມີແຜນ', + planProgress: 'ຄວາມຄືບໜ້າ', + planned: 'ແຜນ', + actual: 'ຕົວຈິງ', + deviation: 'ສ່ວນຕ່າງ', + status: 'ສະຖານະ', + notStarted: 'ບໍ່ເລີ່ມ', + inProgress: 'ດຳເນີນ', + completed: 'ສຳເລັດ', + overdue: 'ເກີນກຳນົດ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມເຫຼວ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + noProjectData: 'ບໍ່ມີຂໍ້ມູນ', + getListFailed: 'ດຶງລາຍການລົ້ມເຫຼວ', + confirmDelete: 'ຢືນຢັນລຶບ', + confirmDeleteMsg: 'ລຶບແຜນນີ້?', + noPermission: 'ບໍ່ມີສິດ', + newPlan: 'New Payment Plan', + editPlan: 'Edit Payment Plan', + planCode: 'Plan Code', + purchaseOrder: 'Purchase Order', + paymentDate: 'Payment Date', + amount: 'Amount', + paymentType: 'Payment Type', + creator: 'Creator', + action: 'Action', + pending: 'Pending', + approved: 'Approved', + executed: 'Executed', + cancelled: 'Cancelled', + partialPayment: 'Partial Payment', + fullPayment: 'Full Payment', + selectOrder: 'Please select purchase order', + selectDate: 'Please select payment date', + inputAmount: 'Please enter payment amount', + selectCurrency: 'Please select currency', + selectType: 'Please select payment type', + selectStatus: 'Please select status', + inputCreator: 'Please enter creator', + amountPlaceholder: 'Payment amount', + descPlaceholder: 'Please enter payment plan description', + detailTitle: 'Payment Plan Details', + detailCode: 'Plan Code', + currencyCNY: 'RMB', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + getDetailFailed: 'Failed to get payment plan details', + }, + + verification: { + title: 'ການກວດສອບ', + description: 'ກວດສອບ ແລະ ໃບຢັ້ງຢືນຄ່າໃຊ້ຈ່າຍ', + newVerification: 'ສ້າງການກວດສອບໃໝ່', + edit: 'ແກ້ໄຂ', + delete: 'ລຶບ', + verificationNo: 'ເລກທີ', + project: 'ໂຄງການ', + applicant: 'ຜູ້ຂໍ', + amount: 'ຈຳນວນ', + status: 'ສະຖານະ', + pending: 'ລໍດຳເນີນ', + approved: 'ຜ່ານ', + rejected: 'ປະຕິເສດ', + selectProject: 'ເລືອກໂຄງການ', + verificationType: 'ປະເພດ', + completion: 'ສຳເລັດໜ້າວຽກ', + progress: 'ຄວາມຄືບໜ້າ', + finalAcceptance: 'ຮັບງານສຸດທ້າຍ', + verificationDate: 'ວັນທີກວດສອບ', + verificationAmount: 'ຈຳນວນ', + attachment: 'ເອກະສານ', + descriptionPlaceholder: 'ປ້ອນຄຳອະທິບາຍ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມເຫຼວ', + getListFailed: 'ດຶງລາຍການລົ້ມເຫຼວ', + confirmDelete: 'ຢືນຢັນ?', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + verifier: 'ຜູ້ກວດ', + verifiedAt: 'ເວລາກວດ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີຂໍ້ມູນ', + selectVerificationType: 'ເລືອກປະເພດ', + currency: 'ສະກຸນເງິນ', + exchangeRate: 'ອັດຕາ', + amountPlaceholder: 'ປ້ອນຈຳນວນ', + datePlaceholder: 'ເລືອກວັນທີ', + remark: 'ໝາຍເຫດ', + remarkPlaceholder: 'ປ້ອນໝາຍເຫດ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມເຫຼວ', + projectPlaceholder: 'ເລືອກໂຄງການ', + category: 'ໝວດໝູ່ຄ່າໃຊ້ຈ່າຍ', + categoryPlaceholder: 'ເລືອກໝວດໝູ່ຄ່າໃຊ້ຈ່າຍ', + categoryLabel: 'ໝວດໝູ່: ', + categoryRequired: 'ກະລຸນາເລືອກໝວດໝູ່ຄ່າໃຊ້ຈ່າຍ', + settlementType: 'ປະເພດການຊຳລະ', + settlement: 'ການຊຳລະ', + settlementInfo: 'ຂໍ້ມູນການຊຳລະ', + nonSettlement: 'ບໍ່ແມ່ນການຊຳລະ', + expenseDetail: 'ລາຍລະອຽດຄ່າໃຊ້ຈ່າຍ', + totalItems: 'ທັງໝົດ {count} ລາຍການ', + paymentProof: 'ຫຼັກຖານການຊຳລະ', + receipt: 'ໃບຮັບ', + addExpense: 'ເພີ່ມຄ່າໃຊ້ຈ່າຍ', + editExpense: 'ແກ້ໄຂຄ່າໃຊ້ຈ່າຍ', + viewDetail: 'ເບິ່ງລາຍລະອຽດ', + refund: 'ສົ່ງຄືນ', + supplement: 'ຊຳລະເພີ່ມ', + inputAmount: 'ກະລຸນາປ້ອນຈຳນວນເງິນ', + equivalentCNY: 'ທຽບເທົ່າຢວນ: ¥', + totalAmount: 'ລວມ: ¥{amount}', + advanceCode: 'ລະຫັດເງິນລ່ວງໜ້າ', + advanceCodePlaceholder: 'ເລືອກ ຫຼື ປ້ອນລະຫັດເງິນລ່ວງໜ້າ', + projectExpense: 'ຄ່າໃຊ້ຈ່າຍໂຄງການ', + companyExpense: 'ຄ່າໃຊ້ຈ່າຍບໍລິສັດ', + deleteConfirm: 'ຢືນຢັນການລຶບ', + deleteConfirmMsg: 'ຢືນຢັນລຶບບັນທຶກການກວດສອບນີ້?', + editVerification: 'Edit Verification', + subject: 'Subject', + relatedAdvance: 'Related Advance', + code: 'Code', + action: 'Action', + detail: 'Details', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + addDetail: 'Add Detail', + advanceAmount: 'Advance Amount', + settlementOptions: 'Settlement Options', + settlementAmount: 'Settlement Amount', + expenseType: 'Expense Type', + subjectPlaceholder: 'Verification reason description', + detailLabel: 'Verification Details', + expenseDescription: 'Expense Description', + expenseCategory: 'Expense Category', + detailAmount: 'Amount', + mainAttachment: 'Main Attachment', + refundProof: 'Refund Proof (Required)', + overallAttachment: 'Overall Proof Attachment', + selectAdvance: 'Please select related advance', + selectAdvanceOrInput: 'Select or enter advance code', + selectProjectRequired: 'Please select project', + uploadRefundRequired: 'Please upload refund proof', + finalSettlement: 'Final Settlement', + verifiedAmount: 'Verified Amount: ', + remainingAmount: 'Remaining Amount: ', + refundLabel: 'Refund ¥{amount}', + supplementLabel: 'Supplement ¥{amount}', + totalLabel: 'Total: ', + refundNote: '* Refund-type settlement verification must upload refund proof', + unknownProject: 'Unknown Project', + advanceInfo: 'Advance Information', + advanceTotalAmount: 'Advance Amount', + advanceVerified: 'Verified Amount', + advanceRemaining: 'Remaining Amount', + detailTitle: 'Verification Details', + attachmentCount: '{count} sheets', + isSettlement: 'Yes', + notSettlement: 'No', + refundText: 'Refund ', + supplementText: 'Supplement ', + pendingApproval: 'Pending Approval', + withdrawn: 'Withdrawn', + paid: 'Paid', + pendingEdit: 'Pending Edit', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + createSuccess: 'Created successfully', + submitSuccess: 'Submitted successfully', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + }, + + advance: { + title: 'ການຂໍເງິນລ່ວງໜ້າ', + description: 'ຂະບວນການຂໍເງິນລ່ວງໜ້າ', + newAdvance: 'ສ້າງໃໝ່', + edit: 'ແກ້ໄຂ', + delete: 'ລຶບ', + advanceNo: 'ເລກທີ', + project: 'ໂຄງການ', + applicant: 'ຜູ້ຂໍ', + amount: 'ຈຳນວນ', + status: 'ສະຖານະ', + pending: 'ລໍ', + approved: 'ຜ່ານ', + rejected: 'ປະຕິເສດ', + verified: 'ກວດແລ້ວ', + cleared: 'ລ້າງແລ້ວ', + selectProject: 'ເລືອກ', + advanceType: 'ປະເພດ', + materialAdvance: 'ວັດສະດຸ', + travelAdvance: 'ເດີນທາງ', + otherAdvance: 'ອື່ນ', + advanceDate: 'ວັນທີ', + advanceAmount: 'ຈຳນວນ', + expectedReturn: 'ກຳນົດຄືນ', + attachment: 'ເອກະສານ', + reason: 'ເຫດຜົນ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມເຫຼວ', + getListFailed: 'ດຶງລົ້ມເຫຼວ', + confirmDelete: 'ຢືນຢັນ?', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + currency: 'ສະກຸນເງິນ', + amountPlaceholder: 'ປ້ອນ', + datePlaceholder: 'ເລືອກ', + remark: 'ໝາຍເຫດ', + remarkPlaceholder: 'ປ້ອນ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມເຫຼວ', + returned: 'ຄືນແລ້ວ', + overdue: 'ເກີນກຳນົດ', + editAdvance: 'Edit Advance', + subject: 'Subject', + code: 'Code', + action: 'Action', + detail: 'Details', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + activeApplications: 'Active Applications', + completed: 'Completed', + advanceCode: 'Advance Code', + subjectPlaceholder: 'Please enter advance reason', + inputAmount: 'Please enter amount', + equivalentCNY: 'Equivalent RMB: ¥ ', + pendingApproval: 'Pending Approval', + withdrawn: 'Withdrawn', + pendingEdit: 'Pending Edit', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + createSuccess: 'Created successfully', + submitSuccess: 'Submitted successfully', + deletePassInput: 'Please enter password to confirm deletion', + deletePassPlaceholder: 'Enter password', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + detailTitle: 'Advance Details', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + }, + + advanceVerification: { + title: 'ກວດສອບເງິນລ່ວງໜ້າ', + description: 'ຕິດຕາມການກວດສອບເງິນລ່ວງໜ້າ', + advanceNo: 'ເລກທີ', + project: 'ໂຄງການ', + applicant: 'ຜູ້ຂໍ', + advanceAmount: 'ຈຳນວນລ່ວງໜ້າ', + verifiedAmount: 'ກວດແລ້ວ', + unverifiedAmount: 'ຍັງບໍ່ກວດ', + status: 'ສະຖານະ', + pendingVerification: 'ລໍກວດ', + partiallyVerified: 'ກວດບາງສ່ວນ', + fullyVerified: 'ກວດຄົບ', + cleared: 'ລ້າງ', + noRecord: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມເຫຼວ', + noAccess: 'ບໍ່ມີສິດ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມເຫຼວ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມເຫຼວ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + reimbursementNo: 'ເລກເບີກຈ່າຍ', + verificationDate: 'ວັນກວດ', + remark: 'ໝາຍເຫດ', + allowsManagement: 'ອະນຸຍາດຈັດການສະຖານະກວດສອບ', + searchByProject: 'ຄົ້ນຫາໂຄງການ', + searchByAdvance: 'ຄົ້ນຫາການລ່ວງໜ້າ', + filterByStatus: 'ກັ່ນສະຖານະ', + filterByDate: 'ກັ່ນວັນທີ', + approved: 'ຜ່ານ', + rejected: 'ປະຕິເສດ', + subject: 'Subject', + remainingAmount: 'Remaining Amount', + advanceDate: 'Advance Date', + code: 'Code', + action: 'Action', + searchByName: 'Search by applicant name', + startDate: 'Start Date', + endDate: 'End Date', + search: 'Search', + unverified: 'Not Fully Verified', + completed: 'Completed', + detail: 'Details', + noAccessMsg: 'You do not have permission to access this page. Only admin and finance personnel can view advance verification status.', + pendingApproval: 'Pending Approval', + withdrawn: 'Withdrawn', + verified: 'Verified', + pendingEdit: 'Pending Edit', + partialVerified: 'Partially Verified', + completedStatus: 'Completed', + detailTitle: 'Advance Details: {code}', + advanceCode: 'Advance Code', + amount: 'Amount', + verifiedLabel: 'Verified Amount', + remaining: 'Remaining Amount', + currency: 'Currency', + relatedVerifications: 'Related Verifications', + verificationCode: 'Verification Code', + relatedAdvance: 'Related Advance', + verificationAmount: 'Verification Amount', + isSettlement: 'Is Settlement', + }, + + reimbursement: { + title: 'ການເບີກຄ່າໃຊ້ຈ່າຍ', + description: 'ຂະບວນການເບີກຄ່າໃຊ້ຈ່າຍ', + newReimbursement: 'ສ້າງໃໝ່', + edit: 'ແກ້ໄຂ', + delete: 'ລຶບ', + reimbursementNo: 'ເລກທີ', + project: 'ໂຄງການ', + applicant: 'ຜູ້ຂໍ', + amount: 'ຈຳນວນ', + status: 'ສະຖານະ', + pending: 'ລໍ', + approved: 'ຜ່ານ', + rejected: 'ປະຕິເສດ', + paid: 'ຈ່າຍແລ້ວ', + selectProject: 'ເລືອກໂຄງການ', + category: 'ໝວດຄ່າໃຊ້ຈ່າຍ', + reimbursementDate: 'ວັນທີ', + reimbursementAmount: 'ຈຳນວນ', + attachment: 'ເອກະສານ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມເຫຼວ', + getListFailed: 'ດຶງລົ້ມເຫຼວ', + confirmDelete: 'ຢືນຢັນ?', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມເຫຼວ', + currency: 'ສະກຸນເງິນ', + advanceNo: 'ການລ່ວງໜ້າ', + expenseType: 'ປະເພດ', + travel: 'ເດີນທາງ', + material: 'ວັດສະດຸ', + office: 'ຫ້ອງການ', + entertainment: 'ຕ້ອນຮັບ', + other: 'ອື່ນ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມເຫຼວ', + remark: 'ໝາຍເຫດ', + amountPlaceholder: 'ປ້ອນ', + datePlaceholder: 'ເລືອກ', + inputCategory: 'ເລືອກໝວດ', + remarkPlaceholder: 'ປ້ອນໝາຍເຫດ', + expenseDetail: 'ລາຍລະອຽດ', + addExpenseItem: '+ ເພີ່ມລາຍການ', + expenseDate: 'ວັນທີ', + expenseAmount: 'ຈຳນວນ', + invoiceNo: 'ເລກບິນ', + noExpenseItems: 'ຍັງບໍ່ມີລາຍການ', + totalItems: 'ທັງໝົດ {count} ລາຍການ', + receipt: 'ໃບຮັບ', + addExpense: 'ເພີ່ມຄ່າໃຊ້ຈ່າຍ', + editExpense: 'ແກ້ໄຂຄ່າໃຊ້ຈ່າຍ', + categoryLabel: 'ໝວດໝູ່: ', + categoryPlaceholder: 'ເລືອກໝວດໝູ່ຄ່າໃຊ້ຈ່າຍ', + categoryRequired: 'ກະລຸນາເລືອກໝວດໝູ່ຄ່າໃຊ້ຈ່າຍ', + descriptionPlaceholder: 'ປ້ອນຄຳອະທິບາຍຄ່າໃຊ້ຈ່າຍ', + projectPlaceholder: 'ເລືອກໂຄງການ', + projectExpense: 'ຄ່າໃຊ້ຈ່າຍໂຄງການ', + companyExpense: 'ຄ່າໃຊ້ຈ່າຍບໍລິສັດ', + inputAmount: 'ກະລຸນາປ້ອນຈຳນວນເງິນ', + equivalentCNY: 'ທຽບເທົ່າຢວນ: ¥', + totalAmount: 'ລວມ: ¥{amount}', + viewDetail: 'ເບິ່ງລາຍລະອຽດ', + deleteConfirmMsg: 'ຢືນຢັນລຶບບັນທຶກການເບີກຄ່າໃຊ້ຈ່າຍນີ້?', + editReimbursement: 'Edit Reimbursement', + subject: 'Subject', + code: 'Code', + action: 'Action', + detail: 'Details', + withdraw: 'Withdraw', + reEdit: 'Edit & Resubmit', + addDetail: 'Add Detail', + activeApplications: 'Active Applications', + completed: 'Completed', + subjectPlaceholder: 'Please enter reimbursement reason', + detailLabel: 'Reimbursement Details', + expenseDescription: 'Expense Description', + expenseCategory: 'Expense Category', + detailAmount: 'Amount', + mainAttachment: 'Main Attachment', + overallAttachment: 'Overall Proof Attachment', + totalLabel: 'Total: ', + unknownProject: 'Unknown Project', + reimbursementCode: 'Reimbursement Code', + attachmentCount: '{count} sheets', + pendingApproval: 'Pending Approval', + withdrawn: 'Withdrawn', + pendingEdit: 'Pending Edit', + withdrawSuccess: 'Withdrawn, can be re-edited', + withdrawFailed: 'Withdrawal failed', + createSuccess: 'Created successfully', + submitSuccess: 'Submitted successfully', + deletePassInput: 'Please enter password to confirm deletion', + deletePassPlaceholder: 'Enter password', + currencyCNY: 'RMB (CNY)', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + detailTitle: 'Reimbursement Details', + withdrawConfirm: 'Confirm Withdrawal', + withdrawConfirmMsg: 'After withdrawal, you can re-edit and resubmit. Confirm withdrawal?', + }, + + approval: { + title: 'ການອະນຸມັດ', + description: 'ຂະບວນການອະນຸມັດ', + pending: 'ລໍອະນຸມັດ', + approved: 'ຜ່ານ', + rejected: 'ປະຕິເສດ', + myApprovals: 'ການອະນຸມັດຂອງຂ້ອຍ', + all: 'ທັງໝົດ', + advance: 'ເງິນລ່ວງໜ້າ', + reimbursement: 'ເບີກຄ່າໃຊ້ຈ່າຍ', + payment: 'ຈ່າຍເງິນ', + verification: 'ກວດສອບ', + purchaseRequest: 'ຂໍຈັດຊື້', + purchaseOrder: 'ໃບສັ່ງຊື້', + filterByType: 'ກັ່ນປະເພດ', + filterByStatus: 'ກັ່ນສະຖານະ', + approve: 'ອະນຸມັດ', + reject: 'ປະຕິເສດ', + comment: 'ຄຳເຫັນ', + commentPlaceholder: 'ປ້ອນຄຳເຫັນ', + approveSuccess: 'ອະນຸມັດສຳເລັດ', + rejectSuccess: 'ປະຕິເສດສຳເລັດ', + approveFailed: 'ອະນຸມັດລົ້ມເຫຼວ', + rejectFailed: 'ປະຕິເສດລົ້ມເຫຼວ', + noApprovals: 'ບໍ່ມີລາຍການ', + getListFailed: 'ດຶງລົ້ມເຫຼວ', + noAccess: 'ບໍ່ມີສິດ', + notFound: 'ບໍ່ພົບ', + confirmApprove: 'ຢືນຢັນອະນຸມັດ', + confirmReject: 'ຢືນຢັນປະຕິເສດ', + document: 'ເອກະສານ', + applicant: 'ຜູ້ຂໍ', + project: 'ໂຄງການ', + amount: 'ຈຳນວນ', + date: 'ວັນທີ', + approver: 'ຜູ້ອະນຸມັດ', + approvalHistory: 'ປະຫວັດ', + approvedBy: 'ອະນຸມັດໂດຍ', + rejectedBy: 'ປະຕິເສດໂດຍ', + approvalDate: 'ວັນທີອະນຸມັດ', + allApprovals: 'ທັງໝົດ', + currentApprover: 'ປັດຈຸບັນ', + referrer: 'ຜູ້ສົ່ງຕໍ່', + projectManager: 'ຜູ້ຈັດການ', + financeManager: 'ຜູ້ຈັດການການເງິນ', + generalManager: 'ຜູ້ບໍລິຫານ', + pendingRequest: 'ຄຳຂໍ', + pendingCount: '{count} ລາຍການ', + noPending: 'ບໍ່ມີລາຍການລໍ', + workflow: 'ຂັ້ນຕອນ', + currentStep: 'ຂັ້ນປັດຈຸບັນ', + nextStep: 'ຂັ້ນຕໍ່ໄປ', + stepCompleted: 'ສຳເລັດ', + stepPending: 'ລໍ', + stepRejected: 'ປະຕິເສດ', + sendBack: 'ສົ່ງກັບ', + sendBackSuccess: 'ສົ່ງກັບສຳເລັດ', + sendBackFailed: 'ສົ່ງກັບລົ້ມເຫຼວ', + batchApprove: 'ອະນຸມັດລວມ', + batchReject: 'ປະຕິເສດລວມ', + selectAll: 'ເລືອກທັງໝົດ', + selected: 'ເລືອກ: {count}', + batchConfirm: 'ຢືນຢັນດຳເນີນການລວມ?', + assigned: 'ມອບໝາຍ', + unassigned: 'ບໍ່ໄດ້ມອບ', + transfer: 'ໂອນ', + transferTo: 'ໂອນໄປ', + transferSuccess: 'ໂອນສຳເລັດ', + transferFailed: 'ໂອນລົ້ມເຫຼວ', + role: 'ບົດບາດ', + department: 'ພະແນກ', + statusLabel: 'ສະຖານະ: ', + detail: 'ລາຍລະອຽດການອະນຸມັດ', + detailDesc: 'ເບິ່ງຄຳຂໍ ແລະ ປະຫວັດ', + returnToList: 'ກັບລາຍການ', + refresh: 'Refresh Data', + pass: 'Approve', + close: 'Close', + pendingTab: 'Pending Approval', + historyTab: 'Approval History', + subject: 'Subject', + type: 'Type', + applicationDate: 'Application Date', + status: 'Status', + code: 'Code', + action: 'Action', + time: 'Time', + operation: 'Operation', + operator: 'Operator', + note: 'Note/Reason', + applicationType: 'Application Type', + applicationCode: 'Application Code', + payeeType: 'Payee Type', + payee: 'Payee', + bankName: 'Bank Name', + bankAccount: 'Bank Account', + expenseType: 'Expense Type', + relatedProject: 'Related Project', + expenseCategory: 'Expense Category', + relatedAdvance: 'Related Advance', + advanceAmount: 'Advance Amount', + settlement: 'Settlement Verification', + verifiedAmount: 'Verified Amount', + remainingAmount: 'Remaining Settlement Amount', + settlementAmount: 'Settlement Amount', + purchaseType: 'Purchase Type', + supplier: 'Supplier', + currency: 'Currency', + remark: 'Remark', + approvalNote: 'Approval Note', + approvalNotePlaceholder: 'Optional: enter approval note', + rejectReason: 'Rejection Reason', + rejectReasonPlaceholder: 'Please enter rejection reason', + productDetail: 'Product Details', + detailList: 'Detail List', + refundProof: 'Refund Proof', + proofAttachment: 'Proof Attachment', + approvalOpinion: 'Approval Opinion', + detailLabel: 'Detail {index}:', + categoryLabel: 'Expense Category: ', + detailAttachment: 'Detail Attachment: ', + specLabel: 'Spec: ', + unitLabel: 'Unit: ', + qtyLabel: 'Qty: ', + priceLabel: 'Unit Price: ', + advanceApply: 'Advance Application', + reimburseApply: 'Reimbursement Application', + paymentApply: 'Payment Application', + verificationApply: 'Verification Application', + purchaseApply: 'Purchase Application', + pendingApproval: 'Pending Approval', + withdrawn: 'Withdrawn', + executed: 'Executed', + partialVerified: 'Partially Verified', + completed: 'Completed', + getPendingFailed: 'Failed to get pending approval data', + getHistoryFailed: 'Failed to get approval history', + withdrawSuccess: 'Application withdrawn', + editResubmit: 'Edited successfully, resubmitted for approval', + withdrawConfirm: 'Withdraw Application', + withdrawConfirmMsg: 'Confirm withdrawal of application {code}?', + withdrawConfirmBtn: 'Confirm Withdrawal', + projectPurchase: 'Project Purchase', + stockPurchase: 'Stock Purchase', + refundText: 'Refund ', + supplementText: 'Supplement ', + editTitle: 'Edit Application: {code}', + detailTitle: '{type} Details: {code}', + advanceDetailTitle: '{type} Details: {code}', + counterpartySubcontractor: 'Subcontractor', + counterpartySupplier: 'Supplier', + counterpartyCustomer: 'Customer', + counterpartyOther: 'Other', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + other: 'Other', + accommodation: 'Accommodation', + catering: 'Catering', + fuel: 'Fuel', + scatteredMaterial: 'Scattered Materials', + customerRelation: 'Customer Relations', + subcontractorRelation: 'Subcontractor Relations', + EDLRelation: 'EDL Relations', + extraConstruction: 'Extra Construction', + generalOperation: 'General Operations (Rent/Consumables)', + commute: 'Commute', + marketing: 'Marketing', + powerSystem: 'Power System Relations', + employeeBenefit: 'Employee Benefits', + expressLogistics: 'Express Logistics', + }, + + execution: { + title: 'ການດຳເນີນການ', + description: 'ຈັດການການດຳເນີນການເອກະສານ', + pending: 'ລໍດຳເນີນ', + processing: 'ກຳລັງດຳເນີນ', + completed: 'ສຳເລັດ', + cancelled: 'ຍົກເລີກ', + myTasks: 'ວຽກຂອງຂ້ອຍ', + all: 'ທັງໝົດ', + advance: 'ເງິນລ່ວງໜ້າ', + reimbursement: 'ເບີກຄ່າ', + payment: 'ຈ່າຍ', + verification: 'ກວດ', + filterByType: 'ກັ່ນປະເພດ', + filterByStatus: 'ກັ່ນສະຖານະ', + execute: 'ດຳເນີນ', + confirm: 'ຢືນຢັນ', + confirmExecute: 'ຢືນຢັນການດຳເນີນ?', + executeSuccess: 'ດຳເນີນສຳເລັດ', + executeFailed: 'ດຳເນີນລົ້ມເຫຼວ', + cancel: 'ຍົກເລີກ', + cancelSuccess: 'ຍົກເລີກສຳເລັດ', + cancelFailed: 'ຍົກເລີກລົ້ມເຫຼວ', + noTasks: 'ບໍ່ມີວຽກ', + getListFailed: 'ດຶງລົ້ມເຫຼວ', + noAccess: 'ບໍ່ມີສິດ', + notFound: 'ບໍ່ພົບ', + document: 'ເອກະສານ', + applicant: 'ຜູ້ຂໍ', + project: 'ໂຄງການ', + amount: 'ຈຳນວນ', + date: 'ວັນທີ', + executor: 'ຜູ້ດຳເນີນ', + executionDate: 'ວັນທີດຳເນີນ', + executionHistory: 'ປະຫວັດ', + remark: 'ໝາຍເຫດ', + remarkPlaceholder: 'ປ້ອນ', + detail: 'ລາຍລະອຽດ', + accountInfo: 'ຂໍ້ມູນບັນຊີ', + bankName: 'ທະນາຄານ', + accountNo: 'ເລກບັນຊີ', + accountName: 'ຊື່ບັນຊີ', + paymentMethod: 'ວິທີຈ່າຍ', + bankTransfer: 'ໂອນ', + cash: 'ເງິນສົດ', + cheque: 'ເຊັກ', + paymentDate: 'ວັນຈ່າຍ', + currency: 'ສະກຸນ', + exchangeRate: 'ອັດຕາ', + originalAmount: 'ຈຳນວນຕົ້ນ', + executedAmount: 'ຈຳນວນດຳເນີນ', + batchExecute: 'ດຳເນີນລວມ', + selectAll: 'ເລືອກທັງ', + selected: 'ເລືອກ {count}', + noSelection: 'ບໍ່ມີລາຍການເລືອກ', + pendingCount: '{count} ວຽກ', + noPending: 'ບໍ່ມີ', + edit: 'Edit', + pass: 'Reject', + close: 'Close', + uploadProof: 'Upload Payment Proof', + pendingTab: 'Pending Execution', + executedTab: 'Executed', + subject: 'Subject', + type: 'Type', + payee: 'Payee', + approvalDate: 'Approval Date', + code: 'Code', + action: 'Action', + executionMethod: 'Execution Method', + status: 'Status', + searchPlaceholder: 'Search subject, code, or applicant', + filterType: 'Filter Type', + sortBy: 'Sort By', + sortDateNew: 'Execution Date (Newest)', + sortDateOld: 'Execution Date (Oldest)', + sortAmountHigh: 'Amount (High to Low)', + sortAmountLow: 'Amount (Low to High)', + confirmationDate: 'Confirmation Date', + executionMethodLabel: 'Execution Method', + proofOfPayment: 'Payment Proof', + paymentConfirmation: 'Payment Confirmation', + returnReason: 'Rejection Reason', + confirmRequired: 'Please enter payment confirmation', + rejectReasonRequired: 'Please enter rejection reason', + confirmationPlaceholder: 'Enter payment confirmation info, such as account, time, etc.', + wechat: 'WeChat', + other: 'Other', + proofUploadTip: 'Please upload payment proof (bank transfer receipt, cash receipt, etc.), supports images and PDF', + noProofRefund: 'This verification is a refund type, no payment proof required', + noProofNonSettlement: 'This verification is non-settlement, no payment proof required', + pendingExecution: 'Pending Execution', + executed: 'Executed', + rejected: 'Rejected', + approved: 'Approved', + proofRequired: 'Please upload payment proof', + rejectSuccess: 'Rejected: {code}, applicant can edit and resubmit', + rejectFailed: 'Rejection failed, please try again', + editSuccess: 'Edited successfully, resubmitted for approval', + uploadSuccess: '{name} uploaded successfully', + uploadFailed: '{name} upload failed', + getPendingFailedFormat: 'Failed to get pending execution data: format error', + getPendingFailed: 'Failed to get pending execution data: ', + getPendingNetworkError: 'Network error, failed to get pending execution data', + getExecutedFailedFormat: 'Failed to get executed data: format error', + getExecutedFailed: 'Failed to get executed data: ', + getExecutedNetworkError: 'Network error, failed to get executed data', + supplierPaymentInfo: 'Supplier Payment Info', + bankAccount: 'Bank Account', + qrCode: 'QR Code', + purchaseDetail: 'Purchase Details', + detailList: 'Detail List', + approvalOpinion: 'Approval Opinion', + refundProof: 'Refund Proof', + applicationAttachment: 'Application Proof Attachment', + executionInfo: 'Execution Information', + applicationType: 'Application Type', + applicationCode: 'Application Code', + payeeType: 'Payee Type', + expenseType: 'Expense Type', + relatedProject: 'Related Project', + expenseCategory: 'Expense Category', + relatedAdvance: 'Related Advance', + advanceAmount: 'Advance Amount', + settlement: 'Settlement Verification', + verifiedAmount: 'Verified Amount', + remainingAmount: 'Remaining Settlement Amount', + settlementAmount: 'Settlement Amount', + purchaseType: 'Purchase Type', + supplier: 'Supplier', + detailLabel: 'Detail {index}:', + categoryLabel: 'Expense Category: ', + detailAttachment: 'Detail Attachment: ', + specLabel: 'Spec: ', + unitLabel: 'Unit: ', + qtyLabel: 'Qty: ', + advanceApply: 'Advance Application', + reimburseApply: 'Reimbursement Application', + paymentApply: 'Payment Application', + verificationApply: 'Verification Application', + purchaseApply: 'Purchase Application', + projectPurchase: 'Project Purchase', + stockPurchase: 'Stock Purchase', + refundText: 'Refund ', + supplementText: 'Supplement ', + counterpartySubcontractor: 'Subcontractor', + counterpartySupplier: 'Supplier', + counterpartyCustomer: 'Customer', + counterpartyOther: 'Other', + companyExpense: 'Company Expense', + projectExpense: 'Project Expense', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + otherCategory: 'Other', + accommodation: 'Accommodation', + catering: 'Catering', + fuel: 'Fuel', + scatteredMaterial: 'Scattered Materials', + customerRelation: 'Customer Relations', + subcontractorRelation: 'Subcontractor Relations', + EDLRelation: 'EDL Relations', + extraConstruction: 'Extra Construction', + generalOperation: 'General Operations (Rent/Consumables)', + commute: 'Commute', + marketing: 'Marketing', + powerSystem: 'Power System Relations', + employeeBenefit: 'Employee Benefits', + expressLogistics: 'Express Logistics', + }, + + procurement: { + title: 'ຈັດການຈັດຊື້', + description: 'ຂະບວນການຈັດຊື້ວັດສະດຸ', + purchaseRequests: 'ຄຳຂໍຈັດຊື້', + purchaseOrders: 'ໃບສັ່ງຊື້', + products: 'ສິນຄ້າ', + inventory: 'ສາງ', + new: 'ສ້າງ', + noData: 'ບໍ່ມີ', + totalRequests: 'ຄຳຂໍ', + totalOrders: 'ສັ່ງຊື້', + totalProducts: 'ສິນຄ້າ', + totalInventory: 'ສາງ', + getListFailed: 'ດຶງລົ້ມເຫຼວ', + noAccess: 'ບໍ່ມີສິດ', + pendingApproval: 'ລໍ', + approved: 'ຜ່ານ', + ordered: 'ສັ່ງ', + received: 'ຮັບ', + cancelled: 'ຍົກເລີກ', + projectFilter: 'ແຍກໂຄງການ', + statusFilter: 'ແຍກສະຖານະ', + newProcurement: 'New Procurement', + orderCode: 'Order Code', + purchaseDate: 'Purchase Date', + supplier: 'Supplier', + materialName: 'Material Name', + quantity: 'Quantity', + unitPrice: 'Unit Price', + totalAmount: 'Total Amount', + status: 'Status', + action: 'Action', + view: 'View', + approve: 'Approve', + stocked: 'Stocked', + rejected: 'Rejected', + startDate: 'Start Date', + endDate: 'End Date', + searchOrder: 'Search order code', + monthPurchase: 'Monthly Purchase Amount', + newApplication: 'New Purchase Application', + selectSupplier: 'Select Supplier', + inputMaterialName: 'Please enter material name', + remark: 'Remark', + remarkPlaceholder: 'Please enter remarks', + submitSuccess: 'Purchase application submitted', + }, + + purchaseRequest: { + title: 'ຄຳຂໍຈັດຊື້', + description: 'ສ້າງ ແລະ ຈັດການຄຳຂໍຈັດຊື້', + newRequest: 'ສ້າງໃໝ່', + edit: 'ແກ້ໄຂ', + delete: 'ລຶບ', + requestNo: 'ເລກທີ', + project: 'ໂຄງການ', + applicant: 'ຜູ້ຂໍ', + amount: 'ຈຳນວນ', + status: 'ສະຖານະ', + draft: 'ຮ່າງ', + pendingApproval: 'ລໍ', + approved: 'ຜ່ານ', + rejected: 'ປະຕິເສດ', + ordered: 'ສັ່ງ', + cancelled: 'ຍົກ', + selectProject: 'ເລືອກ', + requestDate: 'ວັນທີ', + attachment: 'ແນບ', + urgency: 'ຄວາມດ່ວນ', + low: 'ຕ່ຳ', + medium: 'ປານ', + high: 'ສູງ', + urgent: 'ດ່ວນທີ່ສຸດ', + items: 'ລາຍການ', + productName: 'ຊື່', + quantity: 'ຈຳນວນ', + unit: 'ໜ່ວຍ', + estimatedPrice: 'ລາຄາ', + totalPrice: 'ລວມ', + addItem: '+ເພີ່ມ', + noItems: 'ບໍ່ມີ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມ', + getListFailed: 'ດຶງລົ້ມ', + confirmDelete: 'ຢືນຢັນ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມ', + remark: 'ໝາຍ', + remarkPlaceholder: 'ປ້ອນ', + selectProduct: 'ເລືອກສິນຄ້າ', + selectCategory: 'ເລືອກໝວດ', + category: 'ໝວດ', + spec: 'ສະເປັກ', + productSpec: 'ສະເປັກສິນຄ້າ', + existingProduct: 'ສິນຄ້າມີ', + newProduct: 'ສິນຄ້າໃໝ່', + selectExistingProduct: 'ເລືອກສິນຄ້າ', + productCode: 'ລະຫັດ', + activeApplications: 'Active Applications', + completed: 'Completed', + subject: 'Subject', + estimatedAmount: 'Estimated Amount', + demandDate: 'Required Date', + applicationDate: 'Application Date', + code: 'Code', + action: 'Action', + approve: 'Approve', + reject: 'Reject', + withdraw: 'Withdraw', + selectProjectFilter: 'Filter by project', + selectStatusFilter: 'Filter by status', + editRequest: 'Edit Purchase Request', + submitApproval: 'Submit for Approval', + purchaseType: 'Purchase Type', + selectPurchaseType: 'Please select purchase type', + purchaseTypeRequired: 'Please select purchase type', + stockPurchase: 'Stock Purchase', + projectPurchase: 'Project Purchase', + relatedProject: 'Related Project', + projectRequired: 'Project purchase must be linked to a project', + applicantLabel: 'Applicant', + applicantPlaceholder: 'Please enter applicant', + applicantRequired: 'Please enter applicant', + applicationDateLabel: 'Application Date', + dateRequired: 'Please select application date', + subjectDescription: 'Subject Description', + subjectRequired: 'Please enter subject description', + subjectMaxLength: 'Subject description cannot exceed 100 characters', + subjectPlaceholder: 'Briefly describe purchase needs (e.g., cables, poles needed for XX project)', + expenseCategory: 'Expense Category', + categoryRequired: 'Please select expense category', + material: 'Material', + equipment: 'Equipment', + pole: 'Pole', + other: 'Other', + estimatedAmountLabel: 'Estimated Amount', + amountRequired: 'Please enter estimated amount', + estimatedAmountPlaceholder: 'Estimated amount', + currency: 'Currency', + selectCurrency: 'Please select currency', + currencyRequired: 'Please select currency', + demandDateLabel: 'Required Date', + demandDatePlaceholder: 'Expected delivery date', + remarkLabel: 'Remark', + selectFile: 'Select File', + detailTitle: 'Purchase Request Details', + applicationCode: 'Application Code', + createdAt: 'Created At', + createSuccess: 'Created successfully', + submitSuccess: 'Created and submitted successfully', + submitFailed: 'Submit failed', + withdrawSuccess: 'Withdrawn successfully', + withdrawFailed: 'Withdrawal failed', + approveSuccess: 'Approved successfully', + approveFailed: 'Approval failed', + rejectSuccess: 'Rejected successfully', + rejectFailed: 'Rejection failed', + pendingEdit: 'Pending Edit', + executed: 'Executed', + withdrawn: 'Withdrawn', + currencyCNY: 'RMB', + currencyUSD: 'USD', + currencyLAK: 'LAK', + currencyTHB: 'THB', + }, + + purchaseOrder: { + title: 'ໃບສັ່ງຊື້', + description: 'ຈັດການໃບສັ່ງຊື້', + newOrder: 'ສ້າງໃໝ່', + edit: 'ແກ້ໄຂ', + delete: 'ລຶບ', + orderNo: 'ເລກທີ', + project: 'ໂຄງການ', + supplier: 'ຜູ້ສະໜອງ', + amount: 'ຈຳນວນ', + status: 'ສະຖານະ', + draft: 'ຮ່າງ', + pendingApproval: 'ລໍ', + approved: 'ຜ່ານ', + ordered: 'ສັ່ງ', + partiallyReceived: 'ຮັບບາງ', + fullyReceived: 'ຮັບຄົບ', + cancelled: 'ຍົກ', + selectProject: 'ເລືອກ', + selectSupplier: 'ເລືອກ', + orderDate: 'ວັນ', + expectedDate: 'ກຳນົດສົ່ງ', + deliveryAddress: 'ທີ່ສົ່ງ', + paymentTerms: 'ເງື່ອນຈ່າຍ', + items: 'ລາຍການ', + productName: 'ຊື່', + quantity: 'ຈຳນວນ', + receivedQty: 'ຮັບ', + unit: 'ໜ່ວຍ', + unitPrice: 'ລາຄາ', + totalPrice: 'ລວມ', + addItem: '+ເພີ່ມ', + receiveItems: 'ຮັບສິນຄ້າ', + receive: 'ຮັບ', + noItems: 'ບໍ່ມີ', + save: 'ບັນທຶກ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມ', + getList: 'ດຶງ', + getListFailed: 'ດຶງລົ້ມ', + confirmDelete: 'ຢືນຢັນ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມ', + remark: 'ໝາຍ', + remarkPlaceholder: 'ປ້ອນ', + fromRequest: 'ຈາກຄຳຂໍ', + selectRequest: 'ເລືອກ', + currency: 'ສະກຸນ', + totalLabel: 'ລວມ: ', + deliveryDate: 'ສົ່ງ', + orderStatus: 'ສະຖານະ', + supplierContact: 'ຕິດຕໍ່', + supplierPhone: 'ໂທ', + warehouse: 'ສາງ', + receiveDate: 'ວັນຮັບ', + receiverName: 'ຜູ້ຮັບ', + receiveRemark: 'ໝາຍ', + receiveSuccess: 'ຮັບສຳເລັດ', + receiveFailed: 'ຮັບລົ້ມ', + receiveHistory: 'ປະຫວັດຮັບ', + confirmReceive: 'ຢືນຢັນຮັບ', + selectWarehouse: 'ເລືອກສາງ', + inventoryUpdated: 'ສາງອັບເດດແລ້ວ', + basicInfo: 'ຂໍ້ມູນພື້ນຖານ', + productDetails: 'ລາຍລະອຽດສິນຄ້າ', + paymentInfo: 'ຂໍ້ມູນການຊຳລະ', + logisticsInfo: 'ຂໍ້ມູນໂລຈິສຕິກ', + acceptanceRecord: 'ບັນທຶກການຮັບມອບ', + advancePayment: 'ຊຳລະລ່ວງໜ້າ', + deliveryPayment: 'ຊຳລະຄ່າສົ່ງ', + acceptancePayment: 'ຊຳລະຄ່າຮັບມອບ', + finalPayment: 'ຊຳລະສ່ວນທ້າຍ', + addProduct: 'ເພີ່ມສິນຄ້າ', + addProductTitle: 'ເພີ່ມສິນຄ້າ', + editProduct: 'ແກ້ໄຂສິນຄ້າ', + addPaymentPlan: 'ເພີ່ມແຜນຊຳລະ', + addPaymentPlanTitle: 'ເພີ່ມແຜນຊຳລະ', + editPaymentPlan: 'ແກ້ໄຂແຜນຊຳລະ', + selectPhase: 'ເລືອກຂັ້ນຕອນ', + selectProduct: 'ເລືອກສິນຄ້າ', + productNameInput: 'ຊື່ສິນຄ້າ', + specInput: 'ຂໍ້ກຳນົດ', + unitInput: 'ໜ່ວຍ', + quantityInput: 'ຈຳນວນ', + unitPriceInput: 'ລາຄາຕໍ່ໜ່ວຍ', + estimatedAmount: 'ຈຳນວນເງິນປະມານ', + orderAmount: 'ຈຳນວນເງິນຄຳສັ່ງ', + paidAmount: 'ຈຳນວນເງິນທີ່ຊຳລະແລ້ວ', + productTotal: 'ລວມຍ່ອຍ', + supplierCountry: 'ປະເທດຜູ້ສະໜອງ', + noLogistics: 'ບໍ່ມີຂໍ້ມູນໂລຈິສຕິກ', + noAcceptance: 'ບໍ່ມີບັນທຶກການຮັບມອບ', + createdAt: 'ວັນທີສ້າງ', + orderConfirmSuccess: 'ຢືນຢັນຄຳສັ່ງສຳເລັດ', + orderConfirmFailed: 'ຢືນຢັນຄຳສັ່ງລົ້ມເຫຼວ', + orderCancelSuccess: 'ຍົກເລີກຄຳສັ່ງສຳເລັດ', + orderCancelFailed: 'ຍົກເລີກຄຳສັ່ງລົ້ມເຫຼວ', + orderDeleteSuccess: 'ລຶບຄຳສັ່ງສຳເລັດ', + orderDeleteFailed: 'ລຶບຄຳສັ່ງລົ້ມເຫຼວ', + productAddSuccess: 'ເພີ່ມສິນຄ້າສຳເລັດ', + productUpdateSuccess: 'ອັບເດດສິນຄ້າສຳເລັດ', + productDeleteSuccess: 'ລຶບສິນຄ້າສຳເລັດ', + paymentPlanAddSuccess: 'ເພີ່ມແຜນຊຳລະສຳເລັດ', + paymentPlanUpdateSuccess: 'ອັບເດດແຜນຊຳລະສຳເລັດ', + paymentPlanDeleteSuccess: 'ລຶບແຜນຊຳລະສຳເລັດ', + confirmed: 'Confirmed', + partialPayment: 'Partial Payment', + paidOff: 'Paid Off', + inTransit: 'In Transit', + accepted: 'Accepted', + closed: 'Closed', + orderCode: 'Order Code', + relatedProject: 'Project', + paid: 'Paid', + createDate: 'Create Date', + action: 'Action', + confirm: 'Confirm', + cancel: 'Cancel', + confirmCancel: 'Confirm cancel?', + confirmDeleteShort: 'Confirm delete?', + spec: 'Spec', + subtotal: 'Subtotal', + phase: 'Phase', + plannedDate: 'Planned Date', + plannedAmount: 'Planned Amount', + ratioPercent: 'Ratio %', + actualAmount: 'Actual Amount', + pendingPayment: 'Pending Payment', + applied: 'Applied', + paidStatus: 'Paid', + trackingNumber: 'Tracking No.', + origin: 'Origin', + china: 'China', + laos: 'Laos', + logisticsCompany: 'Logistics Company', + freight1: 'Freight 1', + freight2: 'Freight 2', + acceptanceCode: 'Acceptance Code', + acceptanceDate: 'Acceptance Date', + acceptor: 'Acceptor', + acceptedQty: 'Accepted Qty', + selectStatus: 'Filter by status', + detailTitle: 'Purchase Order Details - {code}', + }, + + product: { + title: 'ຈັດການສິນຄ້າ', + description: 'ຈັດການຂໍ້ມູນສິນຄ້າ', + newProduct: 'ສ້າງໃໝ່', + edit: 'ແກ້', + delete: 'ລຶບ', + productCode: 'ລະຫັດ', + productName: 'ຊື່', + category: 'ໝວດ', + spec: 'ສະເປັກ', + unit: 'ໜ່ວຍ', + price: 'ລາຄາ', + stock: 'ສາງ', + status: 'ສະຖານະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + selectCategory: 'ເລືອກ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມ', + getListFailed: 'ດຶງລົ້ມ', + confirmDelete: 'ຢືນຢັນ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມ', + remark: 'ໝາຍ', + remarkPlaceholder: 'ປ້ອນ', + searchByName: 'ຄົ້ນຊື່', + searchByCode: 'ຄົ້ນລະຫັດ', + filterByCategory: 'ແຍກ', + all: 'ທັງ', + noProducts: 'ບໍ່ມີສິນຄ້າ', + addFirstProduct: 'ເພີ່ມ', + importExcel: 'ນຳເຂົ້າ', + exportExcel: 'ສົ່ງອອກ', + totalProducts: '{count} ລາຍ', + productDetail: 'ລາຍລະອຽດ', + priceHistory: 'ປະຫວັດລາຄາ', + minStock: 'ສາງຕ່ຳ', + currentStock: 'ປັດຈຸ', + warningStock: 'ເຕືອນ', + sufficientStock: 'ພຽງພໍ', + insufficientStock: 'ບໍ່ພໍ', + outOfStock: 'ໝົດ', + createProduct: 'ສ້າງ', + editProduct: 'ແກ້', + basicInfo: 'ຂໍ້ມູນ', + image: 'ຮູບ', + uploadImage: 'ອັບ', + noImage: 'ບໍ່ມີຮູບ', + code: 'ລະຫັດສິນຄ້າ', + codePlaceholder: 'ປ້ອນລະຫັດສິນຄ້າ', + codeRequired: 'ກະລຸນາປ້ອນລະຫັດສິນຄ້າ', + name: 'ຊື່ສິນຄ້າ', + namePlaceholder: 'ປ້ອນຊື່ສິນຄ້າ', + nameRequired: 'ກະລຸນາປ້ອນຊື່ສິນຄ້າ', + categoryRequired: 'ກະລຸນາເລືອກໝວດໝູ່', + specPlaceholder: 'ປ້ອນຂໍ້ກຳນົດ', + selectUnit: 'ເລືອກໜ່ວຍ', + unitRequired: 'ກະລຸນາເລືອກໜ່ວຍ', + safetyStock: 'ສາງປອດໄພ', + safetyStockPlaceholder: 'ປ້ອນສາງປອດໄພ', + safetyStockRequired: 'ກະລຸນາປ້ອນສາງປອດໄພ', + createdAt: 'ວັນທີສ້າງ', + piece: 'ອັນ', + meter: 'ແມັດ', + kilometer: 'ກິໂລແມັດ', + ton: 'ໂຕນ', + pole2: 'ເสา', + set: 'ຊຸດ', + unit2: 'ເຄື່ອງ', + detailTitle: 'ລາຍລະອຽດສິນຄ້າ', + brandPlaceholder: 'ປ້ອນຍີ່ຫໍ້', + thumbnail: 'Thumbnail', + model: 'Model', + level1Category: 'Level 1 Category', + level2Category: 'Level 2 Category', + quantity: 'Quantity', + costPrice: 'Cost Price', + brand: 'Brand', + action: 'Action', + confirmDeleteCategory: 'Confirm delete this category?', + addLevel2: 'Add Level 2 Category', + addLevel1: 'Add Level 1 Category', + totalCategories: 'Total Categories', + searchPlaceholder: 'Search product name / model / brand', + downloadTemplate: 'Download Template', + batchUpload: 'Batch Upload', + addProduct: 'Add Product', + productList: 'Product List', + clearFilter: 'Clear Filter', + totalRecords: 'Total {total} records', + categoryManagement: 'Category Management', + addCategory: 'Add Category', + noCategory: 'No categories', + addProductTitle: 'Add Product', + modelPlaceholder: 'e.g., JKLYJ-120-22kV', + selectLevel1: 'Select level 1 category', + level1Required: 'Please select level 1 category', + selectLevel2: 'Select level 2 category (optional)', + level2Extra: 'Optional, defaults to level 1 if not selected', + costPricePlaceholder: 'Default 0', + source: 'Source', + selectSource: 'Select source', + china: 'China', + laos: 'Laos', + specs: 'Specifications', + specsPlaceholder: 'e.g., 120mm², 22kV', + batchUploadTitle: 'Batch Upload Products', + uploadInstructions: 'Upload Instructions:', + uploadStep1: 'Please download the template file first and fill in product information according to the template format', + uploadStep2: 'Supports .xlsx and .xls Excel file formats', + uploadStep3: 'Product name and level 1 category are required fields', + uploadStep4: 'Other fields are optional, fill in as appropriate', + uploadStep5: 'Source field defaults to Laos, can be China/Laos', + uploading: 'Uploading...', + selectExcel: 'Select Excel File', + downloadImportTemplate: 'Download Import Template', + editCategory: 'Edit Category', + addCategoryTitle: 'Add Category', + categoryName: 'Category Name', + categoryNameRequired: 'Please enter category name', + categoryNamePlaceholder: 'e.g., Wire & Cable', + categoryLevel: 'Category Level', + selectLevel: 'Select category level', + levelRequired: 'Please select category level', + parentCategory: 'Parent Category', + selectParent: 'Select parent category (optional)', + parentCategoryExtra: 'When selecting level 2 category, parent category is required', + categoryUpdateSuccess: 'Category updated successfully', + categoryCreateSuccess: 'Category created successfully', + categoryDeleteSuccess: 'Category deleted successfully', + }, + + inventory: { + title: 'ຈັດການສາງ', + description: 'ຈັດການສາງສິນຄ້າ', + warehouse: 'ສາງ', + product: 'ສິນຄ້າ', + quantity: 'ຈຳນວນ', + available: 'ມີ', + reserved: 'ຈອງ', + locked: 'ລັອກ', + inTransit: 'ສົ່ງ', + damaged: 'ເສຍ', + inbound: 'ນຳເຂົ້າ', + outbound: 'ນຳອອກ', + transfer: 'ໂອນ', + adjust: 'ປັບ', + check: 'ກວດ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + saveSuccess: 'ສຳເລັດ', + saveFailed: 'ລົ້ມ', + filterByWarehouse: 'ແຍກສາງ', + filterByProduct: 'ແຍກສິນຄ້າ', + selectWarehouse: 'ເລືອກ', + selectProduct: 'ເລືອກ', + recordType: 'ປະເພດ', + recordDate: 'ວັນ', + recordAmount: 'ຈຳນວນ', + operator: 'ຜູ້ດຳເນີນ', + remark: 'ໝາຍ', + inboundRecord: 'ນຳເຂົ້າ', + outboundRecord: 'ນຳອອກ', + inventoryLog: 'ບັນທຶກ', + stockWarning: 'ສາງເຕືອນ', + lowStockItems: 'ສິນຄ້າສາງຕ່ຳ', + noWarning: 'ບໍ່ມີ', + totalItems: '{count} ລາຍ', + totalValue: 'ມູນຄ່າລວມ', + productCode: 'ລະຫັດສິນຄ້າ', + productName: 'ຊື່ສິນຄ້າ', + spec: 'ຂໍ້ກຳນົດ', + unit: 'ໜ່ວຍ', + stock: 'ສາງ', + safetyStock: 'ສາງປອດໄພ', + lastIn: 'ເຂົ້າຄັ້ງສຸດທ້າຍ', + lastOut: 'ອອກຄັ້ງສຸດທ້າຍ', + status: 'ສະຖານະ', + normal: 'ປົກກະຕິ', + lowStock: 'ສາງຕ່ຳ', + outOfStock: 'ສິນຄ້າໝົດ', + tabInventory: 'ສາງ', + tabLog: 'ບັນທຶກເຂົ້າ-ອອກ', + categoryFilter: 'ກັ່ນຕາມໝວດໝູ່', + stockFilter: 'ກັ່ນສາງ', + all: 'ທັງໝົດ', + time: 'ເວລາ', + type: 'ປະເພດ', + in: 'ເຂົ້າ', + out: 'ອອກ', + before: 'ກ່ອນປ່ຽນ', + after: 'ຫຼັງປ່ຽນ', + orderCode: 'ຄຳສັ່ງທີ່ກ່ຽວຂ້ອງ', + relatedOrder: 'ຄຳສັ່ງທີ່ກ່ຽວຂ້ອງ', + relatedOrderPlaceholder: 'ປ້ອນລະຫັດຄຳສັ່ງທີ່ກ່ຽວຂ້ອງ', + date: 'ວັນທີ', + dateRequired: 'ກະລຸນາເລືອກວັນທີ', + remarkPlaceholder: 'ປ້ອນໝາຍເຫດ', + quantityPlaceholder: 'ປ້ອນຈຳນວນ', + stockInTitle: 'ຮັບສິນຄ້າເຂົ້າ', + stockOutTitle: 'ສົ່ງສິນຄ້າອອກ', + stockInSuccess: 'ຮັບສິນຄ້າເຂົ້າສຳເລັດ', + stockInFailed: 'ຮັບສິນຄ້າເຂົ້າລົ້ມເຫຼວ', + stockOutSuccess: 'ສົ່ງສິນຄ້າອອກສຳເລັດ', + getLogFailed: 'ດຶງຂໍ້ມູນບັນທຶກລົ້ມເຫຼວ', + stockIn: 'Stock In', + stockOut: 'Stock Out', + project: 'Project', + unitPrice: 'Unit Price', + totalAmount: 'Total Amount', + unitLabel: 'Unit', + totalStockIn: 'Total Stock In', + totalStockOut: 'Total Stock Out', + currentStock: 'Current Stock', + stockRecord: 'Stock Records', + stockSummary: 'Stock Summary', + selectProject: 'Filter by project', + selectRecordType: 'Select record type', + stockOutBtn: 'Stock Out', + relatedProject: 'Related Project', + selectProjectRequired: 'Please select project', + selectProductRequired: 'Please select product', + stockOutQuantity: 'Stock Out Quantity', + quantityRequired: 'Please enter stock out quantity', + inputUnitPrice: 'Please enter unit price', + inputTotalAmount: 'Please enter total amount', + inputRemark: 'Please enter remarks', + stockOutFailed: 'Stock out failed', + }, + + supplier: { + title: 'ຈັດການຜູ້ສະໜອງ', + description: 'ຈັດການຂໍ້ມູນຜູ້ສະໜອງ', + newSupplier: 'ສ້າງ', + edit: 'ແກ້', + delete: 'ລຶບ', + supplierCode: 'ລະຫັດ', + supplierName: 'ຊື່', + contactPerson: 'ຜູ້', + contactPhone: 'ໂທ', + contactEmail: 'ອີ', + address: 'ທີ່', + taxId: 'ເລກອາ', + bankAccount: 'ບັນຊີ', + status: 'ສະຖານະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມ', + getListFailed: 'ດຶງລົ້ມ', + confirmDelete: 'ຢືນຢັນ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມ', + remark: 'ໝາຍ', + searchByName: 'ຄົ້ນ', + filterByStatus: 'ແຍກ', + all: 'ທັງ', + supplierDetail: 'ລາຍລະອຽດ', + products: 'ສິນຄ້າ', + orders: 'ສັ່ງຊື້', + totalOrders: 'ລວມ', + bankName: 'ທະນາ', + accountNo: 'ເລກ', + accountName: 'ຊື່', + branch: 'ສາຂາ', + swift: 'SWIFT', + note: 'ໝາຍ', + bankInfo: 'ຂໍ້ມູນທະນາ', + supplierType: 'ປະເພດ', + local: 'ພາຍ', + foreign: 'ຕ່າງ', + totalCount: 'Total Suppliers', + totalPurchase: 'Total Purchase Amount', + totalPayable: 'Total Payable', + searchPlaceholder: 'Search supplier code, name, or supply category', + editSupplier: 'Edit Supplier', + name: 'Name', + supplyCategory: 'Supply Category', + country: 'Country', + purchaseAmount: 'Purchase Amount', + payableAmount: 'Payable Amount', + action: 'Action', + contact: 'Contacts', + mainContact: 'Primary Contact', + paymentInfo: 'Payment Info', + addContact: '+ Add Contact', + addPaymentInfo: '+ Add Payment Info', + qrCode: 'QR Code', + mainAccount: 'Primary Account', + deleteContact: 'Delete', + deletePaymentInfo: 'Delete this payment info', + nameRequired: 'Please enter name', + namePlaceholder: 'Supplier name', + categoryPlaceholder: 'Manual entry: e.g., Electrical Equipment, Building Materials', + china: 'China', + laos: 'Laos', + remarkPlaceholder: 'Remarks', + confirmDeleteMsg: 'Are you sure you want to delete this supplier?', + basicInfo: 'Basic Information', + code: 'Code', + remarkLabel: 'Remark: ', + returnToList: 'Back to List', + ledger: 'Business Ledger', + phoneLabel: 'Phone: ', + positionLabel: 'Position: ', + qrCodeLabel: 'QR Code: ', + accountNameLabel: 'Account Name: ', + accountNumLabel: 'Account No.: ', + bankLabel: 'Bank: ', + notFoundTitle: 'Supplier Not Found', + noContact: 'No contacts', + noPayment: 'No payment info', + mainContactTag: 'Primary Contact', + mainAccountTag: 'Primary Account', + }, + + subcontractor: { + title: 'ຜູ້ຮັບເໝົາຊ່ວງ', + description: 'ຈັດການຂໍ້ມູນຜູ້ຮັບເໝົາ', + new: 'ສ້າງ', + edit: 'ແກ້', + delete: 'ລຶບ', + code: 'ລະຫັດ', + name: 'ຊື່', + contactPerson: 'ຜູ້', + phone: 'ໂທ', + email: 'ອີ', + address: 'ທີ່', + specialty: 'ຊ່ຽວ', + teamSize: 'ຂະໜາດ', + status: 'ສະຖານະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + save: 'ບັນທຶກ', + saveSuccess: 'ສຳເລັດ', + saveFailed: 'ລົ້ມ', + getListFailed: 'ດຶງລົ້ມ', + confirmDelete: 'ຢືນ', + confirmDelMsg: 'ລຶບບໍ?', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + detail: 'ລາຍລະອຽດ', + remark: 'ໝາຍ', + searchByName: 'ຄົ້ນ', + all: 'ທັງ', + filterByStatus: 'ແຍກ', + bankName: 'ທະນາ', + accountNo: 'ເລກ', + accountName: 'ຊື່', + bankInfo: 'ຂໍ້ມູນ', + idCard: 'ບັດ', + license: 'ໃບ', + attachment: 'ແນບ', + rating: 'ຄະແນນ', + completedProjects: 'ສຳເລັດ', + ongoingProjects: 'ດຳເນີນ', + totalPaid: 'ຈ່າຍລວມ', + totalCount: 'Total Subcontractors', + totalContract: 'Total Contract Amount', + totalPayable: 'Total Payable', + searchPlaceholder: 'Search subcontractor code, name, or scope', + newSubcontractor: 'New Subcontractor', + editSubcontractor: 'Edit Subcontractor', + scope: 'Scope', + country: 'Country', + contractAmount: 'Contract Amount', + payableAmount: 'Payable Amount', + action: 'Action', + contact: 'Contacts', + mainContact: 'Primary Contact', + paymentInfo: 'Payment Info', + addContact: '+ Add Contact', + addPaymentInfo: '+ Add Payment Info', + bankAccount: 'Bank Account', + qrCode: 'QR Code', + mainAccount: 'Primary Account', + deleteContact: 'Delete', + deletePaymentInfo: 'Delete this payment info', + nameRequired: 'Please enter name', + namePlaceholder: 'Subcontractor name', + scopePlaceholder: 'Manual entry: e.g., Electrical Installation, Civil Engineering', + china: 'China', + laos: 'Laos', + feature: 'Features', + featurePlaceholder: 'Manual entry: e.g., Professional team, Fully equipped, Reasonable pricing', + remarkPlaceholder: 'Remarks', + confirmDeleteMsg: 'Are you sure you want to delete this subcontractor?', + basicInfo: 'Basic Information', + featureLabel: 'Features: ', + remarkLabel: 'Remark: ', + returnToList: 'Back to List', + ledger: 'Business Ledger', + phoneLabel: 'Phone: ', + positionLabel: 'Position: ', + bankLabel: 'Bank: ', + accountLabel: 'Account: ', + qrCodeLabel: 'QR Code: ', + defaultAccount: 'Default Account', + notFoundTitle: 'Subcontractor Not Found', + noContact: 'No contacts', + noPayment: 'No payment info', + mainContactTag: 'Primary Contact', + }, + + customer: { + title: 'ຈັດການລູກຄ້າ', + description: 'ຈັດການຂໍ້ມູນລູກຄ້າ', + newCustomer: 'ສ້າງ', + edit: 'ແກ້', + delete: 'ລຶບ', + customerCode: 'ລະຫັດ', + customerName: 'ຊື່', + contactPerson: 'ຜູ້', + contactPhone: 'ໂທ', + contactEmail: 'ອີ', + address: 'ທີ່', + taxId: 'ເລກອາ', + status: 'ສະຖານະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + saveSuccess: 'ບັນທຶກສຳເລັດ', + saveFailed: 'ບັນທຶກລົ້ມ', + getListFailed: 'ດຶງລົ້ມ', + confirmDelete: 'ຢືນຢັນ', + deleteSuccess: 'ລຶບສຳເລັດ', + deleteFailed: 'ລຶບລົ້ມ', + noAccess: 'ບໍ່ມີສິດ', + noData: 'ບໍ່ມີ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ດຶງລົ້ມ', + remark: 'ໝາຍ', + searchByName: 'ຄົ້ນ', + filterByStatus: 'ແຍກ', + customerDetail: 'ລາຍລະອຽດ', + projects: 'ໂຄງການ', + totalProjects: 'ລວມ', + customerType: 'ປະເພດ', + government: 'ລັດ', + private: 'ເອກ', + soe: 'ລັດວິ', + invoiceAddress: 'ທີ່ບິນ', + deliveryAddress: 'ທີ່ສົ່ງ', + bankName: 'ທະນາ', + accountNo: 'ເລກ', + accountName: 'ຊື່', + bankInfo: 'ຂໍ້ມູນທະນາ', + representative: 'ຜູ້ແທນ', + position: 'ຕຳແໜ່ງ', + totalCount: 'Total Customers', + totalContract: 'Total Contract Amount', + totalReceivable: 'Total Receivable', + searchPlaceholder: 'Search customer code, name, or address', + editCustomer: 'Edit Customer', + name: 'Name', + mainContact: 'Primary Contact', + paymentInfo: 'Payment Info', + contractAmount: 'Contract Amount', + receivableAmount: 'Receivable Amount', + action: 'Action', + contact: 'Contacts', + addContact: '+ Add Contact', + addPaymentInfo: '+ Add Payment Info', + bankAccount: 'Bank Account', + qrCode: 'QR Code', + mainAccount: 'Primary Account', + deleteContact: 'Delete', + deletePaymentInfo: 'Delete this payment info', + nameRequired: 'Please enter name', + namePlaceholder: 'Customer name', + addressPlaceholder: 'Customer address', + remarkPlaceholder: 'Remarks', + confirmDeleteMsg: 'Are you sure you want to delete this customer?', + basicInfo: 'Basic Information', + code: 'Code', + remarkLabel: 'Remark: ', + returnToList: 'Back to List', + ledger: 'Business Ledger', + relatedBudget: 'Related Budget', + projectName: 'Project Name', + businessManager: 'Business Manager', + inNegotiation: 'In Negotiation', + signed: 'Signed', + unsigned: 'Unsigned', + quotationCount: 'Quotation Versions', + createdAt: 'Created At', + noBudget: 'No related budget projects', + phoneLabel: 'Phone: ', + positionLabel: 'Position: ', + accountNameLabel: 'Account Name:', + accountNumLabel: 'Account No.:', + mainContactTag: 'Primary Contact', + noContact: 'No contacts', + noPayment: 'No payment info', + }, + + logistics: { + title: 'ຈັດການໂລຈິສຕິກ', + description: 'ຈັດການການຂົນສົ່ງ', + shipment: 'ຈັດສົ່ງ', + tracking: 'ຕິດຕາມ', + delivery: 'ສົ່ງ', + receive: 'ຮັບ', + shipmentNo: 'ເລກ', + origin: 'ຕົ້ນ', + destination: 'ປາຍ', + carrier: 'ຜູ້ຂົນ', + status: 'ສະຖານະ', + pending: 'ລໍ', + inTransit: 'ທາງ', + delivered: 'ສົ່ງແລ້ວ', + received: 'ຮັບແລ້ວ', + cancelled: 'ຍົກ', + shipmentDate: 'ວັນ', + estimatedArrival: 'ຮອດ', + actualArrival: 'ຮອດຈິງ', + cost: 'ຄ່າ', + weight: 'ນ້ຳ', + volume: 'ປະ', + packages: 'ຫໍ່', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + save: 'ບັນ', + saveSuccess: 'ສຳເລັດ', + saveFailed: 'ລົ້ມ', + filterStatus: 'ແຍກ', + searchByNo: 'ຄົ້ນ', + addShipment: 'ສ້າງ', + editShipment: 'ແກ້', + shipmentDetail: 'ລາຍລະ', + items: 'ລາຍ', + driver: 'ຄົນ', + driverPhone: 'ໂທ', + vehicleNo: 'ທະ', + remark: 'ໝາຍ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳເລັດ', + deleteFailed: 'ລົ້ມ', + notFound: 'ບໍ່ພົບ', + getDetailFailed: 'ລົ້ມ', + warehouse: 'ສາງ', + arrivalPhoto: 'ຮູບ', + loadingPhoto: 'ຮູບ', + deliveryPhoto: 'ຮູບ', + confirmReceive: 'ຢືນຮັບ', + receiveSuccess: 'ຮັບສຳ', + receiveFailed: 'ຮັບລົ້ມ', + realTimeTracking: 'ຕິດຕາມ', + location: 'ທີ່', + lastUpdate: 'ອັບ', + history: 'ປະຫວັດ', + totalCost: 'ລວມ', + qrCode: 'QR ໂຄ້ດ', + newCompany: 'New Logistics Company', + editCompany: 'Edit Logistics Company', + companyName: 'Company Name', + phone: 'Phone', + quoteDescription: 'Quote Description', + createdAt: 'Created At', + action: 'Action', + contact: 'Contacts', + name: 'Name', + position: 'Position', + phoneLabel: 'Phone', + mainContact: 'Primary Contact', + confirmDeleteShort: 'Confirm delete?', + paymentInfo: 'Payment Info', + accountName: 'Account Name', + bankAccount: 'Bank Account', + bankName: 'Bank Name', + defaultAccount: 'Default', + defaultLabel: 'Default', + orders: 'Orders', + trackingNumber: 'Tracking No.', + purchaseOrder: 'Purchase Order', + deliveryDate: 'Delivery Date', + freight1: 'Freight 1', + freight1Status: 'Freight 1 Status', + freight2: 'Freight 2', + freight2Status: 'Freight 2 Status', + pendingPayment: 'Pending Payment', + applied: 'Applied', + paid: 'Paid', + addContact: 'Add Contact', + addPaymentInfo: 'Add Payment Info', + basicInfo: 'Basic Information', + email: 'Email', + paymentTab: 'Payment Info', + ledger: 'Business Ledger', + editContact: 'Edit Contact', + addContactTitle: 'Add Contact', + editPaymentInfo: 'Edit Payment Info', + addPaymentInfoTitle: 'Add Payment Info', + nameRequired: 'Please enter name', + positionRequired: 'Please enter position', + phoneRequired: 'Please enter phone', + accountNameRequired: 'Please enter account name', + bankAccountRequired: 'Please enter bank account', + bankNameRequired: 'Please enter bank name', + qrCodeRequired: 'Please enter QR code image URL', + isMainContact: 'Is Primary Contact', + isDefaultAccount: 'Is Default Account', + address: 'Address', + addressPlaceholder: 'Please enter address', + quotePlaceholder: 'Please enter quote description (e.g., China-Laos land freight rates, transit time, etc.)', + remarkPlaceholder: 'Please enter remarks', + contactUpdateSuccess: 'Contact updated successfully', + contactAddSuccess: 'Contact added successfully', + contactDeleteSuccess: 'Contact deleted successfully', + paymentInfoUpdateSuccess: 'Payment info updated successfully', + paymentInfoAddSuccess: 'Payment info added successfully', + paymentInfoDeleteSuccess: 'Payment info deleted successfully', + detailTitle: 'Logistics Company Details - {name}', + }, + + businessLedger: { + title: 'ບັນຊີທຸລະກິດ', + description: 'ຈັດການບັນຊີດຸ່ນ', + date: 'ວັນ', + project: 'ໂຄງການ', + type: 'ປະ', + income: 'ຮັບ', + expense: 'ຈ່າຍ', + amount: 'ຈຳ', + balance: 'ດຸ່ນ', + remark: 'ໝາຍ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + filterByProject: 'ແຍກ', + filterByType: 'ປະ', + filterByDate: 'ວັນ', + totalIncome: 'ຮັບລວມ', + totalExpense: 'ຈ່າຍລວມ', + netBalance: 'ດຸ່ນສຸດ', + period: 'ໄລຍະ', + from: 'ຈາກ', + to: 'ຮອດ', + search: 'ຄົ້ນ', + export: 'ສົ່ງ', + detail: 'ລາຍ', + notFound: 'ບໍ່ພົບ', + contractTotal: 'Contract Total', + totalPaid: 'Total Paid', + totalUnpaid: 'Total Unpaid', + projectCount: 'Project Count', + purchaseTotal: 'Purchase Total', + totalReceived: 'Total Received', + totalReceivable: 'Total Receivable', + orderCount: 'Order Count', + freight1Total: 'Freight 1 Total', + logisticsCount: 'Logistics Count', + code: 'Code', + name: 'Name', + contractAmount: 'Contract Amount', + status: 'Status', + purchaseAmount: 'Purchase Amount', + freight1: 'Freight 1', + freight1Status: 'Freight 1 Status', + completed: 'Completed', + inProgress: 'In Progress', + planning: 'Planning', + pending: 'Pending', + approved: 'Approved', + paid: 'Paid', + applied: 'Applied', + noRecord: 'No business records', + }, + + exchangeRate: { + title: 'ອັດຕາແລກປ່ຽນ', + description: 'ຕັ້ງຄ່າອັດຕາແລກປ່ຽນ', + baseCurrency: 'ສະກຸນ', + targetCurrency: 'ເປົ້າ', + rate: 'ອັດ', + date: 'ວັນ', + effectiveDate: 'ວັນ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + save: 'ບັນ', + saveSuccess: 'ສຳ', + saveFailed: 'ລົ້ມ', + edit: 'ແກ້', + delete: 'ລຶບ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + notFound: 'ບໍ່ພົບ', + add: 'ເພີ່ມ', + setDefault: 'ຕັ້ງ', + defaultRate: 'ຄ່າ', + latestRate: 'ຫຼ້າ', + historicalRates: 'ປະຫວັດ', + rateHistory: 'ປະຫວັດ', + currencyPair: 'ຄູ່', + buyRate: 'ຊື້', + sellRate: 'ຂາຍ', + midRate: 'ກາງ', + CNYLAK: 'CNY-LAK Rate', + CNY: 'RMB', + LAK: 'LAK', + CNYUSD: 'CNY-USD Rate', + USD: 'USD', + CNYTHB: 'CNY-THB Rate', + THB: 'THB', + USDLAK: 'USD-LAK Rate', + THBLAK: 'THB-LAK Rate', + ratePair: 'Rate Pair', + setTime: 'Set Time', + setBy: 'Set By', + lastUpdated: 'Last Updated: ', + actualRate: 'Actual Rate: ', + confirmSave: 'Confirm Save Rate', + historyRate: 'Historical Rate Records', + tipText: 'Tip: Enter either side value and the other side will auto-calculate. Actual rate displays as 1 source currency = X target currency. Click "Confirm Save Rate" to save current settings to database.', + getRateFailed: 'Failed to get exchange rate', + noChange: 'No exchange rate changes detected', + inputFrom: 'Enter {from} amount', + inputTo: 'Enter {to} amount', + }, + + projectCost: { + title: 'ຕົ້ນທຶນໂຄງການ', + description: 'ວິເຄາະຕົ້ນທຶນໂຄງການ', + project: 'ໂຄງການ', + budget: 'ງົບ', + actual: 'ຕົວ', + deviation: 'ຕ່າງ', + ratio: 'ອັດ(%)', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + material: 'ວັດສະ', + labor: 'ແຮງ', + subcontract: 'ເໝົາ', + other: 'ອື່ນ', + totalCost: 'ລວມ', + costBreakdown: 'ລະອຽດ', + percentage: '%', + overBudget: 'ເກີນ', + underBudget: 'ຕ່ຳ', + withinBudget: 'ພໍດີ', + selectProject: 'ກະລຸນາເລືອກໂຄງການ', + contractAmount: 'ມູນຄ່າສັນຍາ', + purchaseCost: 'ຕົ້ນທຶນຈັດຊື້', + paymentExpense: 'ລາຍຈ່າຍຊຳລະ', + totalIncome: 'ລາຍຮັບລວມ', + totalExpense: 'ລາຍຈ່າຍລວມ', + profit: 'ກຳໄລ', + profitRate: 'ອັດຕາກຳໄລ', + totalCostBreakdown: 'ອົງປະກອບຕົ້ນທຶນລວມ', + costProgress: 'ຄວາມຄືບໜ້າຕົ້ນທຶນ', + costRatio: 'ສັດສ່ວນຕົ້ນທຶນ/ສັນຍາ', + purchaseCategoryBreakdown: 'ໝວດໝູ່ຕົ້ນທຶນຈັດຊື້', + incomeBreakdown: 'ລາຍລະອຽດລາຍຮັບ', + expenseBreakdown: 'ລາຍລະອຽດລາຍຈ່າຍ', + expenseByLevel1: 'ສະຫຼຸບລາຍຈ່າຍຕາມໝວດໝູ່', + overviewTab: 'ພາບລວມ', + detailsTab: 'ລາຍການທຸລະກຳ', + detail: 'ລາຍລະອຽດ', + equipment: 'ອຸປະກອນ', + pole: 'ເສົາໄຟ', + getDataFailed: 'ດຶງຂໍ້ມູນຕົ້ນທຶນລົ້ມເຫຼວ', + }, + + systemLogs: { + title: 'ບັນທຶກລະບົບ', + description: 'ເບິ່ງບັນທຶກການດຳເນີນ', + timestamp: 'ເວລາ', + user: 'ຜູ້', + action: 'ດຳ', + module: 'ໂມ', + ip: 'IP', + detail: 'ລາຍ', + status: 'ສະ', + success: 'ສຳ', + failed: 'ລົ້ມ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + filterByUser: 'ຜູ້', + filterByModule: 'ໂມ', + filterByDate: 'ວັນ', + fromDate: 'ຈາກ', + toDate: 'ຮອດ', + search: 'ຄົ້ນ', + clear: 'ລ້າງ', + clearConfirm: 'ຢືນຢັນ?', + clearSuccess: 'ລ້າງສຳເລັດ', + clearFailed: 'ລ້າງລົ້ມ', + export: 'ສົ່ງ', + logDetail: 'ລາຍ', + notFound: 'ບໍ່ພົບ', + logId: 'Log ID', + time: 'Time', + level: 'Level', + operator: 'Operator', + operation: 'Operation', + ipAddress: 'IP Address', + logLevel: 'Log Level', + selectModule: 'Module', + searchPlaceholder: 'Search log content', + moduleUser: 'User Management', + moduleProject: 'Project Management', + moduleFinance: 'Finance Management', + moduleSystem: 'System', + login: 'User Login', + createProject: 'Create Project', + approveAdvance: 'Approve Advance', + dataBackup: 'Data Backup', + }, + + about: { + title: 'ກ່ຽວ', + description: 'ຂໍ້ມູນລະບົບ', + systemName: 'ຊິງຢວນ', + version: 'ເວີ', + buildDate: 'ວັນສ້າງ', + techStack: 'ເທັກ', + frontend: 'React', + backend: 'Node.js', + database: 'PostgreSQL', + developer: 'OpenClaw AI', + contact: 'ຕິດ', + license: 'ສິດ', + copyright: 'ລິຂ', + allRights: 'ສະຫງວນ', + systemInfo: 'ຂໍ້ມູນ', + dependencies: 'ແພັກ', + noData: 'ບໍ່ມີ', + noAccess: 'ບໍ່ມີ', + aboutDesc: 'ຂໍ້ມູນລະບົບ', + releaseNotes: 'ບັນທຶກ', + supportEmail: 'ອີ', + website: 'ເວັບ', + documentation: 'ເອກະ', + downloadApp: 'ດາວ', + mobileVersion: 'ມື', + allRightsReserved: 'ສະຫງວນ', + termsOfService: 'ເງື່ອນ', + privacyPolicy: 'ຄວາມ', + updateLog: 'ປະຫວັດ', + contributors: 'ຜູ້', + acknowledgements: 'ຂອບ', + technicalSupport: 'ສະ', + feedback: 'ຕຳ', + leaveMessage: 'ຂໍ້', + systemNameValue: 'Qingyuan Power Lao ERP', + versionValue: 'V1.0.0', + devTeam: 'Development Team', + devTeamValue: 'Qingyuan Power IT Department', + onlineDate: 'Launch Date', + onlineDateValue: 'March 2026', + techArchitecture: 'Technical Architecture', + deployEnv: 'Deployment Environment', + deployEnvValue: 'Tencent Cloud Server', + frontendValue: 'Vite + React + TypeScript', + backendValue: 'Express.js + PostgreSQL', + modules: 'Feature Modules', + serverStatus: 'Server Status', + databaseStatus: 'Database Status', + cpuUsage: 'CPU Usage', + memoryUsage: 'Memory Usage', + diskSpace: 'Disk Space', + serverIp: 'Server IP', + os: 'Operating System', + osValue: 'OpenCloudOS 9', + nodeVersion: 'Node Version', + running: 'Running Normally', + dbName: 'Database Name', + connectionStatus: 'Connection Status', + normal: 'Normal', + lastBackup: 'Last Backup', + footer: '© 2026 Qingyuan Power Lao ERP System - Version V1.0.0', + }, + + backup: { + title: 'ສຳຮອງ', + description: 'ຈັດການສຳຮອງ', + createBackup: 'ສ້າງ', + restore: 'ກູ້', + download: 'ດາວ', + delete: 'ລຶບ', + backupDate: 'ວັນ', + backupSize: 'ຂະ', + backupType: 'ປະ', + full: 'ເຕັມ', + incremental: 'ເພີ່ມ', + status: 'ສະ', + completed: 'ສຳ', + failed: 'ລົ້ມ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + createSuccess: 'ສຳ', + createFailed: 'ລົ້ມ', + restoreSuccess: 'ກູ້ສຳ', + restoreFailed: 'ກູ້ລົ້ມ', + restoreConfirm: 'ຢືນ', + restoreWarning: 'ຂໍ້ມູນຈະຖືກແທນ', + notFound: 'ບໍ່ພົບ', + backupName: 'Backup Name', + backupTime: 'Backup Time', + fileSize: 'File Size', + auto: 'Auto', + manual: 'Manual', + success: 'Success', + action: 'Action', + totalBackups: 'Total Backups', + totalSize: 'Total Size', + lastBackup: 'Last Backup', + storageSpace: 'Storage Space', + backupList: 'Backup List', + autoBackupSetting: 'Auto Backup Settings', + immediateBackup: 'Backup Now', + backupCreated: 'Backup created successfully', + }, + + processManagement: { + title: 'ຈັດການຂະບວນການ', + description: 'ຕັ້ງຄ່າຂະບວນການອະນຸມັດ', + approvalFlows: 'ຂະບວນ', + executionFlows: 'ດຳເນີນ', + addFlow: 'ສ້າງ', + edit: 'ແກ້', + delete: 'ລຶບ', + flowName: 'ຊື່', + flowType: 'ປະ', + steps: 'ຂັ້ນ', + status: 'ສະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + save: 'ບັນ', + saveSuccess: 'ສຳ', + saveFailed: 'ລົ້ມ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + notFound: 'ບໍ່', + stepName: 'ຊື່', + stepOrder: 'ລຳ', + assignee: 'ຜູ້', + assigneeType: 'ປະ', + role: 'ບົດ', + user: 'ຜູ້', + department: 'ພະ', + addStep: '+ເພີ່ມ', + removeStep: 'ລຶບ', + configureSteps: 'ຕັ້ງ', + noSteps: 'ບໍ່ມີ', + dragToReorder: 'ລາກ', + flowDetail: 'ລາຍ', + setDefault: 'ຕັ້ງ', + advanceProcess: 'ຂະບວນການເງິນລ່ວງໜ້າ', + advanceProcessDesc: 'ຂະບວນການອະນຸມັດຄຳຂໍເງິນລ່ວງໜ້າ', + reimburseProcess: 'ຂະບວນການເບີກຄ່າໃຊ້ຈ່າຍ', + reimburseProcessDesc: 'ຂະບວນການອະນຸມັດຄຳຂໍເບີກຄ່າໃຊ້ຈ່າຍ', + paymentProcess: 'ຂະບວນການຊຳລະ', + paymentProcessDesc: 'ຂະບວນການອະນຸມັດຄຳຂໍຊຳລະ', + verificationProcess: 'ຂະບວນການກວດສອບ', + verificationProcessDesc: 'ຂະບວນການອະນຸມັດຄຳຂໍກວດສອບ', + submitApplication: 'ສົ່ງຄຳຂໍ', + approvalNode: 'ຈຸດອະນຸມັດ', + executePayment: 'ດຳເນີນການຊຳລະ', + roleApplicant: 'ຜູ້ຍື່ນຄຳຂໍ', + roleManager: 'ຜູ້ຈັດການໂຄງການ', + roleFinance: 'ເຈົ້າໜ້າທີ່ການເງິນ', + roleAdmin: 'ຜູ້ເບິ່ງແຍງລະບົບ', + selectRole: 'ເລືອກບົດບາດ', + selectRolePlaceholder: 'ກະລຸນາເລືອກບົດບາດອະນຸມັດ', + nodeSaved: 'ບັນທຶກຈຸດສຳເລັດ', + tipTitle: 'ຄຳແນະນຳ', + tipContent: 'ຫຼັງແກ້ໄຂບົດບາດຈຸດຂະບວນການ ຄຳຂໍໃໝ່ຈະໃຊ້ຂະບວນການອະນຸມັດໃໝ່', + warning: 'ຄຳເຕືອນ', + flowchart: 'Current Flowchart', + nodeConfig: 'Node Configuration', + applicableProcess: 'Applicable Process', + processType: 'Process Type', + desc: 'Description', + enabled: 'Enabled', + disabled: 'Disabled', + sequence: 'Sequence', + nodeName: 'Node Name', + executeRole: 'Execution Role', + action: 'Action', + editNode: 'Edit Node: {name}', + }, + + processTemplate: { + title: 'ແມ່ແບບວິສະວະກຳ', + description: 'ຈັດການແມ່ແບບຂັ້ນຕອນກໍ່ສ້າງ', + addTemplate: 'ສ້າງ', + edit: 'ແກ້', + delete: 'ລຶບ', + templateName: 'ຊື່', + templateType: 'ປະ', + phases: 'ໄລຍະ', + status: 'ສະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + save: 'ບັນ', + saveSuccess: 'ສຳ', + saveFailed: 'ລົ້ມ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + notFound: 'ບໍ່', + phaseName: 'ຊື່', + phaseOrder: 'ລຳ', + phaseType: 'ປະ', + sequential: 'ລຳ', + parallel: 'ຂະ', + duration: 'ມື້', + addPhase: '+ເພີ່ມ', + removePhase: 'ລຶບ', + configurePhases: 'ຕັ້ງ', + noPhases: 'ບໍ່', + dragToReorder: 'ລາກ', + templateDetail: 'ລາຍ', + standardTemplate: 'ມາດ', + applyToProject: 'ໃຊ້', + apply: 'ໃຊ້', + applySuccess: 'ໃຊ້ສຳ', + applyFailed: 'ໃຊ້ລົ້ມ', + selectProject: 'ເລືອກ', + applyConfirm: 'ຢືນ', + applyConfirmMsg: 'ຂໍ້ມູນຈະຖືກແທນ?', + basicInfo: 'Basic Info', + designPhase: 'Design Phases', + preview: 'Preview & Confirm', + templateDescription: 'Template Description', + namePlaceholder: 'e.g., Distribution Installation Project', + descPlaceholder: 'Describe the type of project this template applies to', + newTemplate: 'New Template', + editTemplate: 'Edit Project Template', + phaseCount: 'Phase Count', + desc: 'Description', + action: 'Action', + copy: 'Copy', + phaseNamePlaceholder: 'e.g., Material Procurement', + serial: 'Serial (must wait for dependencies)', + dependency: 'Dependencies (which phases must complete first)', + subItems: 'Sub-items (one per line)', + subItemsPlaceholder: 'Pole procurement\nTransformer procurement\nCable procurement', + dependencyLabel: 'Dependencies: ', + emptySubItems: 'No sub-items', + noPhase: 'No phases. Click below to add.', + saveEdit: 'Save Changes', + confirmCreate: 'Confirm Create', + cancel: 'Cancel', + prev: 'Previous', + next: 'Next', + copySuccess: 'Copied successfully', + copyFailed: 'Copy failed', + nameRequired: 'Please enter template name', + phaseRequired: 'Please add at least one phase', + updateSuccess: 'Template updated successfully', + createSuccess: 'Template created successfully', + phaseNameEmpty: 'Phase name cannot be empty', + systemPreset: 'System Preset', + serialLabel: 'Serial', + parallelLabel: 'Parallel', + dependencyLabelShort: 'Dep: ', + phaseEdit: 'Phase Edit', + }, + + expenseCategory: { + title: 'ໝວດການເງິນ', + description: 'ຈັດການໝວດລາຍຮັບ/ຈ່າຍ', + add: 'ເພີ່ມ', + edit: 'ແກ້', + delete: 'ລຶບ', + categoryName: 'ຊື່', + categoryType: 'ປະ', + income: 'ຮັບ', + expense: 'ຈ່າຍ', + parent: 'ແມ່', + level: 'ຊັ້ນ', + code: 'ລະຫັດ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່ມີ', + save: 'ບັນ', + saveSuccess: 'ສຳ', + saveFailed: 'ລົ້ມ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + notFound: 'ບໍ່', + noParent: 'ບໍ່ມີ', + subCategories: 'ຍ່ອຍ', + selectParent: 'ເລືອກ', + categoryCode: 'ລະຫັດ', + remark: 'ໝາຍ', + all: 'ທັງ', + editCategory: 'Edit Category', + addCategory: 'Add Category', + id: 'ID', + level1: 'Level 1 Category', + level2Code: 'Level 2 Code', + displayName: 'Display Name', + desc: 'Description', + order: 'Order', + status: 'Status', + action: 'Action', + refresh: 'Refresh', + projectExpense: 'Project Expense', + companyExpense: 'Company Expense', + getFailed: 'Failed to get categories', + enabled: 'Enabled', + disabled: 'Disabled', + selectLevel1: 'Please select', + inputLevel2: 'Please enter', + codePlaceholder: 'e.g., material, salary', + namePlaceholder: 'e.g., Material Purchase', + }, + + excelImport: { + title: 'ນຳເຂົ້າ Excel', + description: 'ນຳເຂົ້າຂໍ້ມູນຈາກ Excel', + selectFile: 'ເລືອກ', + uploadFile: 'ອັບ', + downloadTemplate: 'ດາວ', + template: 'ແມ່', + import: 'ນຳ', + importType: 'ປະ', + project: 'ໂຄງ', + product: 'ສິນ', + supplier: 'ຜູ້', + customer: 'ລູກ', + employee: 'ພະ', + selectType: 'ເລືອກ', + preview: 'ຕົວ', + importing: 'ນຳ...', + importSuccess: 'ນຳສຳ', + importFailed: 'ນຳລົ້ມ', + noAccess: 'ບໍ່ມີ', + parseError: 'ຜິດ', + validationError: 'ຜິດ', + successCount: 'ສຳ: {count}', + failCount: 'ລົ້ມ: {count}', + skipCount: 'ຂ້າມ: {count}', + totalCount: 'ລວມ: {count}', + importResult: 'ຜົນ', + downloadErrors: 'ດາວ', + close: 'ປິດ', + confirmImport: 'ຢືນ', + confirmImportMsg: 'ຢືນຢັນນຳເຂົ້າ {count} ລາຍ?', + rowNumber: 'ແຖວ', + errorMessage: 'ຜິດ', + fileFormat: 'ຮູບ', + dragOrClick: 'ລາກ', + maxFileSize: 'ຂະ', + supportedFormats: 'ຮອງ', + processing: 'ດຳ', + downloadResult: 'ຜົນ', + templateRequirements: 'Excel Template Format Requirements', + columnOrder: 'Column Order: Date | Income/Expense Type | Level 1 Category | Level 2 Category | Project Name | Amount | Currency | Exchange Rate | Equivalent RMB | Counterparty Name | Counterparty Type | Person Name | Description | Voucher No.', + formatRequirements: 'Income/Expense Type: Income / Expense | Level 1 Category: Income / Project Expense / Company Expense | Currency: CNY / USD / LAK / THB', + categoryRequirements: 'Level 2 categories must use existing system category names (e.g., Material Purchase, Salary & Benefits, etc.)', + templateFileName: 'Finance Ledger Import Template.xlsx', + noData: 'Excel file has no data rows', + invalidDate: 'Date is empty', + invalidType: 'Invalid income/expense type: {type}', + invalidLevel1: 'Invalid level 1 category: {level1}', + invalidLevel2: 'Invalid level 2 category: {level2}', + amountPositive: 'Amount must be greater than 0', + projectRequired: 'Project expense/income must have a project name', + parseComplete: 'Parsing complete, total {count} records', + parseFailed: 'Excel parsing failed: ', + noValidData: 'No valid data to import', + importComplete: 'Import complete: {success} succeeded, {fail} failed', + rowNum: 'Row', + date: 'Date', + incomeExpense: 'I/E', + income: 'Income', + expense: 'Expense', + level1: 'L1', + projectLabel: 'Project', + companyLabel: 'Company', + level2: 'L2', + amount: 'Amount', + currency: 'Currency', + rate: 'Rate', + equivalentCNY: 'Eqv. RMB', + desc: 'Desc', + validation: 'Validation', + countPrefix: 'Total ', + countSuffix: ' records', + validPrefix: 'Valid ', + errorPrefix: 'Error ', + importPrefix: 'Importing ', + importSuffix: ' valid records', + rowPrefix: 'Row ', + rowSuffix: ': ', + }, + + errorBoundary: { + title: 'ຜິດພາດ', + description: 'ມີຂໍ້ຜິດພາດ', + retry: 'ລອງ', + goHome: 'ໜ້າ', + reportError: 'ລາຍ', + errorInfo: 'Error Info:', + stackTrace: 'Stack Trace:', + refresh: 'Refresh Page', + }, + + fileUpload: { + upload: 'ອັບ', + dragTip: 'ລາກ', + maxSize: 'ຂະ', + maxCount: 'ຈຳ', + formatError: 'ຮູບ', + sizeError: 'ຂະ', + countError: 'ຈຳ', + uploading: 'Uploading...', + preview: 'Image Preview', + uploadSuccess: 'Upload successful', + uploadFailed: 'Upload failed', + }, + + component: { + selectAll: 'ທັງ', + clearAll: 'ລ້າງ', + expandAll: 'ຂະ', + collapseAll: 'ຫຍໍ້', + phonePrefix: 'Phone: ', + wechatPrefix: 'WeChat: ', + whatsappLabel: 'WhatsApp', + whatsappPlaceholder: 'Enter WhatsApp number', + }, + + roles: { + title: 'ການຈັດການບົດບາດ', + description: 'ຕັ້ງຄ່າສິດບົດບາດ', + add: 'ເພີ່ມ', + edit: 'ແກ້', + delete: 'ລຶບ', + roleName: 'ຊື່', + roleCode: 'ລະຫັດ', + permissions: 'ສິດ', + status: 'ສະ', + active: 'ໃຊ້', + inactive: 'ຢຸດ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່', + save: 'ບັນ', + saveSuccess: 'ສຳ', + saveFailed: 'ລົ້ມ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + notFound: 'ບໍ່', + permissionDetail: 'ສິດ', + view: 'ເບິ່ງ', + create: 'ສ້າງ', + update: 'ອັບ', + print: 'ພິມ', + export: 'ສົ່ງ', + approve: 'ອະ', + all: 'ທັງ', + none: 'ບໍ່', + selectAll: 'ທັງ', + deselectAll: 'ລ້າງ', + back: 'ກັບ', + roleDetail: 'ລາຍ', + usersInRole: 'ຜູ້', + addUser: 'ເພີ່ມ', + removeUser: 'ລຶບ', + selectRole: 'ເລືອກ', + noUsers: 'ບໍ່ມີ', + selectUser: 'ເລືອກ', + confirmRemoveUser: 'ລຶບ?', + addUserSuccess: 'ສຳ', + removeUserSuccess: 'ລຶບສຳ', + userCount: '{count}', + permissionCount: '{count}', + menuPermission: 'ເມນູ', + actionPermission: 'ດຳ', + dataPermission: 'ຂໍ້', + customPermission: 'ກຳ', + departmentScope: 'ພະ', + enterpriseScope: 'ທັງ', + searchRole: 'Search Roles', + newRole: 'New Role', + roleId: 'Role ID', + roleDesc: 'Role Description', + permCount: 'Permission Count', + createdAt: 'Created At', + creator: 'Creator', + action: 'Action', + viewPerm: 'View Permissions', + superAdmin: 'Super Admin', + superAdminDesc: 'Has all system permissions', + admin: 'System', + adminDesc: 'Project management, construction management permissions', + financeManager: 'Finance Manager', + financeManagerDesc: 'Finance management, approval permissions', + employee: 'Employee', + employeeDesc: 'View and application permissions', + roleNameRequired: 'Please enter role name', + roleDescRequired: 'Please enter role description', + roleCreated: 'Role created', + permConfig: 'Permission Configuration', + permProject: 'Project Management', + permViewProject: 'View Projects', + permCreateProject: 'Create Projects', + permEditProject: 'Edit Projects', + permDeleteProject: 'Delete Projects', + permFinance: 'Finance Management', + permViewFinance: 'View Finance', + permApproveAdvance: 'Approve Advances', + permApproveReimburse: 'Approve Reimbursements', + permApprovePayment: 'Approve Payments', + permProcurement: 'Procurement Management', + permViewProcurement: 'View Procurement', + permCreateProcurement: 'Create Procurement', + permApproveProcurement: 'Approve Procurement', + permSystem: 'System Settings', + permUserManagement: 'User Management', + permRoleManagement: 'Role Management', + permSystemConfig: 'System Configuration', + }, + + users: { + title: 'ຈັດການຜູ້ໃຊ້', + description: 'ຈັດການຂໍ້ມູນຜູ້ໃຊ້', + add: 'ເພີ່ມ', + edit: 'ແກ້', + delete: 'ລຶບ', + resetPwd: 'ລະຫັດ', + username: 'ຊື່', + name: 'ຊື່', + email: 'ອີ', + phone: 'ໂທ', + role: 'ບົດ', + status: 'ສະ', + active: 'ໃຊ້', + disabled: 'ປິດ', + noData: 'ບໍ່ມີ', + getListFailed: 'ດຶງລົ້ມ', + noAccess: 'ບໍ່', + save: 'ບັນ', + saveSuccess: 'ສຳ', + saveFailed: 'ລົ້ມ', + confirmDelete: 'ຢືນ', + deleteSuccess: 'ສຳ', + deleteFailed: 'ລົ້ມ', + confirmResetPwd: 'ຕັ້ງໃໝ່?', + resetPwdSuccess: 'ສຳ', + resetPwdFailed: 'ລົ້ມ', + notFound: 'ບໍ່', + filterRole: 'ບົດ', + filterStatus: 'ສະ', + searchName: 'ຄົ້ນ', + all: 'ທັງ', + selectRole: 'ເລືອກ', + inputUsername: 'ປ້ອນ', + inputName: 'ປ້ອນ', + inputPhone: 'ປ້ອນ', + inputEmail: 'ປ້ອນ', + password: 'ລະຫັດ', + confirmPassword: 'ຢືນ', + passwordMinLen: '6', + passwordMismatch: 'ບໍ່ກົງ', + userDetail: 'ລາຍ', + createUser: 'ສ້າງ', + editUser: 'ແກ້', + addSuccess: 'ເພີ່ມຜູ້ໃຊ້ສຳເລັດ', + confirmPasswordPlaceholder: 'ກະລຸນາປ້ອນລະຫັດຜ່ານອີກຄັ້ງ', + getListFailedLog: 'ດຶງຂໍ້ມູນບັນທຶກການດຳເນີນການລົ້ມເຫຼວ', + initialPassword: 'ລະຫັດຜ່ານເລີ่มຕົ້ນ', + initialPasswordPlaceholder: 'ປ້ອນລະຫັດຜ່ານເລີ່ມຕົ້ນ', + namePlaceholder: 'ປ້ອນຊື່', + newPasswordPlaceholder: 'ປ້ອນລະຫັດຜ່ານໃໝ່', + notAdmin: 'ຕ້ອງມີສິດຜູ້ເບິ່ງແຍງ', + reEnterPassword: 'ປ້ອນລະຫັດຜ່ານອີກຄັ້ງ', + selectRolePlaceholder: 'ກະລຸນາເລືອກບົດບາດ', + unknownError: 'ຂໍ້ຜິດພາດບໍ່ຮູ້', + usernamePlaceholder: 'ປ້ອນຊື່ຜູ້ໃຊ້', + newUser: 'New User', + id: 'ID', + avatar: 'Avatar', + user: 'User', + action: 'Action', + resetPassword: 'Reset Password', + confirmDeleteMsg: 'Are you sure you want to delete user {name}?', + getUserFailed: 'Failed to get user list', + }, + + userManagement: { + title: 'ຈັດການຜູ້ໃຊ້', + description: 'ຈັດການຜູ້ໃຊ້ ແລະ ບົດບາດ', + users: 'ຜູ້', + roles: 'ບົດ', + departments: 'ພະ', + batchAssign: 'ມອບ', + batchRole: 'ມອບ', + import: 'ນຳ', + export: 'ສົ່ງ', + onlineCount: '{count}', + testPage: 'This is a test page to check if API calls are working correctly.', + refreshList: 'Refresh User List', + errorPrefix: 'Error: ', + apiResult: 'API returned data:', + loadStatus: 'Load Status:', + loadComplete: 'Load Complete', + apiFailed: 'API returned failure: ', + unknownError: 'Unknown error', + }, +}; \ No newline at end of file diff --git a/frontend/src/locales/th-TH.ts b/frontend/src/locales/th-TH.ts index 379f4b1..5891f91 100644 --- a/frontend/src/locales/th-TH.ts +++ b/frontend/src/locales/th-TH.ts @@ -1,69 +1,2551 @@ -export default { - // Common - common: { - confirm: 'ยืนยัน', - cancel: 'ยกเลิก', - save: 'บันทึก', - delete: 'ลบ', - edit: 'แก้ไข', - add: 'เพิ่ม', - search: 'ค้นหา', - reset: 'รีเซ็ต', - submit: 'ส่ง', - back: 'กลับ', - loading: 'กำลังโหลด...', - success: 'ดำเนินการสำเร็จ', - failed: 'ดำเนินการล้มเหลว', - required: 'จำเป็นต้องกรอก' - }, - - // Login - login: { - title: 'Qingyuan Power Laos ERP', - subtitle: 'แพลตฟอร์มการจัดการโครงการและการเงิน', - username: 'ชื่อผู้ใช้', - password: 'รหัสผ่าน', - loginButton: 'เข้าสู่ระบบ', - usernamePlaceholder: 'กรุณากรอกชื่อผู้ใช้', - passwordPlaceholder: 'กรุณากรอกรหัสผ่าน', - usernameRequired: 'กรุณากรอกชื่อผู้ใช้', - passwordRequired: 'กรุณากรอกรหัสผ่าน', - usernameMin: 'ชื่อผู้ใช้ต้องมีอย่างน้อย 3 ตัวอักษร', - passwordMin: 'รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร', - loginFailed: 'การเข้าสู่ระบบล้มเหลว กรุณาลองอีกครั้ง', - testAccounts: 'บัญชีทดสอบ', - techSupport: 'การสนับสนุนด้านเทคนิค: OpenClaw AI + React + Node.js', - selectLanguage: 'เลือกภาษา' - }, - - // Menu - menu: { - dashboard: 'แดชบอร์ด', - projects: 'การจัดการโครงการ', - advances: 'การจัดการเงินทดรอง', - reimbursements: 'การจัดการเบิกเงิน', - finance: 'การจัดการการเงิน', - reports: 'รายงาน', - settings: 'การตั้งค่าระบบ' - }, - - // User - user: { - profile: 'ข้อมูลส่วนตัว', - settings: 'การตั้งค่าระบบ', - logout: 'ออกจากระบบ', - admin: 'ผู้ดูแลระบบ', - finance: 'เจ้าหน้าที่การเงิน', - manager: 'ผู้จัดการโครงการ', - employee: 'พนักงาน' - }, - - // Features - features: { - projectManage: 'การจัดการโครงการ: สร้าง ติดตาม และวิเคราะห์ความคืบหน้า', - advanceManage: 'การจัดการเงินทดรอง: กระบวนการขอและอนุมัติ', - reimburseManage: 'การจัดการเบิกเงิน: กระบวนการเบิกค่าใช้จ่าย', - financeReport: 'รายงานการเงิน: วิเคราะห์ต้นทุนและกำไรโครงการ', - mobileSupport: 'รองรับมือถือ: เทคโนโลยี PWA สามารถเพิ่มในหน้าจอหลัก' - } -} +export default { + common: { + confirm: 'ยืนยัน', + cancel: 'ยกเลิก', + save: 'บันทึก', + delete: 'ลบ', + edit: 'แก้ไข', + add: 'เพิ่ม', + search: 'ค้นหา', + reset: 'รีเซ็ต', + submit: 'ส่ง', + back: 'กลับ', + loading: 'กำลังโหลด...', + success: 'ดำเนินการสำเร็จ', + failed: 'ดำเนินการล้มเหลว', + required: 'จำเป็นต้องกรอก', + close: 'ปิด', + view: 'ดู', + refresh: 'รีเฟรช', + create: 'สร้าง', + upload: 'อัปโหลด', + download: 'ดาวน์โหลด', + export: 'ส่งออก', + import: 'นำเข้า', + copy: 'คัดลอก', + detail: 'รายละเอียด', + status: 'สถานะ', + action: 'ดำเนินการ', + name: 'ชื่อ', + remark: 'หมายเหตุ', + date: 'วันที่', + amount: 'จำนวนเงิน', + total: 'รวม', + unit: 'หน่วย', + meter: 'เมตร', + currency: 'สกุลเงิน', + country: 'ประเทศ', + phone: 'โทรศัพท์', + email: 'อีเมล', + address: 'ที่อยู่', + position: 'ตำแหน่ง', + is: 'ใช่', + no: 'ไม่', + days: 'วัน', + tenThousand: 'หมื่น', + yuan: 'หยวน', + sheet: 'แผ่น', + item: 'รายการ', + photo: 'รูปภาพ', + person: 'คน', + today: 'วันนี้', + unknown: 'ไม่ทราบ', + none: 'ไม่มี', + all: 'ทั้งหมด', + retry: 'ลองใหม่', + inputPassword: 'กรุณากรอกรหัสผ่าน', + deleteConfirm: 'ยืนยันการลบ', + deleteWarning: 'การดำเนินการนี้ไม่สามารถเรียกคืนได้', + draftFound: 'พบร่างที่ยังไม่เสร็จสมบูรณ์', + draftRestore: 'ตรวจพบข้อมูลที่ยังไม่ได้ส่งครั้งก่อน ต้องการกู้คืนหรือไม่?', + restoreDraft: 'กู้คืนร่าง', + reFill: 'กรอกใหม่', + closeConfirm: 'ยืนยันการปิด', + closeConfirmMsg: 'ข้อมูลฟอร์มยังไม่ได้บันทึก หลังจากปิดสามารถกู้คืนผ่านร่างได้ ต้องการปิดหรือไม่?', + continueEdit: 'แก้ไขต่อ', + noData: 'ยังไม่มีข้อมูล', + loadingData: 'กำลังโหลดข้อมูลโครงการ...', + noProjectData: 'ยังไม่มีข้อมูลโครงการ', + operationFailed: 'การดำเนินการล้มเหลว', + saveFailed: 'บันทึกล้มเหลว', + deleteFailed: 'ลบล้มเหลว', + networkError: 'ข้อผิดพลาดเครือข่าย การดำเนินการล้มเหลว', + totalCount: 'ทั้งหมด {total} รายการ', + systemAdmin: 'ผู้ดูแลระบบ', + currentUser: 'ผู้ใช้ปัจจุบัน', + unnamed: 'ไม่ได้ตั้งชื่อ', + notSet: 'ไม่ได้ตั้งค่า', + pleaseSelect: 'กรุณาเลือก', + inputPlaceholder: 'กรุณากรอก', + selectPlaceholder: 'เลือก', + confirmDelete: 'ต้องการลบหรือไม่?', + confirmDeleteMsg: 'ต้องการลบหรือไม่?', + saveSuccess: 'บันทึกสำเร็จ', + createSuccess: 'สร้างสำเร็จ', + deleteSuccess: 'ลบสำเร็จ', + updateSuccess: 'อัปเดตสำเร็จ', + }, + + login: { + title: 'ชิงหยวนพาวเวอร์ สปป.ลาว ERP', + subtitle: 'แพลตฟอร์มการจัดการโครงการและการเบิกค่าใช้จ่ายทางการเงิน', + username: 'ชื่อผู้ใช้', + password: 'รหัสผ่าน', + loginButton: 'เข้าสู่ระบบ', + usernamePlaceholder: 'กรุณากรอกชื่อผู้ใช้', + passwordPlaceholder: 'กรุณากรอกรหัสผ่าน', + usernameRequired: 'กรุณากรอกชื่อผู้ใช้', + passwordRequired: 'กรุณากรอกรหัสผ่าน', + usernameMin: 'ชื่อผู้ใช้ต้องมีอย่างน้อย 3 ตัวอักษร', + passwordMin: 'รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร', + loginFailed: 'เข้าสู่ระบบล้มเหลว กรุณาลองใหม่อีกครั้ง', + testAccounts: 'บัญชีทดสอบ', + techSupport: 'ฝ่ายสนับสนุนเทคนิค: OpenClaw AI ผู้ช่วย + React + Node.js', + selectLanguage: 'เลือกภาษา', + passwordError: 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', + serverError: 'ข้อผิดพลาดภายในเซิร์ฟเวอร์ กรุณาลองใหม่ภายหลัง', + statusCodeError: 'เข้าสู่ระบบล้มเหลว (รหัสสถานะ: {code})', + }, + + menu: { + dashboard: 'แดชบอร์ด', + projects: 'การจัดการโครงการ', + budgetQuotation: 'งบประมาณและใบเสนอราคา', + construction: 'การจัดการงานก่อสร้าง', + constructionOverview: 'ภาพรวมงานก่อสร้าง', + approval: 'การจัดการอนุมัติ', + pendingApproval: 'รออนุมัติ', + pendingExecution: 'รอดำเนินการ', + financeDocs: 'คำขอทางการเงิน', + advanceApply: 'คำขอเบิกล่วงหน้า', + reimburseApply: 'คำขอเบิกค่าใช้จ่าย', + paymentApply: 'คำขอชำระเงิน', + verificationApply: 'คำขอตรวจสอบ', + financeManagement: 'การจัดการการเงิน', + financeOverview: 'ภาพรวมการเงิน', + exchangeRate: 'จัดการอัตราแลกเปลี่ยน', + projectCost: 'ต้นทุนโครงการ', + advanceVerificationStatus: 'สถานะการเบิกล่วงหน้า', + reports: 'รายงานวิเคราะห์', + procurement: 'การจัดการจัดซื้อ', + productManagement: 'จัดการสินค้า', + purchaseRequest: 'คำขอจัดซื้อ', + purchaseOrder: 'ใบสั่งซื้อ', + paymentPlan: 'แผนการชำระเงิน', + inventory: 'จัดการคลังสินค้า', + partners: 'พันธมิตร', + supplierManagement: 'จัดการซัพพลายเออร์', + subcontractorManagement: 'จัดการผู้รับเหมาช่วง', + customerManagement: 'จัดการลูกค้า', + logisticsManagement: 'จัดการโลจิสติกส์', + admin: 'จัดการระบบ', + userManagement: 'จัดการผู้ใช้', + rolePermission: 'สิทธิ์บทบาท', + processManagement: 'การจัดการกระบวนการ', + templateManagement: 'จัดการเทมเพลตโครงการ', + expenseCategory: 'จัดการหมวดหมู่การเงิน', + excelImport: 'นำเข้า Excel แบบกลุ่ม', + systemLogs: 'บันทึกระบบ', + dataBackup: 'สำรองข้อมูล', + aboutSystem: 'เกี่ยวกับระบบ', + profile: 'ข้อมูลส่วนตัว', + settings: 'ตั้งค่าระบบ', + logout: 'ออกจากระบบ', + backToFront: 'กลับหน้าหลัก', + collapse: 'ย่อเมนู', + }, + + user: { + profile: 'ข้อมูลส่วนตัว', + settings: 'ตั้งค่าระบบ', + logout: 'ออกจากระบบ', + admin: 'ผู้ดูแลระบบ', + finance: 'เจ้าหน้าที่การเงิน', + manager: 'ผู้จัดการโครงการ', + employee: 'พนักงาน', + userLabel: 'ผู้ใช้', + role: 'บทบาท', + managingProfile: 'จัดการข้อมูลบัญชีส่วนตัว', + name: 'ชื่อ-นามสกุล', + phone: 'เบอร์มือถือ', + email: 'อีเมล', + username: 'ชื่อผู้ใช้', + clickToChangeAvatar: 'คลิกเพื่อเปลี่ยนรูปโปรไฟล์', + idDocument: 'ข้อมูลเอกสารประจำตัว', + idDocTip: 'รูปพาสปอร์ตและใบขับขี่ คลิกที่รูปเพื่อดูขนาดใหญ่ คลิก "เปลี่ยน" เพื่ออัปโหลดรูปใหม่', + passport: 'พาสปอร์ต', + driverLicense: 'ใบขับขี่', + uploadPassport: 'คลิกเพื่ออัปโหลดรูปพาสปอร์ต', + uploadDriverLicense: 'คลิกเพื่ออัปโหลดรูปใบขับขี่', + deletePassportConfirm: 'ต้องการลบรูปพาสปอร์ตหรือไม่?', + deleteDriverLicenseConfirm: 'ต้องการลบรูปใบขับขี่หรือไม่?', + saveProfile: 'บันทึกการแก้ไข', + changePassword: 'เปลี่ยนรหัสผ่าน', + currentPassword: 'รหัสผ่านปัจจุบัน', + currentPasswordPlaceholder: 'กรุณากรอกรหัสผ่านปัจจุบัน', + newPassword: 'รหัสผ่านใหม่', + newPasswordPlaceholder: 'กรุณากรอกรหัสผ่านใหม่', + confirmPassword: 'ยืนยันรหัสผ่านใหม่', + confirmPasswordPlaceholder: 'กรุณายืนยันรหัสผ่านใหม่', + passwordMinLen: 'รหัสผ่านต้องมีความยาวอย่างน้อย 6 ตัวอักษร', + passwordMismatch: 'รหัสผ่านที่กรอกทั้งสองครั้งไม่ตรงกัน', + profileUpdated: 'อัปเดตข้อมูลส่วนตัวแล้ว', + passwordUpdated: 'เปลี่ยนรหัสผ่านแล้ว', + updateFailed: 'อัปเดตล้มเหลว', + updateRetry: 'อัปเดตล้มเหลว กรุณาลองใหม่อีกครั้ง', + passwordUpdateFailed: 'เปลี่ยนรหัสผ่านล้มเหลว', + passwordUpdateRetry: 'เปลี่ยนรหัสผ่านล้มเหลว กรุณาลองใหม่อีกครั้ง', + replace: 'เปลี่ยน', + uploadFailed: 'อัปโหลดล้มเหลว', + }, + + features: { + projectManage: 'การจัดการโครงการ: สร้าง ติดตาม วิเคราะห์ความคืบหน้าโครงการ', + advanceManage: 'การจัดการเงินเบิกล่วงหน้า: กระบวนการขอเบิกล่วงหน้าและการอนุมัติของพนักงาน', + reimburseManage: 'การจัดการค่าใช้จ่าย: กระบวนการเบิกค่าใช้จ่ายและการตรวจสอบ', + financeReport: 'รายงานการเงิน: การวิเคราะห์ต้นทุนและกำไรโครงการ', + mobileSupport: 'รองรับอุปกรณ์มือถือ: เทคโนโลยี PWA สามารถเพิ่มลงหน้าจอหลักได้', + }, + + dashboard: { + title: '📊 แดชบอร์ด', + planning: 'กำลังวางแผน', + inProgress: 'กำลังดำเนินการ', + completed: 'เสร็จสมบูรณ์', + projectName: 'ชื่อโครงการ', + budget: 'งบประมาณ', + spent: 'ใช้จ่ายไปแล้ว', + project: 'โครงการ', + budgetSpent: 'งบประมาณ/ใช้จ่าย', + budgetLabel: 'งบประมาณ: ', + spentLabel: 'ใช้จ่ายแล้ว: ', + inProgressProjects: 'โครงการที่กำลังดำเนินการ', + monthlyReimburse: 'การเบิกค่าใช้จ่ายเดือนนี้', + pendingApproval: 'รออนุมัติ', + teamMembers: 'สมาชิกในทีม', + recentProjects: 'โครงการล่าสุด', + }, + + project: { + title: 'การจัดการโครงการ', + description: 'จัดการข้อมูลโครงการ ความคืบหน้า และงบประมาณ', + list: 'รายการโครงการ', + quickCreate: 'สร้างโครงการอย่างรวดเร็ว', + newProject: 'สร้างโครงการใหม่', + editProject: 'แก้ไขโครงการ', + deleteProject: 'ลบโครงการ', + deleteConfirm: 'ยืนยันการลบ', + deleteConfirmMsg: 'ต้องการลบโครงการนี้หรือไม่? การดำเนินการนี้ไม่สามารถเรียกคืนได้', + deletePassMsg: 'กรุณากรอกรหัสผ่านผู้ดูแลระบบเพื่อยืนยันการลบ:', + projectName: 'ชื่อโครงการ', + projectNamePlaceholder: 'เช่น: โครงการสายส่ง 22kV อำเภอไซทานี แขวงเวียงจันทน์', + projectTemplate: 'เทมเพลตโครงการ', + selectTemplate: 'เลือกเทมเพลตโครงการ (ไม่บังคับ)', + projectManager: 'ผู้จัดการโครงการ', + budget: 'งบประมาณ', + progress: 'ความคืบหน้า', + status: 'สถานะ', + planning: 'กำลังวางแผน', + inProgress: 'กำลังดำเนินการ', + completed: 'เสร็จสมบูรณ์', + paused: 'ระงับ', + plan: 'วางแผน', + complete: 'เสร็จสมบูรณ์', + pause: 'ระงับ', + customer: 'ลูกค้า', + selectCustomer: 'เลือกลูกค้า', + selectManager: 'เลือกผู้จัดการโครงการ', + contractAmount: 'จำนวนเงินตามสัญญา', + projectStatus: 'สถานะโครงการ', + completedHistory: 'เสร็จสมบูรณ์ (บันทึกโครงการย้อนหลัง)', + startDate: 'วันที่เริ่ม', + endDate: 'วันที่สิ้นสุด', + location: 'สถานที่โครงการ', + locationPlaceholder: 'เช่น: แขวงเวียงจันทน์ สปป.ลาว', + descriptionPlaceholder: 'อธิบายเนื้อหาโครงการโดยย่อ', + createSuccess: 'สร้างโครงการสำเร็จ', + createFailed: 'สร้างล้มเหลว', + deleteSuccess: 'ลบโครงการสำเร็จ', + getListFailed: 'ดึงรายการโครงการล้มเหลว', + unassigned: 'ยังไม่ได้มอบหมาย', + passwordError: 'รหัสผ่านไม่ถูกต้อง', + projectCode: 'รหัสโครงการ', + basicInfo: 'ข้อมูลพื้นฐาน', + contractDetails: 'สัญญาและการรับชำระเงิน', + financeDetails: 'รายรับรายจ่ายทางการเงิน', + editBasicInfo: 'แก้ไขข้อมูลพื้นฐานโครงการ', + basicInfoSaved: 'บันทึกข้อมูลพื้นฐานสำเร็จ', + selectStartDate: 'กรุณาเลือกวันที่เริ่มงาน', + selectEndDate: 'กรุณาเลือกวันที่เสร็จงาน', + durationDays: 'จำนวนวันดำเนินการ', + durationDaysPlaceholder: 'กรุณากรอกจำนวนวันดำเนินการ', + overview: 'ภาพรวมโครงการ', + overviewPlaceholder: 'กรุณากรอกภาพรวมโครงการ', + createdAt: 'เวลาที่สร้าง', + returnToList: 'กลับสู่รายการ', + unknownManager: 'ไม่ทราบผู้จัดการ', + notFound: 'ไม่พบโครงการหรือถูกลบไปแล้ว', + getInfoFailed: 'ดึงข้อมูลโครงการล้มเหลว', + enterConstruction: 'เข้าสู่การจัดการงานก่อสร้าง', + contractNo: 'เลขที่สัญญา', + contractType: 'ประเภทสัญญา', + unitPriceContract: 'สัญญาราคาต่อหน่วย', + includeTax: 'รวมภาษีหรือไม่', + settlementType: 'วิธีการชำระเงิน', + lumpSum: 'ราคาเหมารวม', + unitPrice: 'ชำระตามหน่วย', + contractTotal: 'มูลค่าสัญญาทั้งหมด', + contractTotalPlaceholder: 'กรุณากรอกมูลค่าสัญญาทั้งหมด', + contractTax: 'สัญญารวมภาษีหรือไม่', + paymentMilestones: 'จุดชำระเงิน', + milestoneName: 'ชื่อจุดชำระเงิน', + milestoneCondition: 'เงื่อนไขจุดชำระเงิน', + milestoneRatio: 'สัดส่วน(%)', + milestoneAmount: 'จำนวนเงิน', + milestoneStatus: 'สถานะความคืบหน้า', + pendingMilestone: 'รอเริ่ม', + noMilestone: 'ยังไม่มีจุดชำระเงิน', + noMilestoneRecord: 'ยังไม่มีบันทึกจุดชำระเงิน', + addMilestone: 'เพิ่มจุดชำระเงิน', + milestoneNotReached: 'ยังไม่ถึงจุดชำระเงิน', + contractAttachment: 'ไฟล์แนบสัญญา', + contractFile: 'ไฟล์สัญญา', + viewContract: 'ดูไฟล์สัญญา', + noContractAttachment: 'ยังไม่มีไฟล์แนบสัญญา', + otherContractInfo: 'ข้อมูลสัญญาอื่นๆ', + otherInfo: 'ข้อมูลอื่นๆ', + otherInfoPlaceholder: 'กรุณากรอกข้อมูลอื่นๆ เกี่ยวกับสัญญา', + warranty: 'การตั้งค่าเงินประกัน', + hasWarranty: 'มีเงินประกันหรือไม่', + warrantyRatio: 'สัดส่วนเงินประกัน', + warrantyAmount: 'จำนวนเงินประกัน', + warrantyPeriod: 'ระยะเวลาประกัน', + warrantyExpiry: 'วันที่หมดอายุ', + warrantyStatus: 'สถานะเงินประกัน', + warrantyReleased: 'คืนแล้ว', + warrantyPending: 'รอคืน', + contractSaveSuccess: 'บันทึกรายละเอียดสัญญาสำเร็จ', + draft: 'บันทึกร่าง', + replaceFile: 'เปลี่ยนไฟล์', + clickUpload: 'คลิกเพื่ออัปโหลด', + fileUploadFailed: 'อัปโหลดไฟล์ล้มเหลว', + subcontract: 'การจัดการผู้รับเหมาช่วง', + addSubcontract: 'เพิ่มผู้รับเหมาช่วงใหม่', + subcontractor: 'ผู้รับเหมาช่วง', + subcontractorName: 'ชื่อผู้รับเหมาช่วง', + subcontractorNamePlaceholder: 'กรุณากรอกชื่อผู้รับเหมาช่วง', + paidAmount: 'ชำระแล้ว', + noSubcontract: 'ยังไม่มีบันทึกผู้รับเหมาช่วง', + subcontractDetail: 'รายละเอียดผู้รับเหมาช่วง', + startDateRequired: 'กรุณาเลือกวันที่เริ่ม', + endDateRequired: 'กรุณาเลือกวันที่สิ้นสุด', + otherTerms: 'ข้อตกลงอื่นๆ', + otherTermsPlaceholder: 'กรุณากรอกข้อตกลงอื่นๆ', + paymentNote: 'คำอธิบายการชำระเงิน', + paymentNotePlaceholder: 'กรุณากรอกคำอธิบายการชำระเงิน', + addSubSuccess: 'เพิ่มผู้รับเหมาช่วงสำเร็จ', + addSubFailed: 'เพิ่มผู้รับเหมาช่วงล้มเหลว กรุณาตรวจสอบข้อมูลฟอร์ม', + projectItems: 'รายการราคาต่อหน่วยโครงการ', + quantity: 'จำนวน', + unitPriceLabel: 'ราคาต่อหน่วย', + totalPrice: 'ราคารวม', + addItem: '+ เพิ่มรายการโครงการ', + noItems: 'ยังไม่มีรายการโครงการ', + material: 'การจัดการวัสดุ', + materialName: 'ชื่อวัสดุ', + budgetQty: 'จำนวนงบประมาณ', + purchaseQty: 'จำนวนจัดซื้อ', + usedQty: 'จำนวนใช้แล้ว', + avgPrice: 'ราคาเฉลี่ย', + noMaterial: 'ยังไม่มีบันทึกวัสดุ', + constructionNode: 'จุดงานก่อสร้าง', + contractMilestone: 'จุดชำระเงินตามสัญญา', + milestoneDesc: 'สถานะความสำเร็จของจุดสำคัญ', + plannedDate: 'วันที่ตามแผน', + actualDate: 'วันที่จริง', + uploadProof: 'อัปโหลดหลักฐาน', + noRecord: 'ยังไม่มีบันทึก', + constructionLog: 'บันทึกงานก่อสร้าง', + addLog: 'เพิ่มบันทึกใหม่', + weather: 'สภาพอากาศ', + recorder: 'ผู้บันทึก', + todayWork: 'งานวันนี้', + viewPhoto: 'ดูรูป', + noLog: 'ยังไม่มีบันทึก', + finance: 'ข้อมูลทางการเงิน', + received: 'รับชำระแล้ว', + totalExpense: 'รายจ่ายรวม', + grossProfit: 'กำไรขั้นต้น', + marginRate: 'อัตรากำไรขั้นต้น', + addReceipt: 'เพิ่มรายรับ', + receiptRecords: 'บันทึกรายรับ', + noReceipts: 'ยังไม่มีรายรับ', + receiptType: 'ประเภทรายรับ', + receiptTypeNode: 'รับตามขั้นตอน', + receiptTypeAdvance: 'เงินล่วงหน้าจากลูกค้า', + receiptTypeOther: 'รายรับอื่นๆ', + receiptDate: 'วันที่รับ', + receiptNode: 'ขั้นตอนที่เกี่ยวข้อง', + receiptAmount: 'จำนวนเงิน', + receiptAmountCNY: 'จำนวนเงิน (CNY)', + receiptDesc: 'รายละเอียด', + receiptDescPlaceholder: 'กรอกรายละเอียด', + receiptAdded: 'เพิ่มรายรับสำเร็จ', + selectMilestone: 'ขั้นตอนการชำระเงิน', + selectMilestonePlaceholder: 'เลือกขั้นตอนการชำระเงิน', + selectMilestoneRequired: 'กรุณาเลือกขั้นตอนการชำระเงิน', + payer: 'ผู้ชำระ', + payerPlaceholder: 'กรอกชื่อผู้ชำระ', + exchangeRate: 'อัตราแลกเปลี่ยน', + voucher: 'ใบเสร็จ', + uploadVoucher: 'อัพโหลดใบเสร็จ', + viewVoucher: 'ดู', + expenseBreakdown: 'รายละเอียดประเภทรายจ่าย', + category: 'หมวดหมู่', + categoryAmount: 'จำนวนเงิน(¥)', + count: 'จำนวนครั้ง', + ratio: 'สัดส่วน', + personnelExpense: 'รายละเอียดรายจ่ายบุคลากร', + personnel: 'บุคลากร', + warrantyManagement: 'การจัดการเงินประกัน', + warrantyStartDate: 'วันที่เริ่มนับ', + markReleased: 'ทำเครื่องหมายคืนแล้ว', + extendWarranty: 'ขยายเวลา', + currentLabel: 'ปัจจุบัน: ', + progressLabel: 'ความคืบหน้า: ', + warrantyLabel: 'เงินประกัน: ', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + amountRequired: 'กรุณากรอกจำนวนเงินตามสัญญา', + settlementRequired: 'กรุณาเลือกวิธีการชำระเงิน', + nodeNameRequired: 'กรุณากรอกชื่อจุดชำระเงิน', + nodeConditionRequired: 'กรุณากรอกเงื่อนไขจุดชำระเงิน', + ratioRequired: 'กรุณากรอกสัดส่วน', + milestoneAmountRequired: 'กรุณากรอกจำนวนเงิน', + statusRequired: 'กรุณาเลือกสถานะ', + }, + + construction: { + overview: 'ภาพรวมงานก่อสร้าง', + noProjects: 'ยังไม่มีโครงการก่อสร้าง', + underConstruction: 'กำลังก่อสร้าง', + pendingStart: 'รอเริ่มงาน', + completed: 'ก่อสร้างเสร็จแล้ว', + paused: 'ระงับ', + currentPhase: 'ขั้นตอนปัจจุบัน: ', + completedProjects: 'โครงการที่เสร็จแล้ว: ', + enter: 'เข้า', + getListFailed: 'ดึงรายการโครงการล้มเหลว', + progress: 'ความคืบหน้างานก่อสร้าง', + todayLog: 'บันทึกวันนี้: ', + todayLogEmpty: 'บันทึกวันนี้: ยังไม่ได้กรอก', + writeLog: 'เขียนบันทึกวันนี้', + constructionLog: 'บันทึกงานก่อสร้าง', + uploadPhoto: 'อัปโหลดรูปภาพ', + milestoneProgress: 'ความคืบหน้าจุดชำระเงิน', + management: 'การจัดการงานก่อสร้าง', + description: 'ดูและจัดการโครงการก่อสร้างของคุณ', + noConstructionProjects: 'ยังไม่มีโครงการก่อสร้าง', + contactAdmin: 'กรุณาติดต่อผู้ดูแลระบบเพื่อมอบหมายโครงการก่อสร้างให้คุณ', + myProjects: 'โครงการก่อสร้างของฉัน', + customerLabel: 'ลูกค้า: ', + getInfoFailed: 'ดึงข้อมูลโครงการล้มเหลว', + getPhaseFailed: 'ดึงข้อมูลขั้นตอนล้มเหลว', + phaseComplete: 'ขั้นตอนเสร็จสมบูรณ์! ความคืบหน้า: ', + advancedTo: 'ได้เลื่อนไปยัง: ', + reopenPhase: 'เปิดขั้นตอนใหม่', + reopenConfirm: 'ต้องการเปิดขั้นตอนนี้อีกครั้งหรือไม่? ความคืบหน้าโครงการจะถูกย้อนกลับ', + phaseReopened: 'ขั้นตอนได้ถูกเปิดอีกครั้งแล้ว', + updateItemFailed: 'อัปเดตรายการย่อยล้มเหลว', + returnOverview: 'กลับสู่ภาพรวม', + currentLabel: 'ปัจจุบัน: ', + parallelPhase: 'ขั้นตอนคู่ขนาน (สามารถดำเนินการพร้อมกับขั้นตอนอื่นได้)', + completionStandard: 'มาตรฐานการเสร็จสมบูรณ์: ', + remarkOptional: 'หมายเหตุ (ไม่บังคับ): ', + remarkPlaceholder: 'กรอกหมายเหตุการเสร็จสมบูรณ์...', + confirmCompleteMsg: 'ยืนยันการเสร็จสมบูรณ์ของขั้นตอนนี้', + subsequentPhases: 'ขั้นตอนถัดไป', + parallelLabel: 'คู่ขนาน', + phaseHistory: 'ประวัติขั้นตอน', + rollback: 'ย้อนกลับ', + projectCompleted: 'โครงการเสร็จสมบูรณ์แล้ว', + projectCompletedDesc: 'โครงการนี้เสร็จสมบูรณ์แล้ว ไม่จำเป็นต้องเลื่อนขั้นตอนงานก่อสร้าง', + viewProjectDetail: 'ดูรายละเอียดโครงการ', + notInitialized: 'โครงการนี้ยังไม่ได้เริ่มต้นขั้นตอนงานก่อสร้าง', + notInitializedDesc: 'กรุณาเลือกเทมเพลตโครงการในรายละเอียดโครงการเพื่อเริ่มต้นขั้นตอนงานก่อสร้าง', + goToProjectDetail: 'ไปยังรายละเอียดโครงการ', + confirmCompleteTitle: 'ยืนยันการเสร็จสมบูรณ์ของขั้นตอน', + confirmCompleteDesc: 'ยืนยันว่าขั้นตอนนี้เสร็จสมบูรณ์แล้ว? ระบบจะเลื่อนไปยังขั้นตอนถัดไปโดยอัตโนมัติ', + remarkLabel: 'หมายเหตุ: ', + uploadProofLabel: 'อัปโหลดเอกสารหลักฐาน (รูปภาพ/ไฟล์): ', + selectFile: 'เลือกไฟล์', + supportFormats: 'รองรับรูปภาพ, PDF, Word, Excel และไฟล์รูปแบบอื่นๆ', + sunny: 'แจ่มใส', + cloudy: 'มีเมฆมาก', + rain: 'ฝนตก', + thunderstorm: 'พายุฝนฟ้าคะนอง', + windy: 'ลมแรง', + getLogFailed: 'ดึงรายการบันทึกล้มเหลว', + logAddSuccess: 'เพิ่มบันทึกสำเร็จ', + logAddFailed: 'เพิ่มบันทึกล้มเหลว', + logDeleteSuccess: 'ลบบันทึกสำเร็จ', + logDeleteFailed: 'ลบบันทึกล้มเหลว', + yearMonth: 'เดือน YYYY', + monthDay: 'วันที่ DD', + recorderLabel: 'ผู้บันทึก: ', + deleteLogConfirmTitle: 'ต้องการลบบันทึกนี้หรือไม่?', + deleteLogConfirmDesc: 'ไม่สามารถกู้คืนได้หลังจากลบ', + todayWorkLabel: 'งานวันนี้:', + tomorrowPlanLabel: 'แผนวันพรุ่งนี้:', + issueRecordLabel: 'บันทึกปัญหา:', + constructionPhoto: 'รูปภาพงานก่อสร้าง', + noLog: 'ยังไม่มีบันทึกงานก่อสร้าง', + addFirstLog: 'เพิ่มบันทึกแรก', + newLog: 'เพิ่มบันทึกใหม่', + newConstructionLog: 'เพิ่มบันทึกงานก่อสร้างใหม่', + selectDate: 'กรุณาเลือกวันที่', + selectWeather: 'กรุณาเลือกสภาพอากาศ', + inputTodayWork: 'กรุณากรอกเนื้อหางานวันนี้', + todayWorkPlaceholder: 'อธิบายงานก่อสร้างที่เสร็จในวันนี้...', + tomorrowPlanPlaceholder: 'แผนงานวันพรุ่งนี้...', + issuePlaceholder: 'ปัญหาที่พบหรือเรื่องที่ต้องประสานงาน...', + addPhoto: 'เพิ่มรูปภาพ', + multiPhotoSupport: 'รองรับการอัปโหลดหลายรูป สูงสุด 9 รูป', + plannedComplete: 'แผนเสร็จสมบูรณ์: ', + overallProgress: 'ความคืบหน้าโดยรวม', + totalNodes: 'จำนวนจุดทั้งหมด', + noMilestones: 'ยังไม่มีจุดงานก่อสร้าง', + milestoneConfigured: 'จุดงานถูกกำหนดค่าโดยผู้จัดการโครงการในการตั้งค่าโครงการ', + inProgress: 'กำลังดำเนินการ', + cancelled: 'ยกเลิกแล้ว', + progressTab: 'ความคืบหน้า', + documentsTab: 'เอกสาร', + logsTab: 'บันทึกก่อสร้าง', + markComplete: 'ทำเครื่องหมายเสร็จ', + phasesCompleted: 'ขั้นตอนเสร็จสิ้น', + initPhases: 'เริ่มต้นขั้นตอน', + selectTemplateInit: 'เลือกเทมเพลตและเริ่มต้น', + selectTemplate: 'กรุณาเลือกเทมเพลต', + initSuccess: 'เริ่มต้นขั้นตอนสำเร็จ', + completedAt: 'เสร็จสิ้นเมื่อ', + completionTime: 'เวลาที่เสร็จสิ้น', + proofPhotos: 'รูปภาพหลักฐาน', + optional: 'ไม่บังคับ', + uploadImage: 'อัพโหลดรูปภาพ', + uploadDocument: 'อัพโหลดเอกสาร', + imageDocs: 'รูปภาพ', + fileDocs: 'เอกสาร', + noImages: 'ยังไม่มีรูปภาพ', + noDocuments: 'ยังไม่มีเอกสาร', + fileName: 'ชื่อไฟล์', + uploader: 'ผู้อัพโหลด', + uploadTime: 'เวลาอัพโหลด', + descriptionPlaceholder: 'กรอกคำอธิบาย', + clickUpload: 'คลิกเพื่ออัพโหลด', + uploadSuccess: 'อัพโหลดสำเร็จ', + addLog: 'เพิ่มบันทึก', + logAdded: 'เพิ่มบันทึกสำเร็จ', + logDate: 'วันที่บันทึก', + weather: 'สภาพอากาศ', + weatherSunny: 'แดดออก', + weatherCloudy: 'เมฆมาก', + weatherRainy: 'ฝนตก', + weatherStormy: 'พายุ', + weatherWindy: 'ลมแรง', + workContent: 'เนื้อหางาน', + workContentPlaceholder: 'กรอกเนื้อหางานวันนี้', + nextPlan: 'แผนวันพรุ่งนี้', + nextPlanPlaceholder: 'กรอกแผนงานวันพรุ่งนี้', + issues: 'ปัญหา', + issuesPlaceholder: 'กรอกปัญหาที่พบ', + sitePhotos: 'รูปถ่ายสถานที่', + noLogs: 'ยังไม่มีบันทึกก่อสร้าง', + customer: 'ลูกค้า', + manager: 'ผู้จัดการโครงการ', + }, + + budget: { + title: 'จัดการงบประมาณและใบเสนอราคา', + description: 'จัดการโครงการที่อยู่ระหว่างการเจรจาและเวอร์ชันใบเสนอราคา', + newProject: 'สร้างโครงการเจรจาใหม่', + statusFilter: 'กรองตามสถานะ:', + inNegotiation: 'กำลังเจรจา', + signed: 'ลงนามแล้ว', + unsigned: 'ยังไม่ลงนาม', + draft: 'ร่าง', + sent: 'ส่งแล้ว', + approved: 'อนุมัติแล้ว', + rejected: 'ปฏิเสธแล้ว', + deleteConfirm: 'ยืนยันการลบ', + deleteConfirmMsg: 'ต้องการลบโครงการงบประมาณนี้หรือไม่? การดำเนินการนี้ไม่สามารถเรียกคืนได้', + deletePassMsg: 'กรุณากรอกรหัสผ่านผู้ดูแลระบบเพื่อยืนยันการลบ:', + deletePass: 'กรุณากรอกรหัสผ่านผู้ดูแลระบบ', + getDataFailed: 'ดึงข้อมูลล้มเหลว', + deleteSuccess: 'ลบสำเร็จ', + passwordError: 'รหัสผ่านไม่ถูกต้อง', + customerLabel: 'ลูกค้า: ', + managerLabel: 'ผู้จัดการฝ่ายธุรกิจ: ', + intermediaryLabel: 'คนกลาง: ', + intermediaryFee: 'ค่าธรรมเนียมคนกลาง: ', + versionDeleteConfirm: 'ต้องการลบเวอร์ชันใบเสนอราคานี้หรือไม่? การดำเนินการนี้ไม่สามารถเรียกคืนได้', + createTitle: 'สร้างโครงการเจรจาใหม่', + createDesc: 'สร้างโครงการเจรจาใหม่ เพิ่มข้อมูลพื้นฐานโครงการ', + basicInfo: 'ข้อมูลพื้นฐาน', + projectName: 'ชื่อโครงการ', + projectNamePlaceholder: 'กรุณากรอกชื่อโครงการ', + customer: 'ลูกค้า', + selectCustomer: 'กรุณาเลือกลูกค้า', + businessManager: 'ผู้จัดการฝ่ายธุรกิจ', + selectManager: 'กรุณาเลือกผู้จัดการฝ่ายธุรกิจ', + unknownDept: 'ไม่ทราบแผนก', + projectLocation: 'สถานที่โครงการ', + locationPlaceholder: 'กรุณากรอกสถานที่โครงการ', + surveyDate: 'วันที่สำรวจ', + intermediary: 'ข้อมูลคนกลาง', + intermediaryName: 'ชื่อคนกลาง', + intermediaryNamePlaceholder: 'กรุณากรอกชื่อคนกลาง', + intermediaryType: 'ประเภทค่าธรรมเนียมคนกลาง', + fixedAmount: 'จำนวนเงินคงที่', + percentage: 'เปอร์เซ็นต์', + intermediaryRatio: 'สัดส่วนค่าธรรมเนียมคนกลาง(%)', + intermediaryRatioPlaceholder: 'กรอกสัดส่วน เช่น: 5', + intermediaryAmount: 'จำนวนเงินค่าธรรมเนียมคนกลาง', + intermediaryAmountPlaceholder: 'กรอกจำนวนเงิน', + projectDetail: 'รายละเอียดโครงการ', + customerRequirement: 'ความต้องการของลูกค้า', + requirementPlaceholder: 'กรุณากรอกความต้องการเฉพาะของลูกค้า', + overview: 'ภาพรวมโครงการ', + overviewPlaceholder: 'กรุณากรอกคำอธิบายภาพรวมโครงการ', + attachment: 'ไฟล์แนบ', + attachmentUpload: 'อัปโหลดไฟล์แนบ', + surveyPhoto: 'รูปภาพสำรวจ', + noAccess: 'คุณไม่มีสิทธิ์เข้าถึงหน้านี้', + createSuccess: 'สร้างสำเร็จ', + createFailed: 'สร้างล้มเหลว', + leaveConfirm: 'ยืนยันการออก', + leaveConfirmMsg: 'ข้อมูลฟอร์มยังไม่ได้บันทึก หลังจากออกสามารถกู้คืนผ่านร่างได้ ต้องการออกหรือไม่?', + leave: 'ออก', + continueEdit: 'แก้ไขต่อ', + return: 'กลับ', + signedSuccess: 'ทำเครื่องหมายเป็นยังไม่ลงนามสำเร็จ', + notFound: 'ไม่พบโครงการ', + quotationVersions: 'เวอร์ชันใบเสนอราคา', + addVersion: 'เพิ่มเวอร์ชันใบเสนอราคาใหม่', + versionDate: 'วันที่เสนอราคา: ', + versionAmount: 'จำนวนเงินเสนอราคา: ', + versionRemark: 'หมายเหตุ: ', + noVersion: 'ยังไม่มีเวอร์ชันใบเสนอราคา', + markSigned: 'ทำเครื่องหมายลงนาม', + markUnsigned: 'ทำเครื่องหมายยังไม่ลงนาม', + enterProject: 'เข้าสู่การจัดการโครงการ', + deleteProject: 'ลบโครงการ', + detailTitle: 'รายละเอียดโครงการงบประมาณ', + detailDesc: 'ดูข้อมูลรายละเอียดโครงการและเวอร์ชันใบเสนอราคา', + projectInfo: 'ข้อมูลโครงการ', + photos: 'รูปภาพ ', + noPhotos: 'ยังไม่มีรูปภาพสำรวจ', + noAttachment: 'ยังไม่มีไฟล์แนบ', + quickSign: 'ลงนามอย่างรวดเร็ว', + quickSignSuccess: 'ลงนามสำเร็จ โครงการถูกสร้างโดยอัตโนมัติ', + contractNo: 'เลขที่สัญญา', + contractNoPlaceholder: 'กรุณากรอกเลขที่สัญญา', + contractType: 'วิธีการรับเหมา', + selectContractType: 'กรุณาเลือกวิธีการรับเหมา', + totalPrice: 'ราคารวม', + totalPricePlaceholder: 'กรุณากรอกราคารวม', + durationDays: 'ระยะเวลาดำเนินการ (วัน)', + durationPlaceholder: 'กรุณากรอกระยะเวลาดำเนินการ', + durationDaysPlaceholder: 'กรุณากรอกระยะเวลาดำเนินการ (วัน)', + quickSignNote: 'หมายเหตุ: เป็นกระบวนการลงนามอย่างรวดเร็ว กรอกเฉพาะข้อมูลพื้นฐาน สามารถเพิ่มรายละเอียดสัญญาในการจัดการโครงการได้', + newQuotation: 'เพิ่มเวอร์ชันใบเสนอราคาใหม่', + quotationDate: 'วันที่เสนอราคา', + selectDate: 'กรุณาเลือกวันที่เสนอราคา', + quotationAmount: 'จำนวนเงินเสนอราคา', + amountPlaceholder: 'กรุณากรอกจำนวนเงินเสนอราคา', + quotationFile: 'ไฟล์ใบเสนอราคา', + viewFile: 'ดูไฟล์', + uploadFile: 'อัปโหลดไฟล์', + remarkPlaceholder: 'กรุณากรอกหมายเหตุ', + uploadSuccess: 'อัปโหลดสำเร็จ', + version: 'เวอร์ชันปัจจุบัน: ', + projectLabel: 'ชื่อโครงการ: ', + }, + + finance: { + title: 'การจัดการการเงิน', + addRecord: 'เพิ่มบันทึกทางการเงินใหม่', + addRecordBtn: 'เพิ่มบันทึก', + exportExcel: 'ส่งออก Excel', + totalIncome: 'รายรับรวม', + totalExpense: 'รายจ่ายรวม', + netProfit: 'กำไรสุทธิ', + expenseSummary: 'สรุปรายจ่ายตามหมวดหมู่', + detail: 'รายละเอียดทางการเงิน', + filterType: 'ประเภทตัวกรอง', + date: 'วันที่', + incomeType: 'ประเภทรายรับรายจ่าย', + level1Category: 'หมวดหมู่ระดับ 1', + level2Category: 'หมวดหมู่ระดับ 2', + projectName: 'ชื่อโครงการ', + amount: 'จำนวนเงิน', + currency: 'สกุลเงิน', + exchangeRate: 'อัตราแลกเปลี่ยน', + equivalentCNY: 'เทียบเท่าหยวน', + counterpartyName: 'ชื่อคู่สัญญา', + counterpartyType: 'ประเภทคู่สัญญา', + personName: 'ชื่อบุคคล', + desc: 'คำอธิบาย', + voucherNo: 'เลขที่ใบสำคัญ', + selectDate: 'กรุณาเลือกวันที่', + selectIncomeType: 'กรุณาเลือก', + selectLevel1: 'กรุณาเลือก', + selectLevel2: 'กรุณาเลือกหมวดหมู่ระดับ 1 ก่อน', + selectProject: 'เลือกโครงการ', + amountPlaceholder: '0', + counterpartyPlaceholder: 'ชื่อผู้รับ/ผู้จ่าย', + selectType: 'เลือกประเภท', + personNamePlaceholder: 'ชื่อพนักงานที่เกี่ยวข้อง', + descPlaceholder: 'คำอธิบายเพิ่มเติม', + voucherPlaceholder: 'เลขที่ใบเสร็จ/ใบแจ้งหนี้', + income: 'รายรับ', + expense: 'รายจ่าย', + projectExpense: 'รายจ่ายโครงการ', + companyExpense: 'รายจ่ายบริษัท', + incomeCategory: 'รายรับ', + manual: 'ด้วยตนเอง', + advance: 'เบิกล่วงหน้า', + reimbursement: 'เบิกค่าใช้จ่าย', + payment: 'ชำระเงิน', + material: 'วัสดุ', + freight: 'ค่าขนส่ง', + source: 'แหล่งที่มา', + recordSuccess: 'บันทึกสำเร็จ', + exporting: 'กำลังส่งออก...', + exportSuccess: 'ส่งออกสำเร็จ', + exportFailed: 'ส่งออกล้มเหลว', + sheetName: 'บัญชีการเงิน', + totalRecords: 'ทั้งหมด {total} รายการ', + currencyCNY: 'CNY (หยวน)', + currencyLAK: 'LAK (กีบลาว)', + currencyUSD: 'USD (ดอลลาร์)', + currencyTHB: 'THB (บาท)', + counterpartySupplier: 'ซัพพลายเออร์', + counterpartySubcontractor: 'ผู้รับเหมาช่วง', + counterpartyCustomer: 'ลูกค้า', + counterpartyEmployee: 'พนักงาน', + counterpartyLogistics: 'บริษัทโลจิสติกส์', + counterpartyShareholder: 'ผู้ถือหุ้น', + counterpartyOther: 'อื่นๆ', + projectRevenue: 'รายรับจากสัญญาโครงการ', + warrantyReturn: 'คืนเงินประกัน', + shareholderInvestment: 'เงินลงทุนจากผู้ถือหุ้น', + otherIncome: 'รายรับอื่นๆ', + materialPurchase: 'จัดซื้อวัสดุ', + equipmentPurchase: 'จัดซื้ออุปกรณ์', + constructionSubcontract: 'ผู้รับเหมาช่วงงานก่อสร้าง', + laborWage: 'ค่าแรงงาน', + travelTransport: 'ค่าเดินทาง', + accommodationFood: 'ค่าอาหารและที่พัก', + transportLogistics: 'ค่าขนส่งโลจิสติกส์', + surveyDesign: 'สำรวจและออกแบบ', + smallTools: 'เครื่องมือขนาดเล็ก', + customerEDLRelation: 'ความสัมพันธ์ลูกค้า/EDL', + otherProjectExpense: 'รายจ่ายโครงการอื่นๆ', + salaryWelfare: 'เงินเดือนและสวัสดิการ', + rentProperty: 'ค่าเช่าและค่าสถานที่', + officeExpense: 'ค่าใช้จ่ายสำนักงาน', + commute: 'ค่าเดินทาง', + vehicleMaintenance: 'บำรุงรักษายานพาหนะ', + fixedAsset: 'สินทรัพย์ถาวร', + marketing: 'การตลาด', + entertainment: 'ค่าต้อนรับ', + employeeBenefit: 'สวัสดิการพนักงาน', + expressLogistics: 'ค่าขนส่งพัสดุ', + otherCompanyExpense: 'รายจ่ายบริษัทอื่นๆ', + }, + + cash: { + tabOverview: 'ภาพรวม', + tabIncome: 'บันทึกรายรับ', + tabExpense: 'บันทึกรายจ่าย', + addIncome: 'เพิ่มรายรับ', + addExpense: 'เพิ่มรายจ่าย', + financeExpense: 'รายจ่ายทางการเงิน', + customerAdvance: 'เงินล่วงหน้า/กู้ยืมจากลูกค้า', + bankLoan: 'เงินกู้ธนาคาร', + otherLoan: 'เงินกู้อื่นๆ', + dividendIncome: 'เงินปันผล', + interestIncome: 'ดอกเบี้ยรับ', + assetDisposal: 'จำหน่ายสินทรัพย์', + taxRefund: 'คืนภาษี', + governmentSubsidy: 'เงินอุดหนุนรัฐบาล', + loanRepayment: 'ชำระคืนเงินกู้', + interestExpense: 'ดอกเบี้ยจ่าย', + dividendPayment: 'จ่ายเงินปันผล', + taxPayment: 'ชำระภาษี', + depositPayment: 'มัดจำ/ค้ำประกัน', + ownerExpense: 'รายจ่ายเจ้าของ', + otherFinance: 'รายจ่ายทางการเงินอื่นๆ', + sourceLabel: 'การจัดการเงินสด', + receiptSource: 'รับเงินโครงการ', + counterpartyBank: 'ธนาคาร', + counterpartySelect: 'เลือกคู่สัญญา', + counterpartySelectPlaceholder: 'เลือกคู่สัญญา', + voucherUpload: 'อัปโหลดใบเสร็จ', + uploadVoucher: 'อัปโหลดใบเสร็จ', + uploadSuccess: 'อัปโหลดสำเร็จ', + uploadFailed: 'อัปโหลดล้มเหลว', + }, + + reports: { + title: 'รายงานสถิติ', + description: 'ดูรายงานการเงินโครงการและข้อมูลวิเคราะห์ทางสถิติ', + totalIncome: 'รายรับรวม', + totalExpense: 'รายจ่ายรวม', + netProfit: 'กำไรสุทธิ', + monthlyReport: 'รายงานการเงินรายเดือน', + selectMonth: 'เลือกเดือน', + month: 'เดือน', + incomeCategory: 'หมวดหมู่รายรับ', + projectExpenseCategory: 'หมวดหมู่รายจ่ายโครงการ', + companyExpenseCategory: 'หมวดหมู่รายจ่ายบริษัท', + categoryTag: 'รายรับ', + projectTag: 'โครงการ', + companyTag: 'บริษัท', + }, + + paymentRequest: { + title: 'คำขอชำระเงิน', + description: 'จัดการคำขอชำระเงินภายนอก', + newRequest: 'สร้างคำขอชำระเงินใหม่', + activeApplications: 'คำขอที่ยังดำเนินการ', + completed: 'เสร็จสมบูรณ์', + subject: 'เรื่อง', + applicant: 'ผู้ยื่นคำขอ', + payee: 'หน่วยงานผู้รับเงิน', + amount: 'จำนวนเงิน', + applicationDate: 'วันที่ยื่นคำขอ', + status: 'สถานะ', + code: 'เลขที่', + action: 'ดำเนินการ', + edit: 'แก้ไข', + withdraw: 'ถอนกลับ', + reEdit: 'แก้ไขและส่งใหม่', + delete: 'ลบ', + deleteSuccess: 'ลบสำเร็จ', + withdrawSuccess: 'ถอนกลับแล้ว สามารถแก้ไขใหม่ได้', + withdrawFailed: 'ถอนกลับล้มเหลว', + editPayment: 'แก้ไขคำขอชำระเงิน', + newPayment: 'สร้างคำขอชำระเงินใหม่', + expenseType: 'ประเภทรายจ่าย', + selectExpenseType: 'เลือกประเภทรายจ่าย', + relatedProject: 'โครงการที่เกี่ยวข้อง', + selectProject: 'เลือกโครงการ', + expenseCategory: 'หมวดหมู่รายจ่าย', + selectCategory: 'เลือกหมวดหมู่รายจ่าย', + payeeType: 'ประเภทหน่วยงานผู้รับเงิน', + selectPayeeType: 'เลือกประเภทหน่วยงานผู้รับเงิน', + selectSubcontractor: 'เลือกผู้รับเหมาช่วง', + selectSupplier: 'เลือกซัพพลายเออร์', + selectCustomer: 'เลือกลูกค้า', + payeeName: 'หน่วยงานผู้รับเงิน', + payeeNamePlaceholder: 'กรอกชื่อหน่วยงานผู้รับเงินด้วยตนเอง', + accountName: 'ชื่อบัญชีผู้รับเงิน', + accountNamePlaceholder: 'ชื่อบัญชีผู้รับเงิน (กรอกอัตโนมัติเมื่อเลือกผู้รับเหมาช่วง/ซัพพลายเออร์/ลูกค้า)', + bankAccount: 'เลขที่บัญชีธนาคาร', + bankAccountPlaceholder: 'เลขที่บัญชีธนาคารผู้รับเงิน (กรอกอัตโนมัติเมื่อเลือกผู้รับเหมาช่วง/ซัพพลายเออร์/ลูกค้า)', + bankName: 'ธนาคารที่เปิดบัญชี', + bankNamePlaceholder: 'ชื่อธนาคารที่เปิดบัญชี (กรอกอัตโนมัติเมื่อเลือกผู้รับเหมาช่วง/ซัพพลายเออร์/ลูกค้า)', + qrCode: 'รหัส QR รับเงิน', + paymentAmount: 'จำนวนเงินที่ชำระ', + paymentAmountPlaceholder: 'กรอกจำนวนเงินที่ชำระ', + paymentReason: 'เหตุผลการชำระเงิน', + paymentReasonPlaceholder: 'เหตุผลการชำระเงิน', + uploadProof: 'อัปโหลดไฟล์หลักฐาน', + proofAttachment: 'ไฟล์หลักฐาน', + equivalentCNY: 'เทียบเท่าหยวน: ¥ ', + getListFailed: 'ดึงรายการคำขอชำระเงินล้มเหลว', + deleteFailed: 'ลบล้มเหลว', + operationSuccess: 'ดำเนินการสำเร็จ', + detailTitle: 'รายละเอียดคำขอชำระเงิน', + applicationCode: 'เลขที่คำขอ', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + rejected: 'ถูกตีกลับ', + withdrawn: 'ถอนกลับแล้ว', + paid: 'ชำระแล้ว', + companyExpense: 'รายจ่ายบริษัท', + projectExpense: 'รายจ่ายโครงการ', + counterpartySubcontractor: 'ผู้รับเหมาช่วง', + counterpartySupplier: 'ซัพพลายเออร์', + counterpartyCustomer: 'ลูกค้า', + counterpartyOther: 'อื่นๆ', + withdrawConfirm: 'ยืนยันการถอนกลับ', + withdrawConfirmMsg: 'หลังจากถอนกลับสามารถแก้ไขและส่งใหม่ได้ ต้องการถอนกลับหรือไม่?', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + deleteConfirmMsg: 'ยืนยันการลบคำขอชำระเงินนี้?', + }, + + paymentPlan: { + title: 'แผนการชำระเงิน', + description: 'จัดการแผนการชำระเงินของใบสั่งซื้อ', + newPlan: 'สร้างแผนการชำระเงินใหม่', + editPlan: 'แก้ไขแผนการชำระเงิน', + planCode: 'เลขที่แผน', + purchaseOrder: 'ใบสั่งซื้อ', + paymentDate: 'วันที่ชำระเงิน', + amount: 'จำนวนเงิน', + paymentType: 'ประเภทการชำระเงิน', + status: 'สถานะ', + creator: 'ผู้สร้าง', + action: 'ดำเนินการ', + pending: 'รอดำเนินการ', + approved: 'อนุมัติแล้ว', + executed: 'ดำเนินการแล้ว', + cancelled: 'ยกเลิกแล้ว', + partialPayment: 'ชำระบางส่วน', + fullPayment: 'ชำระเต็มจำนวน', + selectOrder: 'กรุณาเลือกใบสั่งซื้อ', + selectDate: 'กรุณาเลือกวันที่ชำระเงิน', + inputAmount: 'กรุณากรอกจำนวนเงินชำระ', + selectCurrency: 'กรุณาเลือกสกุลเงิน', + selectType: 'กรุณาเลือกประเภทการชำระเงิน', + selectStatus: 'กรุณาเลือกสถานะ', + inputCreator: 'กรุณากรอกผู้สร้าง', + amountPlaceholder: 'จำนวนเงินชำระ', + descPlaceholder: 'กรุณากรอกคำอธิบายแผนการชำระเงิน', + detailTitle: 'รายละเอียดแผนการชำระเงิน', + detailCode: 'เลขที่แผน', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + getListFailed: 'ดึงรายการแผนการชำระเงินล้มเหลว', + getDetailFailed: 'ดึงรายละเอียดแผนการชำระเงินล้มเหลว', + }, + + verification: { + title: 'คำขอตรวจสอบ', + description: 'การตรวจสอบเงินเบิกล่วงหน้า', + newVerification: 'สร้างคำขอตรวจสอบใหม่', + editVerification: 'แก้ไขคำขอตรวจสอบ', + subject: 'เรื่อง', + applicant: 'ผู้ยื่นคำขอ', + relatedAdvance: 'เงินเบิกล่วงหน้าที่เกี่ยวข้อง', + amount: 'จำนวนเงิน', + verificationDate: 'วันที่ตรวจสอบ', + status: 'สถานะ', + code: 'เลขที่', + action: 'ดำเนินการ', + detail: 'รายละเอียด', + withdraw: 'ถอนกลับ', + reEdit: 'แก้ไขและส่งใหม่', + delete: 'ลบ', + addDetail: 'เพิ่มรายละเอียด', + advanceAmount: 'จำนวนเงินเบิกล่วงหน้า', + settlementOptions: 'ตัวเลือกการชำระบัญชี', + settlementAmount: 'จำนวนเงินชำระบัญชี', + expenseType: 'ประเภทรายจ่าย', + selectProject: 'เลือกโครงการ', + subjectPlaceholder: 'คำอธิบายเหตุผลการตรวจสอบ', + detailLabel: 'รายละเอียดการตรวจสอบ', + expenseDescription: 'คำอธิบายค่าใช้จ่าย', + expenseCategory: 'หมวดหมู่รายจ่าย', + detailAmount: 'จำนวนเงิน', + attachment: 'ไฟล์หลักฐาน', + mainAttachment: 'ไฟล์แนบหลัก', + refundProof: 'หลักฐานการคืนเงิน (จำเป็นต้องกรอก)', + overallAttachment: 'ไฟล์หลักฐานโดยรวม', + selectAdvance: 'กรุณาเลือกใบเบิกล่วงหน้าที่เกี่ยวข้อง', + selectAdvanceOrInput: 'เลือกหรือกรอกเลขที่ใบเบิกล่วงหน้า', + selectProjectRequired: 'กรุณาเลือกโครงการ', + uploadRefundRequired: 'กรุณาอัปโหลดหลักฐานการคืนเงิน', + finalSettlement: 'เป็นการชำระบัญชีขั้นสุดท้ายหรือไม่', + verifiedAmount: 'จำนวนเงินที่ตรวจสอบแล้ว: ', + remainingAmount: 'จำนวนเงินคงเหลือ: ', + refundLabel: 'คืนเงิน ¥{amount}', + supplementLabel: 'ชำระเพิ่ม ¥{amount}', + totalLabel: 'รวม: ', + refundNote: '* การตรวจสอบประเภทคืนเงินต้องอัปโหลดหลักฐานการคืนเงิน', + unknownProject: 'ไม่ทราบโครงการ', + advanceInfo: 'ข้อมูลใบเบิกล่วงหน้า', + advanceCode: 'เลขที่ใบเบิกล่วงหน้า', + advanceTotalAmount: 'จำนวนเงินเบิกล่วงหน้า', + advanceVerified: 'จำนวนเงินที่ตรวจสอบแล้ว', + advanceRemaining: 'จำนวนเงินคงเหลือ', + detailTitle: 'รายละเอียดการตรวจสอบ', + attachmentCount: '{count} แผ่น', + isSettlement: 'ใช่', + notSettlement: 'ไม่', + refundText: 'คืนเงิน ', + supplementText: 'ชำระเพิ่ม ', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + rejected: 'ถูกตีกลับ', + withdrawn: 'ถอนกลับแล้ว', + paid: 'ชำระแล้ว', + pendingEdit: 'รอแก้ไข', + deleteSuccess: 'ลบสำเร็จ', + deleteFailed: 'ลบล้มเหลว', + withdrawSuccess: 'ถอนกลับแล้ว สามารถแก้ไขใหม่ได้', + withdrawFailed: 'ถอนกลับล้มเหลว', + saveSuccess: 'บันทึกสำเร็จ', + createSuccess: 'สร้างสำเร็จ', + submitSuccess: 'ส่งสำเร็จ', + getListFailed: 'ดึงรายการตรวจสอบล้มเหลว', + companyExpense: 'รายจ่ายบริษัท', + projectExpense: 'รายจ่ายโครงการ', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + withdrawConfirm: 'ยืนยันการถอนกลับ', + withdrawConfirmMsg: 'หลังจากถอนกลับสามารถแก้ไขและส่งใหม่ได้ ต้องการถอนกลับหรือไม่?', + deleteConfirmMsg: 'ต้องการลบบันทึกการตรวจสอบนี้หรือไม่?', + project: 'โครงการที่เกี่ยวข้อง', + projectPlaceholder: 'เลือกโครงการ', + category: 'หมวดหมู่ค่าใช้จ่าย', + categoryPlaceholder: 'เลือกหมวดหมู่ค่าใช้จ่าย', + categoryLabel: 'หมวดหมู่: ', + categoryRequired: 'กรุณาเลือกหมวดหมู่ค่าใช้จ่าย', + verificationAmount: 'จำนวนเงินตรวจสอบ', + settlementType: 'ประเภทการชำระ', + settlement: 'การชำระ', + settlementInfo: 'ข้อมูลการชำระ', + nonSettlement: 'ไม่ใช่การชำระ', + expenseDetail: 'รายละเอียดค่าใช้จ่าย', + totalItems: 'ทั้งหมด {count} รายการ', + paymentProof: 'หลักฐานการชำระเงิน', + receipt: 'ใบเสร็จ', + addExpense: 'เพิ่มค่าใช้จ่าย', + editExpense: 'แก้ไขค่าใช้จ่าย', + edit: 'แก้ไข', + viewDetail: 'ดูรายละเอียด', + refund: 'คืนเงิน', + supplement: 'ชำระเพิ่ม', + inputAmount: 'กรุณากรอกจำนวนเงิน', + amountPlaceholder: 'กรอกจำนวนเงิน', + equivalentCNY: 'เทียบเท่าหยวน: ¥', + totalAmount: 'รวม: ¥{amount}', + descriptionPlaceholder: 'กรอกคำอธิบายค่าใช้จ่าย', + advanceCodePlaceholder: 'เลือกหรือกรอกรหัสเบิกล่วงหน้า', + deleteConfirm: 'ยืนยันการลบ', + getDetailFailed: 'ดึงข้อมูลรายละเอียดล้มเหลว', + }, + + advance: { + title: 'คำขอเบิกล่วงหน้า', + description: 'จัดการคำขอเบิกล่วงหน้าของพนักงาน', + newAdvance: 'สร้างคำขอเบิกล่วงหน้าใหม่', + editAdvance: 'แก้ไขคำขอเบิกล่วงหน้า', + subject: 'เรื่อง', + applicant: 'ผู้ยื่นคำขอ', + amount: 'จำนวนเงิน', + advanceDate: 'วันที่เบิกล่วงหน้า', + status: 'สถานะ', + code: 'เลขที่', + action: 'ดำเนินการ', + detail: 'รายละเอียด', + withdraw: 'ถอนกลับ', + reEdit: 'แก้ไขและส่งใหม่', + delete: 'ลบ', + activeApplications: 'คำขอที่ยังดำเนินการ', + completed: 'เสร็จสมบูรณ์', + advanceCode: 'เลขที่เบิกล่วงหน้า', + subjectPlaceholder: 'กรุณากรอกเรื่องการเบิกล่วงหน้า', + amountPlaceholder: 'กรอกจำนวนเงิน', + inputAmount: 'กรุณากรอกจำนวนเงิน', + equivalentCNY: 'เทียบเท่าหยวน: ¥ ', + attachment: 'ไฟล์หลักฐาน', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + rejected: 'ถูกตีกลับ', + withdrawn: 'ถอนกลับแล้ว', + verified: 'ตรวจสอบแล้ว', + pendingEdit: 'รอแก้ไข', + deleteSuccess: 'ลบสำเร็จ', + deleteFailed: 'ลบล้มเหลว', + withdrawSuccess: 'ถอนกลับแล้ว สามารถแก้ไขใหม่ได้', + withdrawFailed: 'ถอนกลับล้มเหลว', + saveSuccess: 'บันทึกสำเร็จ', + createSuccess: 'สร้างสำเร็จ', + submitSuccess: 'ส่งสำเร็จ', + getListFailed: 'ดึงรายการเบิกล่วงหน้าล้มเหลว', + getDetailFailed: 'ดึงรายละเอียดล้มเหลว', + deletePassInput: 'กรุณากรอกรหัสผ่านเพื่อยืนยันการลบ', + deletePassPlaceholder: 'กรอกรหัสผ่าน', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + detailTitle: 'รายละเอียดการเบิกล่วงหน้า', + withdrawConfirm: 'ยืนยันการถอนกลับ', + withdrawConfirmMsg: 'หลังจากถอนกลับสามารถแก้ไขและส่งใหม่ได้ ต้องการถอนกลับหรือไม่?', + }, + + advanceVerification: { + title: 'การจัดการสถานะการเบิกล่วงหน้า', + description: 'จัดการสถานะและความคืบหน้าการตรวจสอบใบเบิกล่วงหน้า', + subject: 'เรื่อง', + applicant: 'ผู้ยื่นคำขอ', + advanceAmount: 'จำนวนเงินเบิกล่วงหน้า', + verifiedAmount: 'จำนวนเงินที่ตรวจสอบแล้ว', + remainingAmount: 'จำนวนเงินคงเหลือ', + advanceDate: 'วันที่เบิกล่วงหน้า', + status: 'สถานะ', + code: 'เลขที่', + action: 'ดำเนินการ', + searchByName: 'ค้นหาตามชื่อผู้ยื่นคำขอ', + startDate: 'วันที่เริ่ม', + endDate: 'วันที่สิ้นสุด', + search: 'ค้นหา', + unverified: 'ยังตรวจสอบไม่เสร็จ', + completed: 'เสร็จสมบูรณ์', + detail: 'รายละเอียด', + noAccess: 'ไม่มีสิทธิ์เข้าถึง', + noAccessMsg: 'คุณไม่มีสิทธิ์เข้าถึงหน้านี้ เฉพาะผู้ดูแลระบบและเจ้าหน้าที่การเงินเท่านั้นที่สามารถดูสถานะการเบิกล่วงหน้าได้', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + rejected: 'ถูกตีกลับ', + withdrawn: 'ถอนกลับแล้ว', + verified: 'ตรวจสอบแล้ว', + pendingEdit: 'รอแก้ไข', + partialVerified: 'ตรวจสอบบางส่วน', + completedStatus: 'เสร็จสมบูรณ์', + detailTitle: 'รายละเอียดใบเบิกล่วงหน้า: {code}', + advanceCode: 'เลขที่เบิกล่วงหน้า', + amount: 'จำนวนเงิน', + verifiedLabel: 'จำนวนเงินที่ตรวจสอบแล้ว', + remaining: 'จำนวนเงินคงเหลือ', + currency: 'สกุลเงิน', + relatedVerifications: 'ใบตรวจสอบที่เกี่ยวข้อง', + verificationCode: 'เลขที่ใบตรวจสอบ', + relatedAdvance: 'ใบเบิกล่วงหน้าที่เกี่ยวข้อง', + verificationAmount: 'จำนวนเงินตรวจสอบ', + verificationDate: 'วันที่ตรวจสอบ', + isSettlement: 'เป็นการชำระบัญชีหรือไม่', + getListFailed: 'ดึงรายการใบเบิกล่วงหน้าล้มเหลว', + getDetailFailed: 'ดึงรายละเอียดใบเบิกล่วงหน้าล้มเหลว', + }, + + reimbursement: { + title: 'คำขอเบิกค่าใช้จ่าย', + description: 'จัดการคำขอเบิกค่าใช้จ่าย', + newReimbursement: 'สร้างคำขอเบิกค่าใช้จ่ายใหม่', + editReimbursement: 'แก้ไขคำขอเบิกค่าใช้จ่าย', + subject: 'เรื่อง', + applicant: 'ผู้ยื่นคำขอ', + amount: 'จำนวนเงิน', + reimbursementDate: 'วันที่เบิกค่าใช้จ่าย', + status: 'สถานะ', + code: 'เลขที่', + action: 'ดำเนินการ', + detail: 'รายละเอียด', + withdraw: 'ถอนกลับ', + reEdit: 'แก้ไขและส่งใหม่', + delete: 'ลบ', + addDetail: 'เพิ่มรายละเอียด', + activeApplications: 'คำขอที่ยังดำเนินการ', + completed: 'เสร็จสมบูรณ์', + expenseType: 'ประเภทรายจ่าย', + selectProject: 'เลือกโครงการ', + subjectPlaceholder: 'กรุณากรอกเรื่องการเบิกค่าใช้จ่าย', + detailLabel: 'รายละเอียดการเบิกค่าใช้จ่าย', + expenseDescription: 'คำอธิบายค่าใช้จ่าย', + expenseCategory: 'หมวดหมู่รายจ่าย', + detailAmount: 'จำนวนเงิน', + attachment: 'ไฟล์หลักฐาน', + mainAttachment: 'ไฟล์แนบหลัก', + overallAttachment: 'ไฟล์หลักฐานโดยรวม', + totalLabel: 'รวม: ', + unknownProject: 'ไม่ทราบโครงการ', + reimbursementCode: 'เลขที่เบิกค่าใช้จ่าย', + attachmentCount: '{count} แผ่น', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + rejected: 'ถูกตีกลับ', + withdrawn: 'ถอนกลับแล้ว', + paid: 'ชำระแล้ว', + pendingEdit: 'รอแก้ไข', + deleteSuccess: 'ลบสำเร็จ', + deleteFailed: 'ลบล้มเหลว', + withdrawSuccess: 'ถอนกลับแล้ว สามารถแก้ไขใหม่ได้', + withdrawFailed: 'ถอนกลับล้มเหลว', + saveSuccess: 'บันทึกสำเร็จ', + createSuccess: 'สร้างสำเร็จ', + submitSuccess: 'ส่งสำเร็จ', + getListFailed: 'ดึงรายการเบิกค่าใช้จ่ายล้มเหลว', + deletePassInput: 'กรุณากรอกรหัสผ่านเพื่อยืนยันการลบ', + deletePassPlaceholder: 'กรอกรหัสผ่าน', + companyExpense: 'รายจ่ายบริษัท', + projectExpense: 'รายจ่ายโครงการ', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + detailTitle: 'รายละเอียดการเบิกค่าใช้จ่าย', + withdrawConfirm: 'ยืนยันการถอนกลับ', + withdrawConfirmMsg: 'หลังจากถอนกลับสามารถแก้ไขและส่งใหม่ได้ ต้องการถอนกลับหรือไม่?', + expenseDetail: 'รายละเอียดค่าใช้จ่าย', + totalItems: 'ทั้งหมด {count} รายการ', + receipt: 'ใบเสร็จ', + addExpense: 'เพิ่มค่าใช้จ่าย', + editExpense: 'แก้ไขค่าใช้จ่าย', + category: 'หมวดหมู่ค่าใช้จ่าย', + categoryLabel: 'หมวดหมู่: ', + categoryPlaceholder: 'เลือกหมวดหมู่ค่าใช้จ่าย', + categoryRequired: 'กรุณาเลือกหมวดหมู่ค่าใช้จ่าย', + descriptionPlaceholder: 'กรอกคำอธิบายค่าใช้จ่าย', + project: 'โครงการที่เกี่ยวข้อง', + projectPlaceholder: 'เลือกโครงการ', + inputAmount: 'กรุณากรอกจำนวนเงิน', + amountPlaceholder: 'กรอกจำนวนเงิน', + equivalentCNY: 'เทียบเท่าหยวน: ¥', + totalAmount: 'รวม: ¥{amount}', + edit: 'แก้ไข', + viewDetail: 'ดูรายละเอียด', + getDetailFailed: 'ดึงข้อมูลรายละเอียดล้มเหลว', + deleteConfirmMsg: 'ยืนยันการลบรายการเบิกค่าใช้จ่ายนี้?', + }, + + approval: { + title: 'การจัดการอนุมัติ', + description: 'อนุมัติคำขอเบิกล่วงหน้า เบิกค่าใช้จ่าย ชำระเงิน และอื่นๆ', + approve: 'อนุมัติ', + refresh: 'รีเฟรชข้อมูล', + pass: 'อนุมัติผ่าน', + reject: 'ตีกลับ', + close: 'ปิด', + pendingTab: 'รออนุมัติ', + historyTab: 'ประวัติการอนุมัติ', + subject: 'เรื่อง', + type: 'ประเภท', + applicant: 'ผู้ยื่นคำขอ', + amount: 'จำนวนเงิน', + applicationDate: 'วันที่ยื่นคำขอ', + status: 'สถานะ', + code: 'เลขที่', + action: 'ดำเนินการ', + time: 'เวลา', + operation: 'การดำเนินการ', + operator: 'ผู้ดำเนินการ', + note: 'หมายเหตุ/เหตุผล', + applicationType: 'ประเภทคำขอ', + applicationCode: 'เลขที่คำขอ', + payeeType: 'ประเภทหน่วยงานผู้รับเงิน', + payee: 'ผู้รับเงิน', + bankName: 'ชื่อธนาคาร', + bankAccount: 'เลขที่บัญชีธนาคาร', + expenseType: 'ประเภทรายจ่าย', + relatedProject: 'โครงการที่เกี่ยวข้อง', + expenseCategory: 'หมวดหมู่รายจ่าย', + relatedAdvance: 'ใบเบิกล่วงหน้าที่เกี่ยวข้อง', + advanceAmount: 'จำนวนเงินเบิกล่วงหน้า', + settlement: 'การตรวจสอบชำระบัญชี', + verifiedAmount: 'จำนวนเงินที่ตรวจสอบแล้ว', + remainingAmount: 'จำนวนเงินคงเหลือการตรวจสอบ', + settlementAmount: 'จำนวนเงินชำระบัญชีการตรวจสอบ', + purchaseType: 'ประเภทการจัดซื้อ', + supplier: 'ซัพพลายเออร์', + currency: 'สกุลเงิน', + remark: 'หมายเหตุ', + approvalNote: 'หมายเหตุการอนุมัติ', + approvalNotePlaceholder: 'ไม่บังคับ: กรอกหมายเหตุการอนุมัติ', + rejectReason: 'เหตุผลการตีกลับ', + rejectReasonPlaceholder: 'กรุณากรอกเหตุผลการตีกลับ', + productDetail: 'รายละเอียดสินค้า', + detailList: 'รายการรายละเอียด', + refundProof: 'หลักฐานการคืนเงิน', + proofAttachment: 'ไฟล์หลักฐาน', + approvalOpinion: 'ความเห็นการอนุมัติ', + detailLabel: 'รายละเอียดที่ {index}:', + categoryLabel: 'หมวดหมู่รายจ่าย: ', + detailAttachment: 'ไฟล์แนบรายละเอียด: ', + specLabel: 'ข้อมูลจำเพาะ: ', + unitLabel: 'หน่วย: ', + qtyLabel: 'จำนวน: ', + priceLabel: 'ราคาต่อหน่วย: ', + advanceApply: 'คำขอเบิกล่วงหน้า', + reimburseApply: 'คำขอเบิกค่าใช้จ่าย', + paymentApply: 'คำขอชำระเงิน', + verificationApply: 'คำขอตรวจสอบ', + purchaseApply: 'คำขอจัดซื้อ', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติผ่าน', + rejected: 'ถูกตีกลับ', + withdrawn: 'ถอนกลับแล้ว', + executed: 'ดำเนินการแล้ว', + partialVerified: 'ตรวจสอบบางส่วน', + completed: 'เสร็จสมบูรณ์', + getPendingFailed: 'ดึงข้อมูลรออนุมัติล้มเหลว', + getHistoryFailed: 'ดึงประวัติการอนุมัติล้มเหลว', + approveSuccess: 'อนุมัติผ่าน: {code}', + rejectSuccess: 'ตีกลับแล้ว: {code}', + withdrawSuccess: 'ถอนกลับคำขอแล้ว', + editResubmit: 'แก้ไขสำเร็จ ส่งอนุมัติใหม่แล้ว', + withdrawConfirm: 'ถอนกลับคำขอ', + withdrawConfirmMsg: 'ต้องการถอนกลับคำขอ {code} หรือไม่?', + withdrawConfirmBtn: 'ยืนยันการถอนกลับ', + projectPurchase: 'จัดซื้อโครงการ', + stockPurchase: 'จัดซื้อคลังสินค้า', + refundText: 'คืนเงิน ', + supplementText: 'ชำระเพิ่ม ', + editTitle: 'แก้ไขคำขอ: {code}', + detailTitle: 'รายละเอียด{type}: {code}', + advanceDetailTitle: 'รายละเอียด{type}: {code}', + counterpartySubcontractor: 'ผู้รับเหมาช่วง', + counterpartySupplier: 'ซัพพลายเออร์', + counterpartyCustomer: 'ลูกค้า', + counterpartyOther: 'อื่นๆ', + companyExpense: 'รายจ่ายบริษัท', + projectExpense: 'รายจ่ายโครงการ', + material: 'วัสดุ', + equipment: 'อุปกรณ์', + pole: 'เสาไฟฟ้า', + other: 'อื่นๆ', + accommodation: 'ที่พัก', + catering: 'อาหาร', + fuel: 'น้ำมันเชื้อเพลิง', + scatteredMaterial: 'วัสดุเบ็ดเตล็ด', + customerRelation: 'ความสัมพันธ์ลูกค้า', + subcontractorRelation: 'ความสัมพันธ์ผู้รับเหมาช่วง', + EDLRelation: 'ความสัมพันธ์ EDL', + extraConstruction: 'งานก่อสร้างเพิ่มเติม', + generalOperation: 'การดำเนินงานทั่วไป (ค่าเช่า/วัสดุสิ้นเปลือง)', + commute: 'ค่าเดินทาง', + marketing: 'การตลาด', + powerSystem: 'ความสัมพันธ์ระบบไฟฟ้า', + employeeBenefit: 'สวัสดิการพนักงาน', + expressLogistics: 'ค่าขนส่งพัสดุ', + }, + + execution: { + title: 'การจัดการการดำเนินการ', + description: 'ดำเนินการคำขอชำระเงินที่อนุมัติแล้ว', + execute: 'ดำเนินการ', + edit: 'แก้ไข', + cancel: 'ยกเลิก', + pass: 'ตีกลับ', + close: 'ปิด', + uploadProof: 'อัปโหลดหลักฐานการชำระเงิน', + pendingTab: 'รอดำเนินการ', + executedTab: 'ดำเนินการแล้ว', + subject: 'เรื่อง', + type: 'ประเภท', + applicant: 'ผู้ยื่นคำขอ', + amount: 'จำนวนเงิน', + payee: 'ผู้รับเงิน', + approvalDate: 'วันที่อนุมัติ', + code: 'เลขที่', + action: 'ดำเนินการ', + executionDate: 'วันที่ดำเนินการ', + executionMethod: 'วิธีการดำเนินการ', + status: 'สถานะ', + searchPlaceholder: 'ค้นหาเรื่อง เลขที่ หรือผู้ยื่นคำขอ', + filterType: 'กรองตามประเภท', + sortBy: 'วิธีการเรียงลำดับ', + sortDateNew: 'วันที่ดำเนินการ (ล่าสุด)', + sortDateOld: 'วันที่ดำเนินการ (เก่าสุด)', + sortAmountHigh: 'จำนวนเงิน (มากไปน้อย)', + sortAmountLow: 'จำนวนเงิน (น้อยไปมาก)', + confirmationDate: 'วันที่ยืนยัน', + paymentMethod: 'วิธีการรับเงิน', + executionMethodLabel: 'วิธีการดำเนินการ', + proofOfPayment: 'หลักฐานการชำระเงิน', + paymentConfirmation: 'ข้อมูลการยืนยันการรับเงิน', + remark: 'หมายเหตุ', + returnReason: 'เหตุผลการตีกลับ', + confirmRequired: 'กรุณากรอกข้อมูลการยืนยันการรับเงิน', + rejectReasonRequired: 'กรุณากรอกเหตุผลการตีกลับ', + confirmationPlaceholder: 'กรุณากรอกข้อมูลการยืนยันการรับเงิน เช่น เลขที่บัญชีรับเงิน เวลารับเงิน เป็นต้น', + remarkPlaceholder: 'ไม่บังคับ: กรอกหมายเหตุการดำเนินการ', + bankTransfer: 'โอนเงินผ่านธนาคาร', + cash: 'เงินสด', + wechat: 'WeChat', + other: 'อื่นๆ', + proofUploadTip: 'กรุณาอัปโหลดหลักฐานการชำระเงิน (ใบโอนเงินธนาคาร ใบรับเงินสด ฯลฯ) รองรับรูปภาพและ PDF', + noProofRefund: 'คำขอตรวจสอบนี้เป็นประเภทคืนเงิน ไม่จำเป็นต้องอัปโหลดหลักฐานการชำระเงิน', + noProofNonSettlement: 'คำขอตรวจสอบนี้เป็นการตรวจสอบแบบไม่ชำระบัญชี ไม่จำเป็นต้องอัปโหลดหลักฐานการชำระเงิน', + pendingExecution: 'รอดำเนินการ', + executed: 'ดำเนินการแล้ว', + rejected: 'ถูกตีกลับ', + approved: 'อนุมัติแล้ว', + executeSuccess: 'ดำเนินการสำเร็จ: {code}', + executeFailed: 'ดำเนินการล้มเหลว กรุณาลองใหม่อีกครั้ง', + proofRequired: 'กรุณาอัปโหลดหลักฐานการชำระเงิน', + rejectSuccess: 'ตีกลับแล้ว: {code} ผู้ยื่นคำขอสามารถแก้ไขและส่งใหม่ได้', + rejectFailed: 'ตีกลับล้มเหลว กรุณาลองใหม่อีกครั้ง', + editSuccess: 'แก้ไขสำเร็จ ส่งอนุมัติใหม่แล้ว', + uploadSuccess: '{name} อัปโหลดสำเร็จ', + uploadFailed: '{name} อัปโหลดล้มเหลว', + getPendingFailedFormat: 'ดึงข้อมูลรอดำเนินการล้มเหลว: รูปแบบข้อมูลไม่ถูกต้อง', + getPendingFailed: 'ดึงข้อมูลรอดำเนินการล้มเหลว: ', + getPendingNetworkError: 'ข้อผิดพลาดเครือข่าย ดึงข้อมูลรอดำเนินการล้มเหลว', + getExecutedFailedFormat: 'ดึงข้อมูลดำเนินการแล้วล้มเหลว: รูปแบบข้อมูลไม่ถูกต้อง', + getExecutedFailed: 'ดึงข้อมูลดำเนินการแล้วล้มเหลว: ', + getExecutedNetworkError: 'ข้อผิดพลาดเครือข่าย ดึงข้อมูลดำเนินการแล้วล้มเหลว', + supplierPaymentInfo: 'ข้อมูลการรับเงินของซัพพลายเออร์', + accountName: 'ชื่อบัญชีผู้รับเงิน', + bankAccount: 'เลขที่บัญชีธนาคาร', + bankName: 'ธนาคารที่เปิดบัญชี', + qrCode: 'รหัส QR รับเงิน', + purchaseDetail: 'รายละเอียดการจัดซื้อ', + detailList: 'รายการรายละเอียด', + approvalOpinion: 'ความเห็นการอนุมัติ', + refundProof: 'หลักฐานการคืนเงิน', + applicationAttachment: 'ไฟล์หลักฐานคำขอ', + executionInfo: 'ข้อมูลการดำเนินการ', + applicationType: 'ประเภทคำขอ', + applicationCode: 'เลขที่คำขอ', + payeeType: 'ประเภทหน่วยงานผู้รับเงิน', + expenseType: 'ประเภทรายจ่าย', + relatedProject: 'โครงการที่เกี่ยวข้อง', + expenseCategory: 'หมวดหมู่รายจ่าย', + relatedAdvance: 'ใบเบิกล่วงหน้าที่เกี่ยวข้อง', + advanceAmount: 'จำนวนเงินเบิกล่วงหน้า', + settlement: 'การตรวจสอบชำระบัญชี', + verifiedAmount: 'จำนวนเงินที่ตรวจสอบแล้ว', + remainingAmount: 'จำนวนเงินคงเหลือการตรวจสอบ', + settlementAmount: 'จำนวนเงินชำระบัญชีการตรวจสอบ', + purchaseType: 'ประเภทการจัดซื้อ', + supplier: 'ซัพพลายเออร์', + currency: 'สกุลเงิน', + detailLabel: 'รายละเอียดที่ {index}:', + categoryLabel: 'หมวดหมู่รายจ่าย: ', + detailAttachment: 'ไฟล์แนบรายละเอียด: ', + specLabel: 'ข้อมูลจำเพาะ: ', + unitLabel: 'หน่วย: ', + qtyLabel: 'จำนวน: ', + advanceApply: 'คำขอเบิกล่วงหน้า', + reimburseApply: 'คำขอเบิกค่าใช้จ่าย', + paymentApply: 'คำขอชำระเงิน', + verificationApply: 'คำขอตรวจสอบ', + purchaseApply: 'คำขอจัดซื้อ', + projectPurchase: 'จัดซื้อโครงการ', + stockPurchase: 'จัดซื้อคลังสินค้า', + refundText: 'คืนเงิน ', + supplementText: 'ชำระเพิ่ม ', + counterpartySubcontractor: 'ผู้รับเหมาช่วง', + counterpartySupplier: 'ซัพพลายเออร์', + counterpartyCustomer: 'ลูกค้า', + counterpartyOther: 'อื่นๆ', + companyExpense: 'รายจ่ายบริษัท', + projectExpense: 'รายจ่ายโครงการ', + material: 'วัสดุ', + equipment: 'อุปกรณ์', + pole: 'เสาไฟฟ้า', + otherCategory: 'อื่นๆ', + accommodation: 'ที่พัก', + catering: 'อาหาร', + fuel: 'น้ำมันเชื้อเพลิง', + scatteredMaterial: 'วัสดุเบ็ดเตล็ด', + customerRelation: 'ความสัมพันธ์ลูกค้า', + subcontractorRelation: 'ความสัมพันธ์ผู้รับเหมาช่วง', + EDLRelation: 'ความสัมพันธ์ EDL', + extraConstruction: 'งานก่อสร้างเพิ่มเติม', + generalOperation: 'การดำเนินงานทั่วไป (ค่าเช่า/วัสดุสิ้นเปลือง)', + commute: 'ค่าเดินทาง', + marketing: 'การตลาด', + powerSystem: 'ความสัมพันธ์ระบบไฟฟ้า', + employeeBenefit: 'สวัสดิการพนักงาน', + expressLogistics: 'ค่าขนส่งพัสดุ', + }, + + procurement: { + title: 'การจัดการจัดซื้อ', + description: 'จัดการใบสั่งซื้อและการรับวัสดุเข้าคลัง', + newProcurement: 'สร้างคำขอจัดซื้อใหม่', + orderCode: 'เลขที่ใบสั่งซื้อ', + purchaseDate: 'วันที่จัดซื้อ', + supplier: 'ซัพพลายเออร์', + materialName: 'ชื่อวัสดุ', + quantity: 'จำนวน', + unitPrice: 'ราคาต่อหน่วย', + totalAmount: 'จำนวนเงินรวม', + status: 'สถานะ', + action: 'ดำเนินการ', + view: 'ดู', + approve: 'อนุมัติ', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + stocked: 'รับเข้าคลังแล้ว', + rejected: 'ปฏิเสธแล้ว', + startDate: 'วันที่เริ่ม', + endDate: 'วันที่สิ้นสุด', + searchOrder: 'ค้นหาเลขที่ใบสั่งซื้อ', + monthPurchase: 'ยอดจัดซื้อเดือนนี้', + newApplication: 'สร้างคำขอจัดซื้อใหม่', + selectSupplier: 'เลือกซัพพลายเออร์', + inputMaterialName: 'กรุณากรอกชื่อวัสดุ', + remark: 'หมายเหตุ', + remarkPlaceholder: 'กรุณากรอกหมายเหตุ', + submitSuccess: 'ส่งคำขอจัดซื้อแล้ว', + }, + + purchaseRequest: { + title: 'คำขอจัดซื้อ', + description: 'จัดการคำขอจัดซื้อของบริษัท (แบบย่อ: กรอกเฉพาะคำอธิบายความต้องการและจำนวนเงินประมาณการ)', + newRequest: 'สร้างคำขอจัดซื้อใหม่', + activeApplications: 'คำขอที่ยังดำเนินการ', + completed: 'เสร็จสมบูรณ์', + subject: 'เรื่อง', + project: 'โครงการ', + category: 'หมวดหมู่', + estimatedAmount: 'จำนวนเงินประมาณการ', + demandDate: 'วันที่ต้องการ', + status: 'สถานะ', + applicationDate: 'วันที่ยื่นคำขอ', + applicant: 'ผู้ยื่นคำขอ', + code: 'เลขที่', + action: 'ดำเนินการ', + approve: 'อนุมัติผ่าน', + reject: 'ปฏิเสธ', + withdraw: 'ถอนกลับ', + edit: 'แก้ไข', + confirmDelete: 'ต้องการลบหรือไม่?', + selectProjectFilter: 'เลือกโครงการเพื่อกรอง', + selectStatusFilter: 'เลือกสถานะเพื่อกรอง', + editRequest: 'แก้ไขคำขอจัดซื้อ', + submitApproval: 'ส่งอนุมัติ', + purchaseType: 'ประเภทการจัดซื้อ', + selectPurchaseType: 'กรุณาเลือกประเภทการจัดซื้อ', + purchaseTypeRequired: 'กรุณาเลือกประเภทการจัดซื้อ', + stockPurchase: 'จัดซื้อคลังสินค้า', + projectPurchase: 'จัดซื้อโครงการ', + relatedProject: 'โครงการที่เกี่ยวข้อง', + selectProject: 'กรุณาเลือกโครงการ', + projectRequired: 'การจัดซื้อโครงการต้องเชื่อมโยงกับโครงการ', + applicantLabel: 'ผู้ยื่นคำขอ', + applicantPlaceholder: 'กรุณากรอกผู้ยื่นคำขอ', + applicantRequired: 'กรุณากรอกผู้ยื่นคำขอ', + applicationDateLabel: 'วันที่ยื่นคำขอ', + dateRequired: 'กรุณาเลือกวันที่ยื่นคำขอ', + subjectDescription: 'คำอธิบายเรื่อง', + subjectRequired: 'กรุณากรอกคำอธิบายเรื่อง', + subjectMaxLength: 'คำอธิบายเรื่องต้องไม่เกิน 100 ตัวอักษร', + subjectPlaceholder: 'กรุณาอธิบายความต้องการจัดซื้อโดยย่อ (เช่น: จัดซื้อสายเคเบิล เสาไฟฟ้า และวัสดุอื่นๆ สำหรับโครงการ XX)', + expenseCategory: 'หมวดหมู่รายจ่าย', + selectCategory: 'กรุณาเลือกหมวดหมู่รายจ่าย', + categoryRequired: 'กรุณาเลือกหมวดหมู่รายจ่าย', + material: 'วัสดุ', + equipment: 'อุปกรณ์', + pole: 'เสาไฟฟ้า', + other: 'อื่นๆ', + estimatedAmountLabel: 'จำนวนเงินประมาณการ', + amountRequired: 'กรุณากรอกจำนวนเงินประมาณการ', + estimatedAmountPlaceholder: 'จำนวนเงินประมาณการ', + currency: 'สกุลเงิน', + selectCurrency: 'กรุณาเลือกสกุลเงิน', + currencyRequired: 'กรุณาเลือกสกุลเงิน', + demandDateLabel: 'วันที่ต้องการ', + demandDatePlaceholder: 'วันที่คาดว่าจะได้รับสินค้า', + remarkLabel: 'หมายเหตุ', + remarkPlaceholder: 'กรุณากรอกหมายเหตุ (ไม่บังคับ)', + attachment: 'ไฟล์แนบ', + selectFile: 'เลือกไฟล์', + detailTitle: 'รายละเอียดคำขอจัดซื้อ', + applicationCode: 'เลขที่คำขอ', + createdAt: 'เวลาที่สร้าง', + getListFailed: 'ดึงรายการคำขอจัดซื้อล้มเหลว', + getDetailFailed: 'ดึงรายละเอียดคำขอจัดซื้อล้มเหลว', + deleteSuccess: 'ลบสำเร็จ', + deleteFailed: 'ลบล้มเหลว', + saveSuccess: 'บันทึกสำเร็จ', + createSuccess: 'สร้างสำเร็จ', + submitSuccess: 'สร้างและส่งสำเร็จ', + submitFailed: 'ส่งล้มเหลว', + withdrawSuccess: 'ถอนกลับสำเร็จ', + withdrawFailed: 'ถอนกลับล้มเหลว', + approveSuccess: 'อนุมัติผ่านสำเร็จ', + approveFailed: 'อนุมัติผ่านล้มเหลว', + rejectSuccess: 'ปฏิเสธสำเร็จ', + rejectFailed: 'ปฏิเสธล้มเหลว', + pendingEdit: 'รอแก้ไข', + pendingApproval: 'รออนุมัติ', + approved: 'อนุมัติแล้ว', + executed: 'ดำเนินการแล้ว', + withdrawn: 'ถอนกลับแล้ว', + currencyCNY: 'หยวน (CNY)', + currencyUSD: 'ดอลลาร์ (USD)', + currencyLAK: 'กีบ (LAK)', + currencyTHB: 'บาท (THB)', + }, + + purchaseOrder: { + title: 'ใบสั่งซื้อ', + description: 'จัดการใบสั่งซื้อ (หลายแท็บ: ข้อมูลพื้นฐาน รายละเอียดสินค้า ข้อมูลการชำระเงิน ข้อมูลโลจิสติกส์ บันทึกการตรวจรับ)', + draft: 'ร่าง', + confirmed: 'ยืนยันแล้ว', + partialPayment: 'ชำระบางส่วน', + paidOff: 'ชำระเต็มจำนวน', + inTransit: 'อยู่ระหว่างขนส่ง', + accepted: 'ตรวจรับแล้ว', + closed: 'ปิดแล้ว', + cancelled: 'ยกเลิกแล้ว', + orderCode: 'เลขที่ใบสั่ง', + supplier: 'ซัพพลายเออร์', + relatedProject: 'โครงการ', + amount: 'จำนวนเงิน', + paid: 'ชำระแล้ว', + status: 'สถานะ', + createDate: 'วันที่สร้าง', + action: 'ดำเนินการ', + confirm: 'ยืนยัน', + cancel: 'ยกเลิก', + delete: 'ลบ', + confirmCancel: 'ต้องการยกเลิกหรือไม่?', + confirmDelete: 'ต้องการลบหรือไม่?', + confirmDeleteShort: 'ต้องการลบ?', + productName: 'ชื่อสินค้า', + spec: 'ข้อมูลจำเพาะ', + unit: 'หน่วย', + quantity: 'จำนวน', + unitPrice: 'ราคาต่อหน่วย', + subtotal: 'รวมย่อย', + phase: 'ขั้นตอน', + plannedDate: 'วันที่ตามแผน', + plannedAmount: 'จำนวนเงินตามแผน', + ratioPercent: 'สัดส่วน%', + actualAmount: 'จำนวนเงินจริง', + pendingPayment: 'รอชำระเงิน', + applied: 'ยื่นคำขอแล้ว', + paidStatus: 'ชำระแล้ว', + trackingNumber: 'เลขที่ติดตามพัสดุ', + origin: 'ต้นทาง', + china: 'จีน', + laos: 'ลาว', + logisticsCompany: 'บริษัทโลจิสติกส์', + deliveryDate: 'วันที่จัดส่ง', + freight1: 'ค่าขนส่งครั้งที่ 1', + freight2: 'ค่าขนส่งครั้งที่ 2', + acceptanceCode: 'เลขที่ใบตรวจรับ', + acceptanceDate: 'วันที่ตรวจรับ', + acceptor: 'ผู้ตรวจรับ', + acceptedQty: 'จำนวนที่ตรวจรับ', + selectProject: 'เลือกโครงการเพื่อกรอง', + selectStatus: 'เลือกสถานะเพื่อกรอง', + detailTitle: 'รายละเอียดใบสั่งซื้อ - {code}', + basicInfo: 'ข้อมูลพื้นฐาน', + supplierCountry: 'ประเทศซัพพลายเออร์', + estimatedAmount: 'จำนวนเงินประมาณการ', + orderAmount: 'จำนวนเงินใบสั่ง', + paidAmount: 'จำนวนเงินที่ชำระแล้ว', + createdAt: 'เวลาที่สร้าง', + remark: 'หมายเหตุ', + productDetails: 'รายละเอียดสินค้า', + addProduct: 'เพิ่มสินค้า', + productTotal: 'รวมสินค้า: ', + paymentInfo: 'ข้อมูลการชำระเงิน', + addPaymentPlan: 'เพิ่มแผนการชำระเงิน', + logisticsInfo: 'ข้อมูลโลจิสติกส์', + noLogistics: 'ยังไม่มีข้อมูลโลจิสติกส์', + acceptanceRecord: 'บันทึกการตรวจรับ', + noAcceptance: 'ยังไม่มีบันทึกการตรวจรับ', + editProduct: 'แก้ไขสินค้า', + addProductTitle: 'เพิ่มสินค้า', + selectProduct: 'เลือกสินค้า', + productNameInput: 'ชื่อสินค้า', + specInput: 'ข้อมูลจำเพาะ', + unitInput: 'หน่วย', + quantityInput: 'จำนวน', + unitPriceInput: 'ราคาต่อหน่วย', + editPaymentPlan: 'แก้ไขแผนการชำระเงิน', + addPaymentPlanTitle: 'เพิ่มแผนการชำระเงิน', + selectPhase: 'เลือกขั้นตอน', + advancePayment: 'เงินมัดจำ', + deliveryPayment: 'เงินค่าจัดส่ง', + acceptancePayment: 'เงินค่าตรวจรับ', + finalPayment: 'เงินส่วนที่เหลือ', + orderConfirmSuccess: 'ยืนยันใบสั่งสำเร็จ', + orderConfirmFailed: 'ยืนยันใบสั่งล้มเหลว', + orderCancelSuccess: 'ยกเลิกใบสั่งแล้ว', + orderCancelFailed: 'ยกเลิกใบสั่งล้มเหลว', + orderDeleteSuccess: 'ลบใบสั่งสำเร็จ', + orderDeleteFailed: 'ลบใบสั่งล้มเหลว', + productUpdateSuccess: 'อัปเดตสินค้าสำเร็จ', + productAddSuccess: 'เพิ่มสินค้าสำเร็จ', + productDeleteSuccess: 'ลบสินค้าสำเร็จ', + paymentPlanUpdateSuccess: 'อัปเดตแผนการชำระเงินสำเร็จ', + paymentPlanAddSuccess: 'เพิ่มแผนการชำระเงินสำเร็จ', + paymentPlanDeleteSuccess: 'ลบแผนการชำระเงินสำเร็จ', + getListFailed: 'ดึงรายการใบสั่งซื้อล้มเหลว', + getDetailFailed: 'ดึงรายละเอียดใบสั่งล้มเหลว', + }, + + product: { + thumbnail: 'รูปขนาดย่อ', + productName: 'ชื่อสินค้า', + model: 'รุ่น', + level1Category: 'หมวดหมู่ระดับ 1', + level2Category: 'หมวดหมู่ระดับ 2', + unit: 'หน่วย', + quantity: 'จำนวน', + costPrice: 'ราคาต้นทุนต่อหน่วย', + brand: 'แบรนด์', + action: 'ดำเนินการ', + edit: 'แก้ไข', + delete: 'ลบ', + confirmDelete: 'ต้องการลบสินค้านี้หรือไม่?', + confirmDeleteCategory: 'ต้องการลบหมวดหมู่นี้หรือไม่?', + addLevel2: 'เพิ่มหมวดหมู่ระดับ 2', + addLevel1: 'เพิ่มหมวดหมู่ระดับ 1', + totalProducts: 'จำนวนสินค้าทั้งหมด', + totalCategories: 'จำนวนหมวดหมู่ทั้งหมด', + searchPlaceholder: 'ค้นหาชื่อสินค้า/รุ่น/แบรนด์', + downloadTemplate: 'ดาวน์โหลดเทมเพลต', + batchUpload: 'อัปโหลดแบบกลุ่ม', + addProduct: 'เพิ่มสินค้าใหม่', + productList: 'รายการสินค้า', + filterByCategory: 'กรองตามหมวดหมู่: ', + selectCategory: 'เลือกหมวดหมู่', + clearFilter: 'ล้างตัวกรอง', + totalRecords: 'ทั้งหมด {total} รายการ', + categoryManagement: 'การจัดการหมวดหมู่', + addCategory: 'เพิ่มหมวดหมู่', + noCategory: 'ยังไม่มีหมวดหมู่', + editProduct: 'แก้ไขสินค้า', + addProductTitle: 'เพิ่มสินค้าใหม่', + nameRequired: 'กรุณากรอกชื่อสินค้า', + namePlaceholder: 'เช่น: JKLYJ-120-22kV สายฉนวนแรงสูง', + modelPlaceholder: 'เช่น: JKLYJ-120-22kV', + selectLevel1: 'เลือกหมวดหมู่ระดับ 1', + level1Required: 'กรุณาเลือกหมวดหมู่ระดับ 1', + selectLevel2: 'เลือกหมวดหมู่ระดับ 2 (ไม่บังคับ)', + level2Extra: 'ไม่บังคับ ถ้าไม่เลือกจะใช้หมวดหมู่ระดับ 1', + selectUnit: 'เลือกหน่วย', + costPricePlaceholder: 'ค่าเริ่มต้นเป็น 0', + source: 'แหล่งที่มา', + selectSource: 'เลือกแหล่งที่มา', + china: 'จีน', + laos: 'ลาว', + brandPlaceholder: 'ชื่อแบรนด์', + specs: 'พารามิเตอร์ข้อมูลจำเพาะ', + specsPlaceholder: 'เช่น: 120mm², 22kV', + remarkPlaceholder: 'คำอธิบายอื่นๆ', + batchUploadTitle: 'อัปโหลดสินค้าแบบกลุ่ม', + uploadInstructions: 'คำแนะนำการอัปโหลด: ', + uploadStep1: 'กรุณาดาวน์โหลดไฟล์เทมเพลตก่อน กรอกข้อมูลสินค้าตามรูปแบบเทมเพลต', + uploadStep2: 'รองรับไฟล์ Excel รูปแบบ .xlsx และ .xls', + uploadStep3: 'ชื่อสินค้าและหมวดหมู่ระดับ 1 เป็นฟิลด์ที่จำเป็น', + uploadStep4: 'ฟิลด์อื่นๆ เป็นไม่บังคับ สามารถกรอกตามสถานการณ์จริง', + uploadStep5: 'ฟิลด์แหล่งที่มาค่าเริ่มต้นเป็นลาว สามารถเลือกกรอกจีน/ลาว', + uploading: 'กำลังอัปโหลด...', + selectExcel: 'เลือกไฟล์ Excel', + downloadImportTemplate: 'ดาวน์โหลดเทมเพลตนำเข้า', + editCategory: 'แก้ไขหมวดหมู่', + addCategoryTitle: 'เพิ่มหมวดหมู่', + categoryName: 'ชื่อหมวดหมู่', + categoryNameRequired: 'กรุณากรอกชื่อหมวดหมู่', + categoryNamePlaceholder: 'เช่น: สายไฟและสายเคเบิล', + categoryLevel: 'ระดับหมวดหมู่', + selectLevel: 'เลือกระดับหมวดหมู่', + levelRequired: 'กรุณาเลือกระดับหมวดหมู่', + parentCategory: 'หมวดหมู่แม่', + selectParent: 'เลือกหมวดหมู่แม่ (ไม่บังคับ)', + parentCategoryExtra: 'เมื่อเลือกหมวดหมู่ระดับ 2 ต้องเลือกหมวดหมู่แม่', + getListFailed: 'ดึงรายการสินค้าล้มเหลว', + categoryUpdateSuccess: 'อัปเดตหมวดหมู่สำเร็จ', + categoryCreateSuccess: 'สร้างหมวดหมู่สำเร็จ', + categoryDeleteSuccess: 'ลบหมวดหมู่สำเร็จ', + title: 'จัดการสินค้า', + code: 'รหัสสินค้า', + codePlaceholder: 'กรอกรหัสสินค้า', + codeRequired: 'กรุณากรอกรหัสสินค้า', + name: 'ชื่อสินค้า', + category: 'หมวดหมู่', + categoryRequired: 'กรุณาเลือกหมวดหมู่', + spec: 'ข้อมูลจำเพาะ', + specPlaceholder: 'กรอกข้อมูลจำเพาะ', + unitRequired: 'กรุณาเลือกหน่วย', + safetyStock: 'สต็อกความปลอดภัย', + safetyStockPlaceholder: 'กรอกสต็อกความปลอดภัย', + safetyStockRequired: 'กรุณากรอกสต็อกความปลอดภัย', + remark: 'หมายเหตุ', + stock: 'สต็อก', + description: 'คำอธิบาย', + createdAt: 'วันที่สร้าง', + piece: 'ชิ้น', + meter: 'เมตร', + kilometer: 'กิโลเมตร', + ton: 'ตัน', + pole2: 'เสา', + set: 'ชุด', + unit2: 'เครื่อง', + detailTitle: 'รายละเอียดสินค้า', + getDetailFailed: 'ดึงข้อมูลรายละเอียดสินค้าล้มเหลว', + deleteSuccess: 'ลบสำเร็จ', + deleteFailed: 'ลบล้มเหลว', + }, + + inventory: { + stockIn: 'รับเข้า', + stockOut: 'จ่ายออก', + recordType: 'ประเภทบันทึก', + product: 'สินค้า', + project: 'โครงการ', + quantity: 'จำนวน', + unitPrice: 'ราคาต่อหน่วย', + totalAmount: 'จำนวนเงินรวม', + recordDate: 'วันที่บันทึก', + operator: 'ผู้ดำเนินการ', + remark: 'หมายเหตุ', + unitLabel: 'หน่วย', + totalStockIn: 'จำนวนรับเข้าทั้งหมด', + totalStockOut: 'จำนวนจ่ายออกทั้งหมด', + currentStock: 'คลังปัจจุบัน', + stockRecord: 'บันทึกคลังสินค้า', + stockSummary: 'สรุปคลังสินค้า', + selectProduct: 'เลือกสินค้าเพื่อกรอง', + selectProject: 'เลือกโครงการเพื่อกรอง', + selectRecordType: 'เลือกประเภทบันทึก', + stockOutBtn: 'จ่ายออก', + stockOutTitle: 'จ่ายสินค้าออก', + relatedProject: 'โครงการที่เกี่ยวข้อง', + selectProjectRequired: 'กรุณาเลือกโครงการ', + selectProductRequired: 'กรุณาเลือกสินค้า', + stockOutQuantity: 'จำนวนจ่ายออก', + quantityRequired: 'กรุณากรอกจำนวนจ่ายออก', + inputUnitPrice: 'กรุณากรอกราคาต่อหน่วย', + inputTotalAmount: 'กรุณากรอกจำนวนเงินรวม', + inputRemark: 'กรุณากรอกหมายเหตุ', + stockOutSuccess: 'จ่ายออกสำเร็จ', + stockOutFailed: 'จ่ายออกล้มเหลว', + getListFailed: 'ดึงบันทึกคลังสินค้าล้มเหลว', + title: 'จัดการคลังสินค้า', + description: 'จัดการสินค้าคงคลังและบันทึกเข้า-ออก', + productCode: 'รหัสสินค้า', + productName: 'ชื่อสินค้า', + spec: 'ข้อมูลจำเพาะ', + unit: 'หน่วย', + stock: 'สต็อก', + safetyStock: 'สต็อกความปลอดภัย', + locked: 'ล็อก', + lastIn: 'เข้าล่าสุด', + lastOut: 'ออกล่าสุด', + status: 'สถานะ', + normal: 'ปกติ', + lowStock: 'สต็อกต่ำ', + outOfStock: 'สินค้าหมด', + tabInventory: 'คลังสินค้า', + tabLog: 'บันทึกเข้า-ออก', + categoryFilter: 'กรองตามหมวดหมู่', + stockFilter: 'กรองสต็อก', + all: 'ทั้งหมด', + time: 'เวลา', + type: 'ประเภท', + in: 'เข้า', + out: 'ออก', + before: 'ก่อนเปลี่ยน', + after: 'หลังเปลี่ยน', + orderCode: 'คำสั่งที่เกี่ยวข้อง', + relatedOrder: 'คำสั่งที่เกี่ยวข้อง', + relatedOrderPlaceholder: 'กรอกรหัสคำสั่งที่เกี่ยวข้อง', + date: 'วันที่', + dateRequired: 'กรุณาเลือกวันที่', + remarkPlaceholder: 'กรอกหมายเหตุ', + quantityPlaceholder: 'กรอกจำนวน', + stockInTitle: 'รับสินค้าเข้า', + stockInSuccess: 'รับสินค้าเข้าสำเร็จ', + stockInFailed: 'รับสินค้าเข้าล้มเหลว', + getLogFailed: 'ดึงข้อมูลบันทึกล้มเหลว', + }, + + supplier: { + title: 'รายการซัพพลายเออร์', + totalCount: 'จำนวนซัพพลายเออร์ทั้งหมด', + totalPurchase: 'ยอดจัดซื้อทั้งหมด', + totalPayable: 'ยอดเจ้าหนี้ทั้งหมด', + searchPlaceholder: 'ค้นหาเลขที่ ชื่อ หรือหมวดหมู่ซัพพลายเออร์', + newSupplier: 'เพิ่มซัพพลายเออร์ใหม่', + editSupplier: 'แก้ไขซัพพลายเออร์', + name: 'ชื่อ', + supplyCategory: 'หมวดหมู่การจัดหา', + country: 'ประเทศ', + purchaseAmount: 'ยอดจัดซื้อ', + payableAmount: 'ยอดเจ้าหนี้', + action: 'ดำเนินการ', + contact: 'ผู้ติดต่อ', + mainContact: 'ผู้ติดต่อหลัก', + paymentInfo: 'ข้อมูลการรับเงิน', + addContact: '+ เพิ่มผู้ติดต่อ', + addPaymentInfo: '+ เพิ่มข้อมูลการรับเงิน', + accountName: 'ชื่อบัญชีผู้รับเงิน', + bankName: 'ธนาคารที่เปิดบัญชี', + bankAccount: 'เลขที่บัญชีธนาคาร', + qrCode: 'รหัส QR รับเงิน', + mainAccount: 'บัญชีรับเงินหลัก', + deleteContact: 'ลบ', + deletePaymentInfo: 'ลบข้อมูลการรับเงินนี้', + nameRequired: 'กรุณากรอกชื่อ', + namePlaceholder: 'ชื่อซัพพลายเออร์', + categoryPlaceholder: 'กรอกเอง: เช่น อุปกรณ์ไฟฟ้า วัสดุก่อสร้าง', + china: 'จีน', + laos: 'ลาว', + remarkPlaceholder: 'หมายเหตุ', + getListFailed: 'ดึงรายการซัพพลายเออร์ล้มเหลว', + confirmDeleteMsg: 'ต้องการลบซัพพลายเออร์นี้หรือไม่?', + notFound: 'ไม่พบซัพพลายเออร์', + basicInfo: 'ข้อมูลพื้นฐาน', + code: 'เลขที่', + remarkLabel: 'หมายเหตุ: ', + returnToList: 'กลับสู่รายการ', + ledger: 'สมุดบัญชีธุรกิจ', + phoneLabel: 'โทรศัพท์: ', + positionLabel: 'ตำแหน่ง: ', + qrCodeLabel: 'รหัส QR รับเงิน: ', + accountNameLabel: 'ชื่อบัญชี: ', + accountNumLabel: 'เลขที่บัญชี: ', + bankLabel: 'ธนาคารผู้รับเงิน: ', + notFoundTitle: 'ไม่พบซัพพลายเออร์', + noContact: 'ยังไม่มีผู้ติดต่อ', + noPayment: 'ยังไม่มีข้อมูลการรับเงิน', + mainContactTag: 'ผู้ติดต่อหลัก', + mainAccountTag: 'บัญชีรับเงินหลัก', + }, + + subcontractor: { + title: 'รายการผู้รับเหมาช่วง', + totalCount: 'จำนวนผู้รับเหมาช่วงทั้งหมด', + totalContract: 'มูลค่าสัญญาทั้งหมด', + totalPayable: 'ยอดเจ้าหนี้ทั้งหมด', + searchPlaceholder: 'ค้นหาเลขที่ ชื่อ หรือขอบเขตงานรับเหมา', + newSubcontractor: 'เพิ่มผู้รับเหมาช่วงใหม่', + editSubcontractor: 'แก้ไขผู้รับเหมาช่วง', + name: 'ชื่อ', + scope: 'ขอบเขตงานรับเหมา', + country: 'ประเทศ', + contractAmount: 'มูลค่าสัญญา', + payableAmount: 'ยอดเจ้าหนี้', + action: 'ดำเนินการ', + contact: 'ผู้ติดต่อ', + mainContact: 'ผู้ติดต่อหลัก', + paymentInfo: 'ข้อมูลการรับเงิน', + addContact: '+ เพิ่มผู้ติดต่อ', + addPaymentInfo: '+ เพิ่มข้อมูลการรับเงิน', + accountName: 'ชื่อบัญชีผู้รับเงิน', + bankName: 'ธนาคารที่เปิดบัญชี', + bankAccount: 'เลขที่บัญชีธนาคาร', + qrCode: 'รหัส QR รับเงิน', + mainAccount: 'บัญชีรับเงินหลัก', + deleteContact: 'ลบ', + deletePaymentInfo: 'ลบข้อมูลการรับเงินนี้', + nameRequired: 'กรุณากรอกชื่อ', + namePlaceholder: 'ชื่อผู้รับเหมาช่วง', + scopePlaceholder: 'กรอกเอง: เช่น งานติดตั้งไฟฟ้า งานวิศวกรรมโยธา', + china: 'จีน', + laos: 'ลาว', + feature: 'คุณลักษณะ', + featurePlaceholder: 'กรอกเอง: เช่น ทีมงานมืออาชีพ อุปกรณ์ครบครัน ราคาสมเหตุสมผล เป็นต้น', + remarkPlaceholder: 'หมายเหตุ', + getListFailed: 'ดึงรายการผู้รับเหมาช่วงล้มเหลว', + confirmDeleteMsg: 'ต้องการลบผู้รับเหมาช่วงนี้หรือไม่?', + notFound: 'ไม่พบผู้รับเหมาช่วง', + basicInfo: 'ข้อมูลพื้นฐาน', + code: 'เลขที่', + featureLabel: 'คุณลักษณะ: ', + remarkLabel: 'หมายเหตุ: ', + returnToList: 'กลับสู่รายการ', + ledger: 'สมุดบัญชีธุรกิจ', + phoneLabel: 'โทรศัพท์: ', + positionLabel: 'ตำแหน่ง: ', + bankLabel: 'ธนาคาร: ', + accountLabel: 'เลขที่บัญชี: ', + qrCodeLabel: 'รหัส QR: ', + defaultAccount: 'บัญชีค่าเริ่มต้น', + notFoundTitle: 'ไม่พบผู้รับเหมาช่วง', + noContact: 'ยังไม่มีผู้ติดต่อ', + noPayment: 'ยังไม่มีข้อมูลการรับเงิน', + mainContactTag: 'ผู้ติดต่อหลัก', + }, + + customer: { + title: 'รายการลูกค้า', + totalCount: 'จำนวนลูกค้าทั้งหมด', + totalContract: 'มูลค่าสัญญาทั้งหมด', + totalReceivable: 'ยอดลูกหนี้ทั้งหมด', + searchPlaceholder: 'ค้นหาเลขที่ ชื่อ หรือที่อยู่ลูกค้า', + newCustomer: 'เพิ่มลูกค้าใหม่', + editCustomer: 'แก้ไขลูกค้า', + name: 'ชื่อ', + address: 'ที่อยู่', + mainContact: 'ผู้ติดต่อหลัก', + paymentInfo: 'ข้อมูลการรับเงิน', + contractAmount: 'มูลค่าสัญญา', + receivableAmount: 'ยอดลูกหนี้', + action: 'ดำเนินการ', + contact: 'ผู้ติดต่อ', + addContact: '+ เพิ่มผู้ติดต่อ', + addPaymentInfo: '+ เพิ่มข้อมูลการรับเงิน', + accountName: 'ชื่อบัญชีผู้รับเงิน', + bankName: 'ธนาคารที่เปิดบัญชี', + bankAccount: 'เลขที่บัญชีธนาคาร', + qrCode: 'รหัส QR รับเงิน', + mainAccount: 'บัญชีรับเงินหลัก', + deleteContact: 'ลบ', + deletePaymentInfo: 'ลบข้อมูลการรับเงินนี้', + nameRequired: 'กรุณากรอกชื่อ', + namePlaceholder: 'ชื่อลูกค้า', + addressPlaceholder: 'ที่อยู่ลูกค้า', + remarkPlaceholder: 'หมายเหตุ', + getListFailed: 'ดึงรายการลูกค้าล้มเหลว', + confirmDeleteMsg: 'ต้องการลบลูกค้านี้หรือไม่?', + notFound: 'ไม่พบลูกค้า', + basicInfo: 'ข้อมูลพื้นฐาน', + code: 'เลขที่', + remarkLabel: 'หมายเหตุ: ', + returnToList: 'กลับสู่รายการ', + ledger: 'สมุดบัญชีธุรกิจ', + relatedBudget: 'งบประมาณที่เกี่ยวข้อง', + projectName: 'ชื่อโครงการ', + businessManager: 'ผู้จัดการฝ่ายธุรกิจ', + inNegotiation: 'กำลังเจรจา', + signed: 'ลงนามแล้ว', + unsigned: 'ยังไม่ลงนาม', + quotationCount: 'จำนวนเวอร์ชันใบเสนอราคา', + createdAt: 'เวลาที่สร้าง', + noBudget: 'ยังไม่มีโครงการงบประมาณที่เกี่ยวข้อง', + phoneLabel: 'โทรศัพท์: ', + positionLabel: 'ตำแหน่ง: ', + accountNameLabel: 'ชื่อบัญชี:', + accountNumLabel: 'เลขที่บัญชี:', + mainContactTag: 'ผู้ติดต่อหลัก', + noContact: 'ยังไม่มีผู้ติดต่อ', + noPayment: 'ยังไม่มีข้อมูลการรับเงิน', + }, + + logistics: { + title: 'จัดการโลจิสติกส์', + description: 'จัดการพันธมิตรด้านโลจิสติกส์ (ตามมาตรฐานอินเทอร์เฟซพันธมิตร)', + newCompany: 'สร้างบริษัทโลจิสติกส์ใหม่', + editCompany: 'แก้ไขบริษัทโลจิสติกส์', + companyName: 'ชื่อบริษัท', + phone: 'เบอร์ติดต่อ', + quoteDescription: 'คำอธิบายใบเสนอราคา', + createdAt: 'เวลาที่สร้าง', + action: 'ดำเนินการ', + confirmDelete: 'ต้องการลบหรือไม่?', + contact: 'ผู้ติดต่อ', + name: 'ชื่อ', + position: 'ตำแหน่ง', + phoneLabel: 'โทรศัพท์', + mainContact: 'ผู้ติดต่อหลัก', + confirmDeleteShort: 'ต้องการลบ?', + paymentInfo: 'ข้อมูลการรับเงิน', + accountName: 'ชื่อบัญชีผู้รับเงิน', + bankAccount: 'เลขที่บัญชีธนาคาร', + bankName: 'ธนาคารที่เปิดบัญชี', + defaultAccount: 'ค่าเริ่มต้น', + defaultLabel: 'ค่าเริ่มต้น', + orders: 'ใบสั่ง', + trackingNumber: 'เลขที่ติดตามพัสดุ', + purchaseOrder: 'ใบสั่งซื้อ', + deliveryDate: 'วันที่จัดส่ง', + freight1: 'ค่าขนส่งครั้งที่ 1', + freight1Status: 'สถานะค่าขนส่งครั้งที่ 1', + freight2: 'ค่าขนส่งครั้งที่ 2', + freight2Status: 'สถานะค่าขนส่งครั้งที่ 2', + status: 'สถานะ', + pendingPayment: 'รอชำระเงิน', + applied: 'ยื่นคำขอแล้ว', + paid: 'ชำระแล้ว', + addContact: 'เพิ่มผู้ติดต่อ', + addPaymentInfo: 'เพิ่มข้อมูลการรับเงิน', + basicInfo: 'ข้อมูลพื้นฐาน', + email: 'อีเมล', + remark: 'หมายเหตุ', + paymentTab: 'ข้อมูลการรับเงิน', + ledger: 'สมุดบัญชีธุรกิจ', + editContact: 'แก้ไขผู้ติดต่อ', + addContactTitle: 'เพิ่มผู้ติดต่อ', + editPaymentInfo: 'แก้ไขข้อมูลการรับเงิน', + addPaymentInfoTitle: 'เพิ่มข้อมูลการรับเงิน', + nameRequired: 'กรุณากรอกชื่อ', + positionRequired: 'กรุณากรอกตำแหน่ง', + phoneRequired: 'กรุณากรอกโทรศัพท์', + accountNameRequired: 'กรุณากรอกชื่อบัญชีผู้รับเงิน', + bankAccountRequired: 'กรุณากรอกเลขที่บัญชีธนาคาร', + bankNameRequired: 'กรุณากรอกธนาคารที่เปิดบัญชี', + qrCodeRequired: 'กรุณากรอก URL รูป QR Code รับเงิน', + isMainContact: 'เป็นผู้ติดต่อหลักหรือไม่', + isDefaultAccount: 'เป็นบัญชีค่าเริ่มต้นหรือไม่', + address: 'ที่อยู่', + addressPlaceholder: 'กรุณากรอกที่อยู่', + quotePlaceholder: 'กรุณากรอกคำอธิบายใบเสนอราคา (เช่น: ราคาขนส่งทางบกจีน-ลาว ระยะเวลา ฯลฯ)', + remarkPlaceholder: 'กรุณากรอกหมายเหตุ', + getListFailed: 'ดึงรายการบริษัทโลจิสติกส์ล้มเหลว', + getDetailFailed: 'ดึงรายละเอียดบริษัทโลจิสติกส์ล้มเหลว', + contactUpdateSuccess: 'อัปเดตผู้ติดต่อสำเร็จ', + contactAddSuccess: 'เพิ่มผู้ติดต่อสำเร็จ', + contactDeleteSuccess: 'ลบผู้ติดต่อสำเร็จ', + paymentInfoUpdateSuccess: 'อัปเดตข้อมูลการรับเงินสำเร็จ', + paymentInfoAddSuccess: 'เพิ่มข้อมูลการรับเงินสำเร็จ', + paymentInfoDeleteSuccess: 'ลบข้อมูลการรับเงินสำเร็จ', + detailTitle: 'รายละเอียดบริษัทโลจิสติกส์ - {name}', + qrCode: 'คิวอาร์โค้ด', + }, + + businessLedger: { + contractTotal: 'มูลค่าสัญญาทั้งหมด', + totalPaid: 'ชำระแล้วทั้งหมด', + totalUnpaid: 'ค้างชำระทั้งหมด', + projectCount: 'จำนวนโครงการ', + purchaseTotal: 'ยอดจัดซื้อทั้งหมด', + totalReceived: 'รับแล้วทั้งหมด', + totalReceivable: 'ยอดลูกหนี้ทั้งหมด', + orderCount: 'จำนวนใบสั่ง', + freight1Total: 'ค่าขนส่งครั้งที่ 1 ทั้งหมด', + logisticsCount: 'จำนวนรายการโลจิสติกส์', + code: 'เลขที่', + name: 'ชื่อ', + contractAmount: 'มูลค่าสัญญา', + status: 'สถานะ', + purchaseAmount: 'ยอดจัดซื้อ', + project: 'โครงการ', + freight1: 'ค่าขนส่งครั้งที่ 1', + freight1Status: 'สถานะค่าขนส่งครั้งที่ 1', + completed: 'เสร็จสมบูรณ์', + inProgress: 'กำลังดำเนินการ', + planning: 'กำลังวางแผน', + pending: 'รอดำเนินการ', + approved: 'อนุมัติแล้ว', + paid: 'ชำระแล้ว', + applied: 'ยื่นคำขอแล้ว', + noRecord: 'ยังไม่มีบันทึกธุรกิจ', + }, + + exchangeRate: { + title: 'จัดการอัตราแลกเปลี่ยน', + description: 'ตั้งค่าอัตราแลกเปลี่ยนแต่ละสกุลเงิน กรอกด้านใดด้านหนึ่งจะคำนวณอีกด้านอัตโนมัติ', + CNYLAK: 'อัตราจีน-ลาว', + CNY: 'หยวน (CNY)', + LAK: 'กีบ (LAK)', + CNYUSD: 'อัตราจีน-สหรัฐฯ', + USD: 'ดอลลาร์ (USD)', + CNYTHB: 'อัตราจีน-ไทย', + THB: 'บาท (THB)', + USDLAK: 'อัตราสหรัฐฯ-ลาว', + THBLAK: 'อัตราไทย-ลาว', + ratePair: 'คู่อัตรา', + rate: 'อัตรา', + effectiveDate: 'วันที่มีผล', + setTime: 'เวลาที่ตั้งค่า', + setBy: 'ผู้ตั้งค่า', + lastUpdated: 'อัปเดตล่าสุด: ', + actualRate: 'อัตราจริง: ', + confirmSave: 'ยืนยันการบันทึกอัตรา', + historyRate: 'บันทึกอัตราย้อนหลัง', + tipText: 'คำแนะนำ: กรอกจำนวนเงินด้านใดด้านหนึ่ง อีกด้านจะคำนวณอัตโนมัติ อัตราจริงแสดงแบบเรียลไทม์เป็น 1 สกุลเงินซ้าย = X สกุลเงินขวา คลิก "ยืนยันการบันทึกอัตรา" เพื่อบันทึกการตั้งค่าปัจจุบันลงฐานข้อมูล', + getRateFailed: 'ดึงอัตราล้มเหลว', + noChange: 'ไม่มีอัตราใดเปลี่ยนแปลง', + saveSuccess: 'บันทึกอัตราสำเร็จ', + saveFailed: 'บันทึกอัตราล้มเหลว', + inputFrom: 'กรอกจำนวนเงิน{from}', + inputTo: 'กรอกจำนวนเงิน{to}', + }, + + projectCost: { + title: 'ต้นทุนโครงการ', + selectProject: 'กรุณาเลือกโครงการเพื่อดูสถิติต้นทุน', + contractAmount: 'มูลค่าสัญญา', + purchaseCost: 'ต้นทุนจัดซื้อ', + paymentExpense: 'รายจ่ายการชำระเงิน', + totalIncome: 'รายรับรวม', + totalExpense: 'รายจ่ายรวม', + profit: 'กำไร', + profitRate: 'อัตรากำไร', + totalCostBreakdown: 'องค์ประกอบต้นทุนรวม', + totalCost: 'ต้นทุนรวม', + costProgress: 'ความคืบหน้าต้นทุน', + costRatio: 'สัดส่วนต้นทุน/สัญญา', + purchaseCategoryBreakdown: 'หมวดหมู่ต้นทุนจัดซื้อ', + incomeBreakdown: 'รายละเอียดรายรับ', + expenseBreakdown: 'รายละเอียดรายจ่าย', + expenseByLevel1: 'สรุปรายจ่ายตามหมวดหมู่', + overviewTab: 'ภาพรวม', + detailsTab: 'รายการธุรกรรม', + detail: 'รายละเอียด', + material: 'วัสดุ', + equipment: 'อุปกรณ์', + pole: 'เสาไฟฟ้า', + other: 'อื่นๆ', + noData: 'ยังไม่มีข้อมูล', + getDataFailed: 'ดึงข้อมูลสถิติต้นทุนล้มเหลว', + }, + + systemLogs: { + title: 'บันทึกระบบ', + description: 'ดูบันทึกการดำเนินการระบบและบันทึกการตรวจสอบ', + logId: 'รหัสบันทึก', + time: 'เวลา', + level: 'ระดับ', + module: 'โมดูล', + operator: 'ผู้ดำเนินการ', + operation: 'การดำเนินการ', + ipAddress: 'ที่อยู่ IP', + detail: 'รายละเอียด', + logLevel: 'ระดับบันทึก', + selectModule: 'โมดูล', + export: 'ส่งออก', + clear: 'ล้าง', + searchPlaceholder: 'ค้นหาเนื้อหาบันทึก', + moduleUser: 'จัดการผู้ใช้', + moduleProject: 'จัดการโครงการ', + moduleFinance: 'จัดการการเงิน', + moduleSystem: 'ระบบ', + login: 'ผู้ใช้เข้าสู่ระบบ', + createProject: 'สร้างโครงการ', + approveAdvance: 'อนุมัติการเบิกล่วงหน้า', + dataBackup: 'สำรองข้อมูล', + }, + + about: { + title: 'เกี่ยวกับระบบ', + description: 'ข้อมูลระบบและรุ่น', + systemInfo: 'ข้อมูลระบบ', + systemName: 'ชื่อระบบ', + systemNameValue: 'ชิงหยวนพาวเวอร์ สปป.ลาว ERP', + version: 'รุ่นระบบ', + versionValue: 'V1.0.0', + devTeam: 'ทีมพัฒนา', + devTeamValue: 'ฝ่ายเทคโนโลยีสารสนเทศ ชิงหยวนพาวเวอร์', + onlineDate: 'วันที่เปิดใช้งาน', + onlineDateValue: 'มีนาคม 2026', + techArchitecture: 'สถาปัตยกรรมทางเทคนิค', + deployEnv: 'สภาพแวดล้อมการติดตั้ง', + deployEnvValue: 'เซิร์ฟเวอร์ Tencent Cloud', + frontend: 'เฟรมเวิร์กส่วนหน้า', + frontendValue: 'Vite + React + TypeScript', + backend: 'เฟรมเวิร์กส่วนหลัง', + backendValue: 'Express.js + PostgreSQL', + modules: 'โมดูลฟังก์ชัน', + serverStatus: 'สถานะเซิร์ฟเวอร์', + databaseStatus: 'สถานะฐานข้อมูล', + cpuUsage: 'อัตราการใช้ CPU', + memoryUsage: 'การใช้หน่วยความจำ', + diskSpace: 'พื้นที่ดิสก์', + serverIp: 'IP เซิร์ฟเวอร์', + os: 'ระบบปฏิบัติการ', + osValue: 'OpenCloudOS 9', + nodeVersion: 'รุ่น Node', + running: 'ทำงานปกติ', + dbName: 'ชื่อฐานข้อมูล', + connectionStatus: 'สถานะการเชื่อมต่อ', + normal: 'ปกติ', + lastBackup: 'สำรองข้อมูลล่าสุด', + footer: '© 2026 ระบบ ERP ชิงหยวนพาวเวอร์ สปป.ลาว - รุ่น V1.0.0', + }, + + backup: { + title: 'สำรองข้อมูล', + description: 'จัดการการสำรองและกู้คืนข้อมูลระบบ', + backupName: 'ชื่อการสำรอง', + backupTime: 'เวลาสำรอง', + fileSize: 'ขนาดไฟล์', + backupType: 'ประเภทการสำรอง', + auto: 'อัตโนมัติ', + manual: 'ด้วยตนเอง', + status: 'สถานะ', + success: 'สำเร็จ', + failed: 'ล้มเหลว', + action: 'ดำเนินการ', + download: 'ดาวน์โหลด', + restore: 'กู้คืน', + delete: 'ลบ', + totalBackups: 'จำนวนการสำรองทั้งหมด', + totalSize: 'ขนาดรวม', + lastBackup: 'สำรองข้อมูลล่าสุด', + storageSpace: 'พื้นที่จัดเก็บ', + backupList: 'รายการสำรองข้อมูล', + autoBackupSetting: 'ตั้งค่าการสำรองอัตโนมัติ', + immediateBackup: 'สำรองข้อมูลทันที', + backupCreated: 'สร้างการสำรองข้อมูลสำเร็จ', + }, + + processManagement: { + title: 'การจัดการกระบวนการ', + description: 'กำหนดค่าจุดอนุมัติกระบวนการคำขอทางการเงิน รองรับการกำหนดบทบาทผู้ดำเนินการเอง', + flowchart: 'แผนผังกระบวนการปัจจุบัน', + nodeConfig: 'การกำหนดค่าจุด', + applicableProcess: 'กระบวนการที่ใช้ได้', + processType: 'ประเภทกระบวนการ', + desc: 'คำอธิบาย', + status: 'สถานะ', + enabled: 'เปิดใช้งานแล้ว', + disabled: 'ปิดใช้งานแล้ว', + sequence: 'ลำดับ', + nodeName: 'ชื่อจุด', + executeRole: 'บทบาทผู้ดำเนินการ', + action: 'ดำเนินการ', + edit: 'แก้ไข', + editNode: 'แก้ไขจุด: {name}', + roleApplicant: 'ผู้ยื่นคำขอ (บทบาทใดก็ได้)', + roleAdmin: 'ผู้ดูแลระบบ', + roleFinance: 'เจ้าหน้าที่การเงิน', + roleManager: 'ผู้จัดการโครงการ', + submitApplication: 'ยื่นคำขอ', + approvalNode: 'อนุมัติ', + executePayment: 'ดำเนินการชำระเงิน', + advanceProcess: 'คำขอเบิกล่วงหน้า', + advanceProcessDesc: 'กระบวนการขอเบิกเงินล่วงหน้าของพนักงาน', + reimburseProcess: 'คำขอเบิกค่าใช้จ่าย', + reimburseProcessDesc: 'กระบวนการขอเบิกค่าใช้จ่าย', + paymentProcess: 'คำขอชำระเงิน', + paymentProcessDesc: 'กระบวนการขอชำระเงินซัพพลายเออร์', + verificationProcess: 'คำขอตรวจสอบ', + verificationProcessDesc: 'กระบวนการขอตรวจสอบเอกสาร', + nodeSaved: 'บันทึกการกำหนดค่าจุดแล้ว', + selectRole: 'เลือกบทบาทผู้ดำเนินการ', + selectRolePlaceholder: 'กรุณาเลือกบทบาทผู้ดำเนินการ', + warning: '⚠️ การแก้ไขบทบาทผู้ดำเนินการจะมีผลต่อคำขอทั้งหมดที่ใช้กระบวนการนี้ แนะนำให้เปลี่ยนจุดดำเนินการเป็นบทบาทการเงินหลังจากบริษัทมีเจ้าหน้าที่การเงินแล้ว', + tipTitle: 'คำอธิบาย: ', + tipContent: 'กระบวนการปัจจุบันคือ «ผู้ยื่นคำขอ → ผู้ดูแลระบบอนุมัติ → ผู้ดูแลระบบดำเนินการ» สามารถแก้ไขบทบาทผู้ดำเนินการเป็นเจ้าหน้าที่การเงินด้านล่าง', + }, + + processTemplate: { + title: 'จัดการเทมเพลตโครงการ', + basicInfo: 'ข้อมูลพื้นฐาน', + designPhase: 'ขั้นตอนการออกแบบ', + preview: 'ดูตัวอย่างยืนยัน', + templateName: 'ชื่อเทมเพลต', + templateDescription: 'คำอธิบายเทมเพลต', + namePlaceholder: 'เช่น: โครงการติดตั้งระบบจำหน่ายไฟฟ้า', + descPlaceholder: 'อธิบายประเภทโครงการที่เทมเพลตนี้ใช้', + newTemplate: 'สร้างเทมเพลตใหม่', + editTemplate: 'แก้ไขเทมเพลตโครงการ', + phaseCount: 'จำนวนขั้นตอน', + desc: 'คำอธิบาย', + action: 'ดำเนินการ', + copy: 'คัดลอก', + delete: 'ลบ', + confirmDelete: 'ต้องการลบ?', + phaseName: 'ชื่อขั้นตอน', + phaseNamePlaceholder: 'เช่น: จัดซื้อวัสดุ', + phaseType: 'ประเภทขั้นตอน', + serial: 'แบบลำดับ (ต้องรอให้รายการที่ต้องพึ่งพาเสร็จ)', + parallel: 'แบบคู่ขนาน (สามารถดำเนินการพร้อมกับขั้นตอนใกล้เคียงได้)', + dependency: 'ความสัมพันธ์การพึ่งพา (ขั้นตอนใดต้องเสร็จก่อนจึงเริ่มได้)', + subItems: 'รายการย่อย (หนึ่งรายการต่อบรรทัด)', + subItemsPlaceholder: 'จัดซื้อเสาไฟฟ้า\nจัดซื้อหม้อแปลง\nจัดซื้อสายเคเบิล', + dependencyLabel: 'การพึ่งพา: ', + emptySubItems: 'ไม่มีรายการย่อย', + noPhase: 'ยังไม่มีขั้นตอน กรุณาคลิกด้านล่างเพื่อเพิ่ม', + addPhase: 'เพิ่มขั้นตอน', + saveEdit: 'บันทึกการแก้ไข', + confirmCreate: 'ยืนยันการสร้าง', + save: 'บันทึก', + cancel: 'ยกเลิก', + prev: 'ก่อนหน้า', + next: 'ถัดไป', + getListFailed: 'ดึงรายการเทมเพลตล้มเหลว', + copySuccess: 'คัดลอกสำเร็จ', + copyFailed: 'คัดลอกล้มเหลว', + deleteSuccess: 'ลบสำเร็จ', + deleteFailed: 'ลบล้มเหลว', + nameRequired: 'กรุณากรอกชื่อเทมเพลต', + phaseRequired: 'กรุณาเพิ่มอย่างน้อยหนึ่งขั้นตอน', + updateSuccess: 'อัปเดตเทมเพลตสำเร็จ', + createSuccess: 'สร้างเทมเพลตสำเร็จ', + phaseNameEmpty: 'ชื่อขั้นตอนต้องไม่ว่างเปล่า', + systemPreset: 'ค่าที่ตั้งไว้ของระบบ', + serialLabel: 'แบบลำดับ', + parallelLabel: 'แบบคู่ขนาน', + dependencyLabelShort: 'การพึ่งพา: ', + phaseEdit: 'แก้ไขขั้นตอน', + }, + + expenseCategory: { + title: 'จัดการหมวดหมู่การเงิน', + editCategory: 'แก้ไขหมวดหมู่', + addCategory: 'เพิ่มหมวดหมู่', + id: 'ID', + level1: 'หมวดหมู่ระดับ 1', + level2Code: 'รหัสระดับ 2', + displayName: 'ชื่อที่แสดง', + desc: 'คำอธิบาย', + order: 'ลำดับ', + status: 'สถานะ', + action: 'ดำเนินการ', + refresh: 'รีเฟรช', + add: 'เพิ่มหมวดหมู่', + income: 'รายรับ', + projectExpense: 'รายจ่ายโครงการ', + companyExpense: 'รายจ่ายบริษัท', + getFailed: 'ดึงหมวดหมู่ล้มเหลว', + enabled: 'เปิดใช้งานแล้ว', + disabled: 'ปิดใช้งานแล้ว', + selectLevel1: 'กรุณาเลือก', + inputLevel2: 'กรุณากรอก', + codePlaceholder: 'เช่น material, salary เป็นต้น', + namePlaceholder: 'เช่น จัดซื้อวัสดุ', + }, + + excelImport: { + title: 'นำเข้าบันทึกการเงินแบบกลุ่มด้วย Excel', + templateRequirements: 'ข้อกำหนดรูปแบบเทมเพลต Excel', + columnOrder: 'ลำดับคอลัมน์: วันที่ | ประเภทรายรับรายจ่าย | หมวดหมู่ระดับ 1 | หมวดหมู่ระดับ 2 | ชื่อโครงการ | จำนวนเงิน | สกุลเงิน | อัตราแลกเปลี่ยน | เทียบเท่าหยวน | ชื่อคู่สัญญา | ประเภทคู่สัญญา | ชื่อบุคคล | คำอธิบาย | เลขที่ใบสำคัญ', + formatRequirements: 'ประเภทรายรับรายจ่าย: รายรับ / รายจ่าย | หมวดหมู่ระดับ 1: รายรับ / รายจ่ายโครงการ / รายจ่ายบริษัท | สกุลเงิน: CNY / USD / LAK / THB', + categoryRequirements: 'หมวดหมู่ระดับ 2 ต้องใช้ชื่อหมวดหมู่ที่มีอยู่ในระบบ (เช่น: จัดซื้อวัสดุ เงินเดือน เป็นต้น)', + selectFile: 'เลือกไฟล์ Excel', + downloadTemplate: 'ดาวน์โหลดไฟล์เทมเพลต', + templateFileName: 'เทมเพลตนำเข้าบัญชีการเงิน.xlsx', + noData: 'ไฟล์ Excel ไม่มีแถวข้อมูล', + invalidDate: 'วันที่ว่างเปล่า', + invalidType: 'ประเภทรายรับรายจ่ายไม่ถูกต้อง: {type}', + invalidLevel1: 'หมวดหมู่ระดับ 1 ไม่ถูกต้อง: {level1}', + invalidLevel2: 'หมวดหมู่ระดับ 2 ไม่ถูกต้อง: {level2}', + amountPositive: 'จำนวนเงินต้องมากกว่า 0', + projectRequired: 'รายจ่ายโครงการ/รายรับโครงการต้องกรอกชื่อโครงการ', + parseComplete: 'การวิเคราะห์เสร็จสมบูรณ์ ทั้งหมด {count} รายการ', + parseFailed: 'การวิเคราะห์ Excel ล้มเหลว: ', + noValidData: 'ไม่มีข้อมูลที่ถูกต้องสำหรับการนำเข้า', + importComplete: 'การนำเข้าเสร็จสมบูรณ์: สำเร็จ {success} รายการ ล้มเหลว {fail} รายการ', + importFailed: 'การนำเข้าล้มเหลว: ', + rowNum: 'แถวที่', + date: 'วันที่', + incomeExpense: 'รายรับรายจ่าย', + income: 'รายรับ', + expense: 'รายจ่าย', + level1: 'ระดับ 1', + projectLabel: 'โครงการ', + companyLabel: 'บริษัท', + level2: 'ระดับ 2', + amount: 'จำนวนเงิน', + currency: 'สกุลเงิน', + rate: 'อัตรา', + equivalentCNY: 'เทียบเท่าหยวน', + desc: 'คำอธิบาย', + validation: 'การตรวจสอบ', + countPrefix: 'ทั้งหมด', + countSuffix: 'รายการ', + validPrefix: 'ถูกต้อง', + errorPrefix: 'มีข้อผิดพลาด', + importPrefix: 'นำเข้า', + importSuffix: 'รายการที่ถูกต้อง', + rowPrefix: 'แถวที่', + rowSuffix: ': ', + }, + + errorBoundary: { + title: 'การโหลดหน้าเกิดข้อผิดพลาด', + description: 'ขออภัย เกิดข้อผิดพลาดระหว่างการแสดงผลหน้า กรุณาลองรีเฟรชหรือติดต่อผู้ดูแลระบบ', + errorInfo: 'ข้อมูลข้อผิดพลาด: ', + stackTrace: 'การติดตามข้อผิดพลาด: ', + refresh: 'รีเฟรชหน้า', + }, + + fileUpload: { + upload: 'อัปโหลด', + uploading: 'กำลังอัปโหลด...', + preview: 'ดูตัวอย่างรูปภาพ', + uploadSuccess: 'อัปโหลดสำเร็จ', + uploadFailed: 'อัปโหลดล้มเหลว', + }, + + component: { + phonePrefix: 'โทรศัพท์: ', + wechatPrefix: 'WeChat: ', + whatsappLabel: 'WhatsApp', + whatsappPlaceholder: 'กรอกหมายเลข WhatsApp', + }, + + roles: { + title: 'สิทธิ์บทบาท', + description: 'จัดการบทบาทระบบและการกำหนดสิทธิ์', + searchRole: 'ค้นหาบทบาท', + newRole: 'เพิ่มบทบาทใหม่', + roleId: 'รหัสบทบาท', + roleName: 'ชื่อบทบาท', + roleDesc: 'คำอธิบายบทบาท', + permCount: 'จำนวนสิทธิ์', + createdAt: 'เวลาที่สร้าง', + creator: 'ผู้สร้าง', + action: 'ดำเนินการ', + viewPerm: 'ดูสิทธิ์', + edit: 'แก้ไข', + delete: 'ลบ', + superAdmin: 'ผู้ดูแลระบบสูงสุด', + superAdminDesc: 'มีสิทธิ์ทั้งหมดของระบบ', + admin: 'ระบบ', + adminDesc: 'สิทธิ์จัดการโครงการ จัดการงานก่อสร้าง', + financeManager: 'ผู้จัดการการเงิน', + financeManagerDesc: 'สิทธิ์จัดการการเงิน อนุมัติ', + employee: 'พนักงาน', + employeeDesc: 'สิทธิ์ดูและยื่นคำขอ', + roleNameRequired: 'กรุณากรอกชื่อบทบาท', + roleDescRequired: 'กรุณากรอกคำอธิบายบทบาท', + roleCreated: 'สร้างบทบาทแล้ว', + permConfig: 'การกำหนดค่าสิทธิ์', + permProject: 'จัดการโครงการ', + permViewProject: 'ดูโครงการ', + permCreateProject: 'สร้างโครงการ', + permEditProject: 'แก้ไขโครงการ', + permDeleteProject: 'ลบโครงการ', + permFinance: 'จัดการการเงิน', + permViewFinance: 'ดูการเงิน', + permApproveAdvance: 'อนุมัติการเบิกล่วงหน้า', + permApproveReimburse: 'อนุมัติการเบิกค่าใช้จ่าย', + permApprovePayment: 'อนุมัติการชำระเงิน', + permProcurement: 'จัดการจัดซื้อ', + permViewProcurement: 'ดูการจัดซื้อ', + permCreateProcurement: 'สร้างการจัดซื้อ', + permApproveProcurement: 'อนุมัติการจัดซื้อ', + permSystem: 'ตั้งค่าระบบ', + permUserManagement: 'จัดการผู้ใช้', + permRoleManagement: 'จัดการบทบาท', + permSystemConfig: 'การกำหนดค่าระบบ', + }, + + users: { + title: 'จัดการผู้ใช้', + newUser: 'เพิ่มผู้ใช้ใหม่', + editUser: 'แก้ไขผู้ใช้', + id: 'ID', + avatar: 'รูปโปรไฟล์', + username: 'ชื่อผู้ใช้', + name: 'ชื่อ-นามสกุล', + email: 'อีเมล', + phone: 'เบอร์มือถือ', + role: 'บทบาท', + user: 'ผู้ใช้', + action: 'ดำเนินการ', + edit: 'แก้ไข', + resetPassword: 'รีเซ็ตรหัสผ่าน', + confirmDelete: 'ยืนยันการลบ', + confirmDeleteMsg: 'ต้องการลบผู้ใช้ {name} หรือไม่?', + usernamePlaceholder: 'กรุณากรอกชื่อผู้ใช้', + namePlaceholder: 'กรุณากรอกชื่อ-นามสกุล', + selectRole: 'เลือกบทบาท', + selectRolePlaceholder: 'กรุณาเลือกบทบาท', + initialPassword: 'รหัสผ่านเริ่มต้น', + initialPasswordPlaceholder: 'กรุณากรอกรหัสผ่านเริ่มต้น', + passwordMinLen: 'รหัสผ่านอย่างน้อย 6 ตัวอักษร', + newPasswordPlaceholder: 'กรุณากรอกรหัสผ่านใหม่', + confirmPassword: 'ยืนยันรหัสผ่าน', + confirmPasswordPlaceholder: 'กรุณายืนยันรหัสผ่านใหม่', + reEnterPassword: 'กรุณากรอกรหัสผ่านใหม่อีกครั้ง', + passwordMismatch: 'รหัสผ่านที่กรอกทั้งสองครั้งไม่ตรงกัน', + getListFailed: 'ดึงรายการผู้ใช้ล้มเหลว', + notAdmin: 'สิทธิ์ไม่เพียงพอ เฉพาะผู้ดูแลระบบเท่านั้นที่เข้าถึงได้', + getUserFailed: 'ดึงรายการผู้ใช้ล้มเหลว', + addSuccess: 'เพิ่มผู้ใช้แล้ว', + getListFailedLog: 'ดึงรายการผู้ใช้ล้มเหลว: ', + unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ', + }, + + userManagement: { + title: 'จัดการผู้ใช้', + testPage: 'นี่คือหน้าทดสอบ สำหรับตรวจสอบว่าการเรียก API ทำงานปกติหรือไม่', + refreshList: 'รีเฟรชรายการผู้ใช้', + errorPrefix: 'ข้อผิดพลาด: ', + apiResult: 'ข้อมูลที่ส่งกลับจาก API:', + loadStatus: 'สถานะการโหลด:', + loadComplete: 'โหลดเสร็จสมบูรณ์', + apiFailed: 'API ส่งกลับล้มเหลว: ', + unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ', + }, +} \ No newline at end of file diff --git a/frontend/src/locales/zh-CN.ts b/frontend/src/locales/zh-CN.ts index cf653e9..1670ab0 100644 --- a/frontend/src/locales/zh-CN.ts +++ b/frontend/src/locales/zh-CN.ts @@ -1,69 +1,2551 @@ -export default { - // 通用 - common: { - confirm: '确认', - cancel: '取消', - save: '保存', - delete: '删除', - edit: '编辑', - add: '添加', - search: '搜索', - reset: '重置', - submit: '提交', - back: '返回', - loading: '加载中...', - success: '操作成功', - failed: '操作失败', - required: '此项为必填' - }, - - // 登录页 - login: { - title: '轻远电力老挝ERP', - subtitle: '项目管理与财务报销一体化平台', - username: '用户名', - password: '密码', - loginButton: '登录', - usernamePlaceholder: '请输入用户名', - passwordPlaceholder: '请输入密码', - usernameRequired: '请输入用户名', - passwordRequired: '请输入密码', - usernameMin: '用户名至少3个字符', - passwordMin: '密码至少6个字符', - loginFailed: '登录失败,请重试', - testAccounts: '测试账户', - techSupport: '技术支持:OpenClaw AI助手 + React + Node.js', - selectLanguage: '选择语言' - }, - - // 菜单 - menu: { - dashboard: '仪表板', - projects: '项目管理', - advances: '预支管理', - reimbursements: '报销管理', - finance: '财务管理', - reports: '报表分析', - settings: '系统设置' - }, - - // 用户 - user: { - profile: '个人资料', - settings: '系统设置', - logout: '退出登录', - admin: '系统管理员', - finance: '财务专员', - manager: '项目经理', - employee: '普通员工' - }, - - // 系统功能 - features: { - projectManage: '项目管理:创建、跟踪、分析项目进度', - advanceManage: '预支管理:员工预支申请与审批流程', - reimburseManage: '报销管理:费用报销与核销流程', - financeReport: '财务报表:项目成本利润分析', - mobileSupport: '移动端支持:PWA技术,可添加到主屏幕' - } -} +export default { + common: { + confirm: '确认', + cancel: '取消', + save: '保存', + delete: '删除', + edit: '编辑', + add: '添加', + search: '搜索', + reset: '重置', + submit: '提交', + back: '返回', + loading: '加载中...', + success: '操作成功', + failed: '操作失败', + required: '此项为必填', + close: '关闭', + view: '查看', + refresh: '刷新', + create: '创建', + upload: '上传', + download: '下载', + export: '导出', + import: '导入', + copy: '复制', + detail: '详情', + status: '状态', + action: '操作', + name: '名称', + remark: '备注', + date: '日期', + amount: '金额', + total: '合计', + unit: '个', + meter: '米', + currency: '币种', + country: '国家', + phone: '电话', + email: '邮箱', + address: '地址', + position: '职位', + is: '是', + no: '否', + days: '天', + tenThousand: '万', + yuan: '元', + sheet: '张', + item: '项', + photo: '照片', + person: '人', + today: '今日', + unknown: '未知', + none: '无', + all: '全部', + retry: '重试', + inputPassword: '请输入密码', + deleteConfirm: '确认删除', + deleteWarning: '此操作不可恢复', + draftFound: '发现未完成的草稿', + draftRestore: '检测到上次未提交的信息,是否恢复?', + restoreDraft: '恢复草稿', + reFill: '重新填写', + closeConfirm: '确认关闭', + closeConfirmMsg: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?', + continueEdit: '继续编辑', + noData: '暂无数据', + loadingData: '加载项目数据...', + noProjectData: '暂无项目数据', + operationFailed: '操作失败', + saveFailed: '保存失败', + deleteFailed: '删除失败', + networkError: '网络错误,操作失败', + totalCount: '共 {total} 条', + systemAdmin: '系统管理员', + currentUser: '当前用户', + unnamed: '未命名', + notSet: '未设置', + pleaseSelect: '请选择', + inputPlaceholder: '请输入', + selectPlaceholder: '选择', + confirmDelete: '确定删除?', + confirmDeleteMsg: '确定要删除吗?', + saveSuccess: '保存成功', + createSuccess: '创建成功', + deleteSuccess: '删除成功', + updateSuccess: '更新成功', + }, + + login: { + title: '轻远电力老挝ERP', + subtitle: '项目管理与财务报销一体化平台', + username: '用户名', + password: '密码', + loginButton: '登录', + usernamePlaceholder: '请输入用户名', + passwordPlaceholder: '请输入密码', + usernameRequired: '请输入用户名', + passwordRequired: '请输入密码', + usernameMin: '用户名至少3个字符', + passwordMin: '密码至少6个字符', + loginFailed: '登录失败,请重试', + testAccounts: '测试账户', + techSupport: '技术支持:OpenClaw AI助手 + React + Node.js', + selectLanguage: '选择语言', + passwordError: '用户名或密码错误', + serverError: '服务器内部错误,请稍后重试', + statusCodeError: '登录失败 (状态码: {code})', + }, + + menu: { + dashboard: '工作台', + projects: '项目管理', + budgetQuotation: '预算报价', + construction: '施工管理', + constructionOverview: '施工总览', + approval: '审批管理', + pendingApproval: '待审批', + pendingExecution: '待执行', + financeDocs: '财务申请', + advanceApply: '预支申请', + reimburseApply: '报销申请', + paymentApply: '付款申请', + verificationApply: '核销申请', + financeManagement: '财务管理', + financeOverview: '财务概览', + exchangeRate: '汇率管理', + projectCost: '项目成本', + advanceVerificationStatus: '预支核销状态', + reports: '报表分析', + procurement: '采购管理', + productManagement: '商品管理', + purchaseRequest: '采购申请', + purchaseOrder: '采购订单', + paymentPlan: '付款计划', + inventory: '库存管理', + partners: '合作伙伴', + supplierManagement: '供应商管理', + subcontractorManagement: '分包商管理', + customerManagement: '客户管理', + logisticsManagement: '物流管理', + admin: '后台管理', + userManagement: '用户管理', + rolePermission: '角色权限', + processManagement: '流程管理', + templateManagement: '工程模板管理', + expenseCategory: '财务分类管理', + excelImport: 'Excel批量导入', + systemLogs: '系统日志', + dataBackup: '数据备份', + aboutSystem: '关于系统', + profile: '个人信息', + settings: '系统设置', + logout: '退出登录', + backToFront: '返回前台', + collapse: '收起菜单', + }, + + user: { + profile: '个人信息', + settings: '系统设置', + logout: '退出登录', + admin: '管理员', + finance: '财务专员', + manager: '项目经理', + employee: '普通员工', + userLabel: '用户', + role: '角色', + managingProfile: '管理个人账号信息', + name: '姓名', + phone: '手机号', + email: '邮箱', + username: '用户名', + clickToChangeAvatar: '点击更换头像', + idDocument: '证件信息', + idDocTip: '护照和驾照照片,点击图片可放大查看,点击"更换"上传新照片', + passport: '护照', + driverLicense: '驾照', + uploadPassport: '点击上传护照照片', + uploadDriverLicense: '点击上传驾照照片', + deletePassportConfirm: '确定删除护照照片?', + deleteDriverLicenseConfirm: '确定删除驾照照片?', + saveProfile: '保存修改', + changePassword: '修改密码', + currentPassword: '当前密码', + currentPasswordPlaceholder: '请输入当前密码', + newPassword: '新密码', + newPasswordPlaceholder: '请输入新密码', + confirmPassword: '确认新密码', + confirmPasswordPlaceholder: '请确认新密码', + passwordMinLen: '密码长度至少为6位', + passwordMismatch: '两次输入的密码不一致', + profileUpdated: '个人信息已更新', + passwordUpdated: '密码已更新', + updateFailed: '更新失败', + updateRetry: '更新失败,请重试', + passwordUpdateFailed: '密码更新失败', + passwordUpdateRetry: '密码更新失败,请重试', + replace: '更换', + uploadFailed: '上传失败', + }, + + features: { + projectManage: '项目管理:创建、跟踪、分析项目进度', + advanceManage: '预支管理:员工预支申请与审批流程', + reimburseManage: '报销管理:费用报销与核销流程', + financeReport: '财务报表:项目成本利润分析', + mobileSupport: '移动端支持:PWA技术,可添加到主屏幕', + }, + + dashboard: { + title: '📊 工作台', + planning: '规划中', + inProgress: '进行中', + completed: '已完成', + projectName: '项目名称', + budget: '预算', + spent: '已花费', + project: '项目', + budgetSpent: '预算/花费', + budgetLabel: '预算:', + spentLabel: '已花:', + inProgressProjects: '进行中项目', + monthlyReimburse: '本月报销', + pendingApproval: '待审批', + teamMembers: '团队成员', + recentProjects: '最近项目', + }, + + project: { + title: '项目管理', + description: '管理项目信息、进度和预算', + list: '项目列表', + quickCreate: '快速新建项目', + newProject: '新建项目', + editProject: '编辑项目', + deleteProject: '删除项目', + deleteConfirm: '删除确认', + deleteConfirmMsg: '确定要删除这个项目吗?此操作不可恢复。', + deletePassMsg: '请输入管理员密码确认删除操作:', + projectName: '项目名称', + projectNamePlaceholder: '如:万象省赛塔尼县22kV线路工程', + projectTemplate: '项目模板', + selectTemplate: '选择项目模板(可选)', + projectManager: '项目经理', + budget: '预算', + progress: '进度', + status: '状态', + planning: '规划中', + inProgress: '进行中', + completed: '已完成', + paused: '已暂停', + plan: '规划', + complete: '完成', + pause: '暂停', + customer: '客户', + selectCustomer: '选择客户', + selectManager: '选择项目经理', + contractAmount: '合同金额', + projectStatus: '项目状态', + completedHistory: '已完成(补录历史项目)', + startDate: '开始日期', + endDate: '结束日期', + location: '项目地点', + locationPlaceholder: '如:老挝万象省', + descriptionPlaceholder: '简要描述项目内容', + createSuccess: '项目创建成功', + createFailed: '创建失败', + deleteSuccess: '项目删除成功', + getListFailed: '获取项目列表失败', + unassigned: '未分配', + passwordError: '密码错误', + projectCode: '项目编号', + basicInfo: '基本信息', + contractDetails: '合同与收款', + financeDetails: '财务收支', + editBasicInfo: '编辑项目基本信息', + basicInfoSaved: '基本信息保存成功', + selectStartDate: '请选择开工日期', + selectEndDate: '请选择完工日期', + durationDays: '工期天数', + durationDaysPlaceholder: '请输入工期天数', + overview: '工程概况', + overviewPlaceholder: '请输入工程概况', + createdAt: '创建时间', + returnToList: '返回列表', + unknownManager: '未知经理', + notFound: '项目不存在或已被删除', + getInfoFailed: '获取项目信息失败', + enterConstruction: '进入施工管理', + contractNo: '合同编号', + contractType: '合同类型', + unitPriceContract: '单价合同', + includeTax: '是否含税', + settlementType: '结算方式', + lumpSum: '总价包干', + unitPrice: '单价结算', + contractTotal: '合同总价', + contractTotalPlaceholder: '请输入合同总价', + contractTax: '合同是否含税', + paymentMilestones: '付款节点', + milestoneName: '节点名称', + milestoneCondition: '节点条件', + milestoneRatio: '比例(%)', + milestoneAmount: '金额', + milestoneStatus: '完成进度', + pendingMilestone: '待开始', + noMilestone: '暂无付款节点', + noMilestoneRecord: '暂无节点记录', + addMilestone: '添加付款节点', + milestoneNotReached: '未到达付款节点', + contractAttachment: '合同附件', + contractFile: '合同文件', + viewContract: '查看合同文件', + noContractAttachment: '暂无合同附件', + otherContractInfo: '其他合同信息', + otherInfo: '其他信息', + otherInfoPlaceholder: '请输入其他合同相关信息', + warranty: '质保金设置', + hasWarranty: '是否有质保金', + warrantyRatio: '质保金比例', + warrantyAmount: '质保金金额', + warrantyPeriod: '质保期限', + warrantyExpiry: '到期日期', + warrantyStatus: '质保金状态', + warrantyReleased: '已释放', + warrantyPending: '待释放', + contractSaveSuccess: '合同细节保存成功', + draft: '暂存', + replaceFile: '更换文件', + clickUpload: '点击上传', + fileUploadFailed: '文件上传失败', + subcontract: '分包管理', + addSubcontract: '新增分包', + subcontractor: '分包商', + subcontractorName: '分包商名称', + subcontractorNamePlaceholder: '请输入分包商名称', + paidAmount: '已付款', + noSubcontract: '暂无分包记录', + subcontractDetail: '分包详情', + startDateRequired: '请选择开始日期', + endDateRequired: '请选择结束日期', + otherTerms: '其他约定', + otherTermsPlaceholder: '请输入其他约定', + paymentNote: '付款说明', + paymentNotePlaceholder: '请输入付款说明', + addSubSuccess: '新增分包成功', + addSubFailed: '新增分包失败,请检查表单数据', + projectItems: '项目单项价', + quantity: '数量', + unitPriceLabel: '单价', + totalPrice: '总价', + addItem: '+ 添加项目单项', + noItems: '暂无项目单项', + material: '材料管理', + materialName: '材料名称', + budgetQty: '预算量', + purchaseQty: '采购量', + usedQty: '使用量', + avgPrice: '均价', + noMaterial: '暂无材料记录', + constructionNode: '施工节点', + contractMilestone: '合同付款节点', + milestoneDesc: '重要节点完成情况', + plannedDate: '计划日期', + actualDate: '实际日期', + uploadProof: '上传凭证', + noRecord: '暂无记录', + constructionLog: '施工日志', + addLog: '新增日志', + weather: '天气', + recorder: '记录人', + todayWork: '今日工作', + viewPhoto: '查看照片', + noLog: '暂无日志记录', + finance: '财务信息', + received: '已收款', + totalExpense: '支出合计', + grossProfit: '毛利润', + marginRate: '毛利率', + addReceipt: '新增收款', + receiptRecords: '收款记录', + noReceipts: '暂无收款记录', + receiptType: '收款类型', + receiptTypeNode: '节点收款', + receiptTypeAdvance: '甲方借款', + receiptTypeOther: '其他收款', + receiptDate: '收款日期', + receiptNode: '关联节点', + receiptAmount: '收款金额', + receiptAmountCNY: '折合人民币', + receiptDesc: '说明', + receiptDescPlaceholder: '请输入收款说明', + receiptAdded: '收款记录添加成功', + selectMilestone: '关联付款节点', + selectMilestonePlaceholder: '请选择付款节点', + selectMilestoneRequired: '请选择付款节点', + payer: '付款方', + payerPlaceholder: '请输入付款方名称', + exchangeRate: '汇率', + voucher: '收款凭证', + uploadVoucher: '上传凭证', + viewVoucher: '查看', + expenseBreakdown: '支出分类明细', + category: '分类', + categoryAmount: '金额(¥)', + count: '笔数', + ratio: '占比', + personnelExpense: '人员支出明细', + personnel: '人员', + warrantyManagement: '质保金管理', + warrantyStartDate: '起算日期', + markReleased: '标记已释放', + extendWarranty: '延期', + currentLabel: '当前: ', + progressLabel: '进度: ', + warrantyLabel: '质保金: ', + currencyCNY: '人民币', + currencyUSD: '美元', + currencyLAK: '老挝基普', + currencyTHB: '泰铢', + amountRequired: '请输入合同金额', + settlementRequired: '请选择结算方式', + nodeNameRequired: '请输入节点名称', + nodeConditionRequired: '请输入节点条件', + ratioRequired: '请输入比例', + milestoneAmountRequired: '请输入金额', + statusRequired: '请选择状态', + }, + + construction: { + overview: '施工总览', + noProjects: '暂无施工项目', + underConstruction: '施工中', + pendingStart: '待开工', + completed: '已完工', + paused: '暂停', + currentPhase: '当前阶段:', + completedProjects: '已完工项目:', + enter: '进入', + getListFailed: '获取项目列表失败', + progress: '施工进度', + todayLog: '今日日志: ', + todayLogEmpty: '今日日志: 未填写', + writeLog: '写今日日志', + constructionLog: '施工日志', + uploadPhoto: '上传照片', + milestoneProgress: '节点进度', + management: '施工管理', + description: '查看和管理您的施工项目', + noConstructionProjects: '暂无施工项目', + contactAdmin: '请联系管理员为您分配施工项目', + myProjects: '我的施工项目', + customerLabel: '客户: ', + getInfoFailed: '获取项目信息失败', + getPhaseFailed: '获取阶段信息失败', + phaseComplete: '阶段完成!进度: ', + advancedTo: '已推进到: ', + reopenPhase: '重新打开阶段', + reopenConfirm: '确定要重新打开此阶段吗?项目进度将回退。', + phaseReopened: '阶段已重新打开', + updateItemFailed: '更新子项失败', + returnOverview: '返回总览', + currentLabel: '当前: ', + parallelPhase: '并行阶段(可与其他阶段同时进行)', + completionStandard: '完工标准:', + remarkOptional: '备注(可选):', + remarkPlaceholder: '填写完成备注...', + confirmCompleteMsg: '确认完成此阶段', + subsequentPhases: '后续阶段', + parallelLabel: '并行', + phaseHistory: '阶段历史', + rollback: '回退', + projectCompleted: '项目已完成', + projectCompletedDesc: '此项目已完工,无需施工阶段推进', + viewProjectDetail: '查看项目详情', + notInitialized: '此项目尚未初始化施工阶段', + notInitializedDesc: '请在项目详情中选择工程模板来初始化施工阶段', + goToProjectDetail: '前往项目详情', + confirmCompleteTitle: '确认完成阶段', + confirmCompleteDesc: '确认此阶段已完成?系统将自动推进到下一阶段。', + remarkLabel: '备注:', + uploadProofLabel: '上传证明材料(照片/文件):', + selectFile: '选择文件', + supportFormats: '支持照片、PDF、Word、Excel等文件', + sunny: '晴', + cloudy: '多云', + rain: '雨', + thunderstorm: '雷暴', + windy: '大风', + getLogFailed: '获取日志列表失败', + logAddSuccess: '日志添加成功', + logAddFailed: '添加日志失败', + logDeleteSuccess: '日志删除成功', + logDeleteFailed: '删除日志失败', + yearMonth: 'YYYY年MM月', + monthDay: 'MM月DD日', + recorderLabel: '记录人: ', + deleteLogConfirmTitle: '确定删除此日志?', + deleteLogConfirmDesc: '删除后无法恢复', + todayWorkLabel: '今日工作:', + tomorrowPlanLabel: '明日计划:', + issueRecordLabel: '问题记录:', + constructionPhoto: '施工照片', + noLog: '暂无施工日志', + addFirstLog: '添加第一条日志', + newLog: '新增日志', + newConstructionLog: '新增施工日志', + selectDate: '请选择日期', + selectWeather: '请选择天气', + inputTodayWork: '请填写今日工作内容', + todayWorkPlaceholder: '描述今日完成的施工工作...', + tomorrowPlanPlaceholder: '明日工作计划...', + issuePlaceholder: '遇到的问题或需要协调的事项...', + addPhoto: '添加照片', + multiPhotoSupport: '支持上传多张照片,最多9张', + plannedComplete: '计划完成: ', + overallProgress: '整体进度', + totalNodes: '总节点', + noMilestones: '暂无施工节点', + milestoneConfigured: '节点由项目经理在项目设置中配置', + inProgress: '进行中', + cancelled: '已取消', + progressTab: '施工进度', + documentsTab: '资料管理', + logsTab: '施工日志', + markComplete: '标记完成', + phasesCompleted: '个阶段已完成', + initPhases: '初始化阶段', + selectTemplateInit: '选择模板并初始化', + selectTemplate: '请选择模板', + initSuccess: '阶段初始化成功', + completedAt: '完成时间', + completionTime: '完成时间', + proofPhotos: '证明图片', + optional: '可选', + uploadImage: '上传图片', + uploadDocument: '上传文档', + imageDocs: '图片资料', + fileDocs: '文档资料', + noImages: '暂无图片资料', + noDocuments: '暂无文档资料', + fileName: '文件名', + uploader: '上传人', + uploadTime: '上传时间', + descriptionPlaceholder: '请输入说明', + clickUpload: '点击上传', + uploadSuccess: '上传成功', + addLog: '新增日志', + logAdded: '日志添加成功', + logDate: '日志日期', + weather: '天气', + weatherSunny: '晴天', + weatherCloudy: '多云', + weatherRainy: '雨天', + weatherStormy: '暴风雨', + weatherWindy: '大风', + workContent: '工作内容', + workContentPlaceholder: '请输入今日工作内容', + nextPlan: '明日计划', + nextPlanPlaceholder: '请输入明日工作计划', + issues: '问题记录', + issuesPlaceholder: '请输入遇到的问题', + sitePhotos: '现场照片', + noLogs: '暂无施工日志', + customer: '客户', + manager: '项目经理', + }, + + budget: { + title: '预算报价管理', + description: '管理商谈项目及报价版本', + newProject: '新建商谈项目', + statusFilter: '状态筛选:', + inNegotiation: '商谈中', + signed: '已签约', + unsigned: '未签约', + draft: '草稿', + sent: '已发送', + approved: '已通过', + rejected: '已拒绝', + deleteConfirm: '删除确认', + deleteConfirmMsg: '确定要删除这个预算项目吗?此操作不可恢复。', + deletePassMsg: '请输入管理员密码确认删除操作:', + deletePass: '请输入管理员密码', + getDataFailed: '获取数据失败', + deleteSuccess: '删除成功', + passwordError: '密码错误', + customerLabel: '客户: ', + managerLabel: '业务经理: ', + intermediaryLabel: '居间人: ', + intermediaryFee: '居间费: ', + versionDeleteConfirm: '确定要删除这个报价版本吗?此操作不可恢复。', + createTitle: '新建商谈项目', + createDesc: '创建新的商谈项目,添加项目基本信息', + basicInfo: '基本信息', + projectName: '项目名称', + projectNamePlaceholder: '请输入项目名称', + customer: '客户', + selectCustomer: '请选择客户', + businessManager: '业务经理', + selectManager: '请选择业务经理', + unknownDept: '未知部门', + projectLocation: '项目地点', + locationPlaceholder: '请输入项目地点', + surveyDate: '勘察日期', + intermediary: '居间人信息', + intermediaryName: '居间人', + intermediaryNamePlaceholder: '请输入居间人姓名', + intermediaryType: '居间费类型', + fixedAmount: '固定金额', + percentage: '百分比', + intermediaryRatio: '居间费比例(%)', + intermediaryRatioPlaceholder: '输入比例,如:5', + intermediaryAmount: '居间费金额', + intermediaryAmountPlaceholder: '输入金额', + projectDetail: '项目详情', + customerRequirement: '客户要求', + requirementPlaceholder: '请输入客户的具体要求', + overview: '工程概况', + overviewPlaceholder: '请输入工程概况描述', + attachment: '附件', + attachmentUpload: '附件上传', + surveyPhoto: '勘察照片', + noAccess: '您没有权限访问此页面', + createSuccess: '创建成功', + createFailed: '创建失败', + leaveConfirm: '确认离开', + leaveConfirmMsg: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?', + leave: '离开', + continueEdit: '继续编辑', + return: '返回', + signedSuccess: '标记未签约成功', + notFound: '项目不存在', + quotationVersions: '报价版本', + addVersion: '新增报价版本', + versionDate: '报价日期: ', + versionAmount: '报价金额: ', + versionRemark: '备注: ', + noVersion: '暂无报价版本', + markSigned: '标记签约', + markUnsigned: '标记未签约', + enterProject: '进入项目管理', + deleteProject: '删除项目', + detailTitle: '预算项目详情', + detailDesc: '查看项目详细信息和报价版本', + projectInfo: '项目信息', + photos: '照片 ', + noPhotos: '暂无勘察照片', + noAttachment: '暂无附件', + quickSign: '快速签约', + quickSignSuccess: '签约成功,项目已自动创建', + contractNo: '合同编号', + contractNoPlaceholder: '请输入合同编号', + contractType: '承包方式', + selectContractType: '请选择承包方式', + totalPrice: '总价', + totalPricePlaceholder: '请输入总价', + durationDays: '工期(天)', + durationPlaceholder: '请输入工期', + durationDaysPlaceholder: '请输入工期(天)', + quickSignNote: '注:此为快速签约流程,仅录入基本信息。详细的合同信息可在项目管理中补充。', + newQuotation: '新增报价版本', + quotationDate: '报价日期', + selectDate: '请选择报价日期', + quotationAmount: '报价金额', + amountPlaceholder: '请输入报价金额', + quotationFile: '报价文件', + viewFile: '查看文件', + uploadFile: '上传文件', + remarkPlaceholder: '请输入备注信息', + uploadSuccess: '上传成功', + version: '当前版本: ', + projectLabel: '项目名称: ', + }, + + finance: { + title: '财务管理', + addRecord: '新增财务记录', + addRecordBtn: '新增记录', + exportExcel: '导出Excel', + totalIncome: '总收入', + totalExpense: '总支出', + netProfit: '净利润', + expenseSummary: '支出分类汇总', + detail: '财务明细', + filterType: '筛选类型', + date: '日期', + incomeType: '收支类型', + level1Category: '一级分类', + level2Category: '二级分类', + projectName: '项目名称', + amount: '金额', + currency: '币种', + exchangeRate: '汇率', + equivalentCNY: '等效人民币', + counterpartyName: '对方名称', + counterpartyType: '对方类型', + personName: '人员姓名', + desc: '描述', + voucherNo: '凭证编号', + selectDate: '请选择日期', + selectIncomeType: '请选择', + selectLevel1: '请选择', + selectLevel2: '请先选择一级分类', + selectProject: '选择项目', + amountPlaceholder: '0', + counterpartyPlaceholder: '收款方/付款方名称', + selectType: '选择类型', + personNamePlaceholder: '关联员工姓名', + descPlaceholder: '补充说明', + voucherPlaceholder: '发票号/收据号', + income: '收入', + expense: '支出', + projectExpense: '项目支出', + companyExpense: '公司支出', + incomeCategory: '收入', + manual: '手动', + advance: '预支', + reimbursement: '报销', + payment: '付款', + material: '材料', + freight: '运费', + source: '来源', + recordSuccess: '录入成功', + exporting: '正在导出...', + exportSuccess: '导出成功', + exportFailed: '导出失败', + sheetName: '财务记账', + totalRecords: '共 {total} 条', + currencyCNY: 'CNY (人民币)', + currencyLAK: 'LAK (老挝基普)', + currencyUSD: 'USD (美元)', + currencyTHB: 'THB (泰铢)', + counterpartySupplier: '供应商', + counterpartySubcontractor: '分包商', + counterpartyCustomer: '客户', + counterpartyEmployee: '员工', + counterpartyLogistics: '物流公司', + counterpartyShareholder: '股东', + counterpartyOther: '其他', + projectRevenue: '项目合同收款', + warrantyReturn: '质保金退回', + shareholderInvestment: '股东投资入股', + otherIncome: '其他收入', + materialPurchase: '材料采购', + equipmentPurchase: '设备采购', + constructionSubcontract: '施工分包', + laborWage: '人工工资', + travelTransport: '差旅交通', + accommodationFood: '食宿费用', + transportLogistics: '运输物流', + surveyDesign: '勘测设计', + smallTools: '小型工具', + customerEDLRelation: '客户/EDL关系', + otherProjectExpense: '其他项目支出', + salaryWelfare: '工资薪酬', + rentProperty: '房租物业', + officeExpense: '办公费用', + commute: '交通通勤', + vehicleMaintenance: '车辆维保', + fixedAsset: '固定资产', + marketing: '营销拓展', + entertainment: '招待费用', + employeeBenefit: '员工福利', + expressLogistics: '快递物流', + otherCompanyExpense: '其他公司支出', + }, + + cash: { + tabOverview: '收支总览', + tabIncome: '收入录入', + tabExpense: '支出录入', + addIncome: '新增收入', + addExpense: '新增支出', + financeExpense: '财务支出', + customerAdvance: '客户预付/借款', + bankLoan: '银行借款', + otherLoan: '其他借款', + dividendIncome: '分红收入', + interestIncome: '利息收入', + assetDisposal: '资产处置收入', + taxRefund: '退税收入', + governmentSubsidy: '政府补贴', + loanRepayment: '还贷支出', + interestExpense: '利息支出', + dividendPayment: '分红支出', + taxPayment: '税费支出', + depositPayment: '保证金/押金', + ownerExpense: '老板支出', + otherFinance: '其他财务支出', + sourceLabel: '资金管理', + receiptSource: '项目收款', + counterpartyBank: '银行', + counterpartySelect: '选择对方', + counterpartySelectPlaceholder: '请选择交易对方', + voucherUpload: '凭证上传', + uploadVoucher: '上传凭证', + uploadSuccess: '上传成功', + uploadFailed: '上传失败', + }, + + reports: { + title: '统计报表', + description: '查看项目财务报表和统计分析数据', + totalIncome: '总收入', + totalExpense: '总支出', + netProfit: '净利润', + monthlyReport: '月度财务报表', + selectMonth: '选择月份', + month: '月份', + incomeCategory: '收入分类', + projectExpenseCategory: '项目支出分类', + companyExpenseCategory: '公司支出分类', + categoryTag: '收入', + projectTag: '项目', + companyTag: '公司', + }, + + paymentRequest: { + title: '付款申请', + description: '管理对外付款申请', + newRequest: '新建付款申请', + activeApplications: '活跃申请', + completed: '已完结', + subject: '事由', + applicant: '申请人', + payee: '收款单位', + amount: '金额', + applicationDate: '申请日期', + status: '状态', + code: '编号', + action: '操作', + edit: '编辑', + withdraw: '撤回', + reEdit: '编辑重提', + delete: '删除', + deleteSuccess: '删除成功', + withdrawSuccess: '已撤回,可重新编辑', + withdrawFailed: '撤回失败', + editPayment: '编辑付款申请', + newPayment: '新建付款申请', + expenseType: '支出类型', + selectExpenseType: '选择支出类型', + relatedProject: '关联项目', + selectProject: '选择项目', + expenseCategory: '支出分类', + selectCategory: '选择支出分类', + payeeType: '收款单位类型', + selectPayeeType: '选择收款单位类型', + selectSubcontractor: '选择分包商', + selectSupplier: '选择供应商', + selectCustomer: '选择客户', + payeeName: '收款单位', + payeeNamePlaceholder: '手动输入收款单位名称', + accountName: '收款户名', + accountNamePlaceholder: '收款户名(选择分包商/供应商/客户时自动填充)', + bankAccount: '银行账号', + bankAccountPlaceholder: '收款银行账号(选择分包商/供应商/客户时自动填充)', + bankName: '开户银行', + bankNamePlaceholder: '开户银行名称(选择分包商/供应商/客户时自动填充)', + qrCode: '收款码', + paymentAmount: '付款金额', + paymentAmountPlaceholder: '输入付款金额', + paymentReason: '付款事由', + paymentReasonPlaceholder: '付款原因', + uploadProof: '上传凭证附件', + proofAttachment: '凭证附件', + equivalentCNY: '等价人民币:¥ ', + getListFailed: '获取付款申请列表失败', + deleteFailed: '删除失败', + operationSuccess: '操作成功', + detailTitle: '付款申请详情', + applicationCode: '申请编号', + pendingApproval: '待审批', + approved: '已批准', + rejected: '已退回', + withdrawn: '已撤回', + paid: '已付款', + companyExpense: '公司支出', + projectExpense: '项目支出', + counterpartySubcontractor: '分包商', + counterpartySupplier: '供应商', + counterpartyCustomer: '客户', + counterpartyOther: '其他', + withdrawConfirm: '确认撤回', + withdrawConfirmMsg: '撤回后可重新编辑提交,确认撤回吗?', + currencyCNY: '人民币 (CNY)', + currencyUSD: '美元 (USD)', + currencyLAK: '老挝基普 (LAK)', + currencyTHB: '泰铢 (THB)', + deleteConfirmMsg: '确认删除此付款申请?', + }, + + paymentPlan: { + title: '付款计划', + description: '管理采购订单的付款计划', + newPlan: '新建付款计划', + editPlan: '编辑付款计划', + planCode: '计划编号', + purchaseOrder: '采购订单', + paymentDate: '付款日期', + amount: '金额', + paymentType: '付款类型', + status: '状态', + creator: '创建人', + action: '操作', + pending: '待处理', + approved: '已审批', + executed: '已执行', + cancelled: '已取消', + partialPayment: '部分付款', + fullPayment: '全额付款', + selectOrder: '请选择采购订单', + selectDate: '请选择付款日期', + inputAmount: '请输入付款金额', + selectCurrency: '请选择币种', + selectType: '请选择付款类型', + selectStatus: '请选择状态', + inputCreator: '请输入创建人', + amountPlaceholder: '付款金额', + descPlaceholder: '请输入付款计划描述', + detailTitle: '付款计划详情', + detailCode: '计划编号', + currencyCNY: '人民币', + currencyUSD: '美元', + currencyLAK: '老挝基普', + currencyTHB: '泰铢', + getListFailed: '获取付款计划列表失败', + getDetailFailed: '获取付款计划详情失败', + }, + + verification: { + title: '核销申请', + description: '预支款项核销', + newVerification: '新建核销', + editVerification: '编辑核销', + subject: '事由', + applicant: '申请人', + relatedAdvance: '关联预支', + amount: '金额', + verificationDate: '核销日期', + status: '状态', + code: '编号', + action: '操作', + detail: '详情', + withdraw: '撤回', + reEdit: '编辑重提', + delete: '删除', + addDetail: '添加明细', + advanceAmount: '预支金额', + settlementOptions: '结算选项', + settlementAmount: '结算金额', + expenseType: '支出类型', + selectProject: '选择项目', + subjectPlaceholder: '核销原因说明', + detailLabel: '核销明细', + expenseDescription: '费用说明', + expenseCategory: '支出分类', + detailAmount: '金额', + attachment: '凭证附件', + mainAttachment: '主附件', + refundProof: '退款凭证(必填)', + overallAttachment: '整体凭证附件', + selectAdvance: '请选择关联预支单', + selectAdvanceOrInput: '选择或输入预支单编号', + selectProjectRequired: '请选择项目', + uploadRefundRequired: '请上传退款凭证', + finalSettlement: '是否作为最终结算', + verifiedAmount: '已核销金额: ', + remainingAmount: '剩余金额: ', + refundLabel: '退款 ¥{amount}', + supplementLabel: '补款 ¥{amount}', + totalLabel: '合计: ', + refundNote: '* 退款类型的结算核销必须上传退款凭证', + unknownProject: '未知项目', + advanceInfo: '预支单信息', + advanceCode: '预支单号', + advanceTotalAmount: '预支金额', + advanceVerified: '已核销金额', + advanceRemaining: '剩余金额', + detailTitle: '核销详情', + attachmentCount: '{count}张', + isSettlement: '是', + notSettlement: '否', + refundText: '退款 ', + supplementText: '补款 ', + pendingApproval: '待审批', + approved: '已批准', + rejected: '已退回', + withdrawn: '已撤回', + paid: '已付款', + pendingEdit: '待编辑', + deleteSuccess: '删除成功', + deleteFailed: '删除失败', + withdrawSuccess: '已撤回,可重新编辑', + withdrawFailed: '撤回失败', + saveSuccess: '保存成功', + createSuccess: '创建成功', + submitSuccess: '提交成功', + getListFailed: '获取核销列表失败', + companyExpense: '公司支出', + projectExpense: '项目支出', + currencyCNY: '人民币 (CNY)', + currencyUSD: '美元 (USD)', + currencyLAK: '老挝基普 (LAK)', + currencyTHB: '泰铢 (THB)', + withdrawConfirm: '确认撤回', + withdrawConfirmMsg: '撤回后可重新编辑提交,确认撤回吗?', + deleteConfirmMsg: '确定要删除这条核销记录吗?', + project: '关联项目', + projectPlaceholder: '请选择项目', + category: '支出分类', + categoryPlaceholder: '请选择支出分类', + categoryLabel: '分类: ', + categoryRequired: '请选择支出分类', + verificationAmount: '核销金额', + settlementType: '结算类型', + settlement: '结算', + settlementInfo: '结算信息', + nonSettlement: '非结算', + expenseDetail: '费用明细', + totalItems: '共{count}项', + paymentProof: '付款凭证', + receipt: '凭证', + addExpense: '添加费用', + editExpense: '编辑费用', + edit: '编辑', + viewDetail: '查看详情', + refund: '退款', + supplement: '补缴', + inputAmount: '请输入金额', + amountPlaceholder: '输入金额', + equivalentCNY: '等价人民币: ¥', + totalAmount: '合计: ¥{amount}', + descriptionPlaceholder: '请输入费用说明', + advanceCodePlaceholder: '选择或输入预支单号', + deleteConfirm: '确认删除', + getDetailFailed: '获取详情失败', + }, + + advance: { + title: '预支申请', + description: '管理员工预支申请', + newAdvance: '新建预支', + editAdvance: '编辑预支', + subject: '事由', + applicant: '申请人', + amount: '金额', + advanceDate: '预支日期', + status: '状态', + code: '编号', + action: '操作', + detail: '详情', + withdraw: '撤回', + reEdit: '编辑重提', + delete: '删除', + activeApplications: '活跃申请', + completed: '已完结', + advanceCode: '预支编号', + subjectPlaceholder: '请输入预支事由', + amountPlaceholder: '输入金额', + inputAmount: '请输入金额', + equivalentCNY: '等价人民币:¥ ', + attachment: '凭证附件', + pendingApproval: '待审批', + approved: '已批准', + rejected: '已退回', + withdrawn: '已撤回', + verified: '已核销', + pendingEdit: '待编辑', + deleteSuccess: '删除成功', + deleteFailed: '删除失败', + withdrawSuccess: '已撤回,可重新编辑', + withdrawFailed: '撤回失败', + saveSuccess: '保存成功', + createSuccess: '创建成功', + submitSuccess: '提交成功', + getListFailed: '获取预支列表失败', + getDetailFailed: '获取详情失败', + deletePassInput: '请输入密码确认删除', + deletePassPlaceholder: '输入密码', + currencyCNY: '人民币 (CNY)', + currencyUSD: '美元 (USD)', + currencyLAK: '老挝基普 (LAK)', + currencyTHB: '泰铢 (THB)', + detailTitle: '预支详情', + withdrawConfirm: '确认撤回', + withdrawConfirmMsg: '撤回后可重新编辑提交,确认撤回吗?', + }, + + advanceVerification: { + title: '预支核销状态管理', + description: '管理预支单的核销状态和进度', + subject: '事由', + applicant: '申请人', + advanceAmount: '预支金额', + verifiedAmount: '已核销金额', + remainingAmount: '剩余金额', + advanceDate: '预支日期', + status: '状态', + code: '编号', + action: '操作', + searchByName: '按申请人姓名搜索', + startDate: '开始日期', + endDate: '结束日期', + search: '搜索', + unverified: '未核销完成', + completed: '已完结', + detail: '详情', + noAccess: '无权限访问', + noAccessMsg: '您没有权限访问此页面,只有管理员和财务人员可以查看预支核销状态。', + pendingApproval: '待审批', + approved: '已批准', + rejected: '已退回', + withdrawn: '已撤回', + verified: '已核销', + pendingEdit: '待编辑', + partialVerified: '部分核销', + completedStatus: '已完成', + detailTitle: '预支单详情:{code}', + advanceCode: '预支编号', + amount: '金额', + verifiedLabel: '已核销金额', + remaining: '剩余金额', + currency: '币种', + relatedVerifications: '关联核销单', + verificationCode: '核销编号', + relatedAdvance: '关联预支单', + verificationAmount: '核销金额', + verificationDate: '核销日期', + isSettlement: '是否结算', + getListFailed: '获取预支单失败', + getDetailFailed: '获取预支单详情失败', + }, + + reimbursement: { + title: '报销申请', + description: '管理费用报销申请', + newReimbursement: '新建报销', + editReimbursement: '编辑报销', + subject: '事由', + applicant: '申请人', + amount: '金额', + reimbursementDate: '报销日期', + status: '状态', + code: '编号', + action: '操作', + detail: '详情', + withdraw: '撤回', + reEdit: '编辑重提', + delete: '删除', + addDetail: '添加明细', + activeApplications: '活跃申请', + completed: '已完结', + expenseType: '支出类型', + selectProject: '选择项目', + subjectPlaceholder: '请输入报销事由', + detailLabel: '报销明细', + expenseDescription: '费用说明', + expenseCategory: '支出分类', + detailAmount: '金额', + attachment: '凭证附件', + mainAttachment: '主附件', + overallAttachment: '整体凭证附件', + totalLabel: '合计: ', + unknownProject: '未知项目', + reimbursementCode: '报销编号', + attachmentCount: '{count}张', + pendingApproval: '待审批', + approved: '已批准', + rejected: '已退回', + withdrawn: '已撤回', + paid: '已付款', + pendingEdit: '待编辑', + deleteSuccess: '删除成功', + deleteFailed: '删除失败', + withdrawSuccess: '已撤回,可重新编辑', + withdrawFailed: '撤回失败', + saveSuccess: '保存成功', + createSuccess: '创建成功', + submitSuccess: '提交成功', + getListFailed: '获取报销列表失败', + deletePassInput: '请输入密码确认删除', + deletePassPlaceholder: '输入密码', + companyExpense: '公司支出', + projectExpense: '项目支出', + currencyCNY: '人民币 (CNY)', + currencyUSD: '美元 (USD)', + currencyLAK: '老挝基普 (LAK)', + currencyTHB: '泰铢 (THB)', + detailTitle: '报销详情', + withdrawConfirm: '确认撤回', + withdrawConfirmMsg: '撤回后可重新编辑提交,确认撤回吗?', + expenseDetail: '费用明细', + totalItems: '共{count}项', + receipt: '凭证', + addExpense: '添加费用', + editExpense: '编辑费用', + category: '支出分类', + categoryLabel: '分类: ', + categoryPlaceholder: '请选择支出分类', + categoryRequired: '请选择支出分类', + descriptionPlaceholder: '请输入费用说明', + project: '关联项目', + projectPlaceholder: '请选择项目', + inputAmount: '请输入金额', + amountPlaceholder: '输入金额', + equivalentCNY: '等价人民币: ¥', + totalAmount: '合计: ¥{amount}', + edit: '编辑', + viewDetail: '查看详情', + getDetailFailed: '获取详情失败', + deleteConfirmMsg: '确认删除此报销记录?', + }, + + approval: { + title: '审批管理', + description: '审批预支、报销、付款等申请', + approve: '审批', + refresh: '刷新数据', + pass: '通过', + reject: '退回', + close: '关闭', + pendingTab: '待审批', + historyTab: '审批记录', + subject: '事由', + type: '类型', + applicant: '申请人', + amount: '金额', + applicationDate: '申请日期', + status: '状态', + code: '编号', + action: '操作', + time: '时间', + operation: '操作', + operator: '操作人', + note: '备注/原因', + applicationType: '申请类型', + applicationCode: '申请编号', + payeeType: '收款单位类型', + payee: '收款方', + bankName: '银行名称', + bankAccount: '银行账号', + expenseType: '支出类型', + relatedProject: '关联项目', + expenseCategory: '支出分类', + relatedAdvance: '关联预支单', + advanceAmount: '预支金额', + settlement: '结算核销', + verifiedAmount: '已核销金额', + remainingAmount: '剩余核销金额', + settlementAmount: '核销结算金额', + purchaseType: '采购类型', + supplier: '供应商', + currency: '币种', + remark: '备注', + approvalNote: '审批备注', + approvalNotePlaceholder: '可选:填写审批备注', + rejectReason: '退回原因', + rejectReasonPlaceholder: '请填写退回原因', + productDetail: '商品明细', + detailList: '明细清单', + refundProof: '退款凭证', + proofAttachment: '凭证附件', + approvalOpinion: '审批意见', + detailLabel: '明细 {index}:', + categoryLabel: '支出分类:', + detailAttachment: '明细附件:', + specLabel: '规格: ', + unitLabel: '单位: ', + qtyLabel: '数量: ', + priceLabel: '单价: ', + advanceApply: '预支申请', + reimburseApply: '报销申请', + paymentApply: '付款申请', + verificationApply: '核销申请', + purchaseApply: '采购申请', + pendingApproval: '待审批', + approved: '已通过', + rejected: '已退回', + withdrawn: '已撤回', + executed: '已执行', + partialVerified: '部分核销', + completed: '已完结', + getPendingFailed: '获取待审批数据失败', + getHistoryFailed: '获取审批历史记录失败', + approveSuccess: '审批通过:{code}', + rejectSuccess: '已退回:{code}', + withdrawSuccess: '申请已撤回', + editResubmit: '修改成功,已重新提交审批', + withdrawConfirm: '撤回申请', + withdrawConfirmMsg: '确认撤回申请 {code} 吗?', + withdrawConfirmBtn: '确认撤回', + projectPurchase: '项目采购', + stockPurchase: '库存采购', + refundText: '退款 ', + supplementText: '补款 ', + editTitle: '编辑申请:{code}', + detailTitle: '{type}详情:{code}', + advanceDetailTitle: '{type}详情:{code}', + counterpartySubcontractor: '分包商', + counterpartySupplier: '供应商', + counterpartyCustomer: '客户', + counterpartyOther: '其他', + companyExpense: '公司支出', + projectExpense: '项目支出', + material: '材料', + equipment: '设备', + pole: '电杆', + other: '其他', + accommodation: '住宿', + catering: '餐饮', + fuel: '加油', + scatteredMaterial: '零散材料', + customerRelation: '客户关系', + subcontractorRelation: '分包关系', + EDLRelation: 'EDL关系', + extraConstruction: '额外施工', + generalOperation: '通用运营(房租/耗材)', + commute: '交通通勤', + marketing: '业扩营销', + powerSystem: '电力系统关系', + employeeBenefit: '员工福利', + expressLogistics: '快递物流', + }, + + execution: { + title: '执行管理', + description: '执行已审批通过的付款申请', + execute: '执行', + edit: '编辑', + cancel: '取消', + pass: '退回', + close: '关闭', + uploadProof: '上传付款凭证', + pendingTab: '待执行', + executedTab: '已执行', + subject: '事由', + type: '类型', + applicant: '申请人', + amount: '金额', + payee: '收款方', + approvalDate: '审批日期', + code: '编号', + action: '操作', + executionDate: '执行日期', + executionMethod: '执行方式', + status: '状态', + searchPlaceholder: '搜索事由、编号或申请人', + filterType: '筛选类型', + sortBy: '排序方式', + sortDateNew: '执行日期(最新)', + sortDateOld: '执行日期(最早)', + sortAmountHigh: '金额(从高到低)', + sortAmountLow: '金额(从低到高)', + confirmationDate: '确认日期', + paymentMethod: '收款方式', + executionMethodLabel: '执行方式', + proofOfPayment: '付款凭证', + paymentConfirmation: '收款确认信息', + remark: '备注', + returnReason: '退回原因', + confirmRequired: '请填写收款确认信息', + rejectReasonRequired: '请填写退回原因', + confirmationPlaceholder: '请填写收款确认信息,如收款账号、收款时间等', + remarkPlaceholder: '可选:填写执行备注', + bankTransfer: '银行转账', + cash: '现金', + wechat: '微信', + other: '其他', + proofUploadTip: '请上传付款凭证(银行转账回单、现金收据等),支持图片和PDF格式', + noProofRefund: '此核销申请为退款类型,无需上传付款凭证', + noProofNonSettlement: '此核销申请为非结算核销,无需上传付款凭证', + pendingExecution: '待执行', + executed: '已执行', + rejected: '已退回', + approved: '已批准', + executeSuccess: '执行成功:{code}', + executeFailed: '执行操作失败,请重试', + proofRequired: '请上传付款凭证', + rejectSuccess: '已退回:{code},申请人可编辑后重新提交', + rejectFailed: '退回操作失败,请重试', + editSuccess: '修改成功,已重新提交审批', + uploadSuccess: '{name} 上传成功', + uploadFailed: '{name} 上传失败', + getPendingFailedFormat: '获取待执行数据失败:数据格式错误', + getPendingFailed: '获取待执行数据失败:', + getPendingNetworkError: '网络错误,获取待执行数据失败', + getExecutedFailedFormat: '获取已执行数据失败:数据格式错误', + getExecutedFailed: '获取已执行数据失败:', + getExecutedNetworkError: '网络错误,获取已执行数据失败', + supplierPaymentInfo: '供应商收款信息', + accountName: '收款户名', + bankAccount: '银行账号', + bankName: '开户银行', + qrCode: '收款码', + purchaseDetail: '采购明细', + detailList: '明细清单', + approvalOpinion: '审批意见', + refundProof: '退款凭证', + applicationAttachment: '申请凭证附件', + executionInfo: '执行信息', + applicationType: '申请类型', + applicationCode: '申请编号', + payeeType: '收款单位类型', + expenseType: '支出类型', + relatedProject: '关联项目', + expenseCategory: '支出分类', + relatedAdvance: '关联预支单', + advanceAmount: '预支金额', + settlement: '结算核销', + verifiedAmount: '已核销金额', + remainingAmount: '剩余核销金额', + settlementAmount: '核销结算金额', + purchaseType: '采购类型', + supplier: '供应商', + currency: '币种', + detailLabel: '明细 {index}:', + categoryLabel: '支出分类:', + detailAttachment: '明细附件:', + specLabel: '规格: ', + unitLabel: '单位: ', + qtyLabel: '数量: ', + advanceApply: '预支申请', + reimburseApply: '报销申请', + paymentApply: '付款申请', + verificationApply: '核销申请', + purchaseApply: '采购申请', + projectPurchase: '项目采购', + stockPurchase: '库存采购', + refundText: '退款 ', + supplementText: '补款 ', + counterpartySubcontractor: '分包商', + counterpartySupplier: '供应商', + counterpartyCustomer: '客户', + counterpartyOther: '其他', + companyExpense: '公司支出', + projectExpense: '项目支出', + material: '材料', + equipment: '设备', + pole: '电杆', + otherCategory: '其他', + accommodation: '住宿', + catering: '餐饮', + fuel: '加油', + scatteredMaterial: '零散材料', + customerRelation: '客户关系', + subcontractorRelation: '分包关系', + EDLRelation: 'EDL关系', + extraConstruction: '额外施工', + generalOperation: '通用运营(房租/耗材)', + commute: '交通通勤', + marketing: '业扩营销', + powerSystem: '电力系统关系', + employeeBenefit: '员工福利', + expressLogistics: '快递物流', + }, + + procurement: { + title: '采购管理', + description: '管理采购订单和物料入库', + newProcurement: '新建采购', + orderCode: '采购单号', + purchaseDate: '采购日期', + supplier: '供应商', + materialName: '物料名称', + quantity: '数量', + unitPrice: '单价', + totalAmount: '总金额', + status: '状态', + action: '操作', + view: '查看', + approve: '审批', + pendingApproval: '待审批', + approved: '已批准', + stocked: '已入库', + rejected: '已拒绝', + startDate: '开始日期', + endDate: '结束日期', + searchOrder: '搜索采购单号', + monthPurchase: '本月采购额', + newApplication: '新建采购申请', + selectSupplier: '选择供应商', + inputMaterialName: '请输入物料名称', + remark: '备注', + remarkPlaceholder: '请输入备注说明', + submitSuccess: '采购申请已提交', + }, + + purchaseRequest: { + title: '采购申请', + description: '管理公司采购申请(简化版:仅填写需求描述和预计金额)', + newRequest: '新建采购申请', + activeApplications: '活跃申请', + completed: '已完成', + subject: '事由', + project: '项目', + category: '分类', + estimatedAmount: '预计金额', + demandDate: '需求日期', + status: '状态', + applicationDate: '申请日期', + applicant: '申请人', + code: '编号', + action: '操作', + approve: '通过', + reject: '驳回', + withdraw: '撤回', + edit: '编辑', + confirmDelete: '确定要删除吗?', + selectProjectFilter: '选择项目筛选', + selectStatusFilter: '选择状态筛选', + editRequest: '编辑采购申请', + submitApproval: '提交审批', + purchaseType: '采购类型', + selectPurchaseType: '请选择采购类型', + purchaseTypeRequired: '请选择采购类型', + stockPurchase: '库存采购', + projectPurchase: '项目采购', + relatedProject: '关联项目', + selectProject: '请选择项目', + projectRequired: '项目采购必须关联项目', + applicantLabel: '申请人', + applicantPlaceholder: '请输入申请人', + applicantRequired: '请输入申请人', + applicationDateLabel: '申请日期', + dateRequired: '请选择申请日期', + subjectDescription: '事由描述', + subjectRequired: '请输入事由描述', + subjectMaxLength: '事由描述不能超过100个字符', + subjectPlaceholder: '请简要描述采购需求(如:采购XX项目所需电缆、电杆等材料)', + expenseCategory: '支出分类', + selectCategory: '请选择支出分类', + categoryRequired: '请选择支出分类', + material: '材料', + equipment: '设备', + pole: '电杆', + other: '其他', + estimatedAmountLabel: '预计金额', + amountRequired: '请输入预计金额', + estimatedAmountPlaceholder: '预计金额', + currency: '币种', + selectCurrency: '请选择币种', + currencyRequired: '请选择币种', + demandDateLabel: '需求日期', + demandDatePlaceholder: '期望到货日期', + remarkLabel: '备注', + remarkPlaceholder: '请输入备注(选填)', + attachment: '附件', + selectFile: '选择文件', + detailTitle: '采购申请详情', + applicationCode: '申请编号', + createdAt: '创建时间', + getListFailed: '获取采购申请列表失败', + getDetailFailed: '获取采购申请详情失败', + deleteSuccess: '删除成功', + deleteFailed: '删除失败', + saveSuccess: '保存成功', + createSuccess: '创建成功', + submitSuccess: '创建并提交成功', + submitFailed: '提交失败', + withdrawSuccess: '撤回成功', + withdrawFailed: '撤回失败', + approveSuccess: '审批通过成功', + approveFailed: '审批通过失败', + rejectSuccess: '驳回成功', + rejectFailed: '驳回失败', + pendingEdit: '待编辑', + pendingApproval: '待审批', + approved: '已审批', + executed: '已执行', + withdrawn: '已撤回', + currencyCNY: '人民币', + currencyUSD: '美元', + currencyLAK: '老挝基普', + currencyTHB: '泰铢', + }, + + purchaseOrder: { + title: '采购订单', + description: '管理采购订单(多TAB:基本信息、商品明细、付款信息、物流信息、验收记录)', + draft: '草稿', + confirmed: '已确认', + partialPayment: '部分付款', + paidOff: '已付清', + inTransit: '物流中', + accepted: '已验收', + closed: '已关闭', + cancelled: '已取消', + orderCode: '订单号', + supplier: '供应商', + relatedProject: '项目', + amount: '金额', + paid: '已付', + status: '状态', + createDate: '创建日期', + action: '操作', + confirm: '确认', + cancel: '取消', + delete: '删除', + confirmCancel: '确定要取消吗?', + confirmDelete: '确定要删除吗?', + confirmDeleteShort: '确定删除?', + productName: '商品名称', + spec: '规格', + unit: '单位', + quantity: '数量', + unitPrice: '单价', + subtotal: '小计', + phase: '阶段', + plannedDate: '计划日期', + plannedAmount: '计划金额', + ratioPercent: '比例%', + actualAmount: '实际金额', + pendingPayment: '待付款', + applied: '已申请', + paidStatus: '已支付', + trackingNumber: '物流单号', + origin: '发货地', + china: '中国', + laos: '老挝', + logisticsCompany: '物流公司', + deliveryDate: '发货日期', + freight1: '一次运费', + freight2: '二次运费', + acceptanceCode: '验收单号', + acceptanceDate: '验收日期', + acceptor: '验收人', + acceptedQty: '验收数量', + selectProject: '选择项目筛选', + selectStatus: '选择状态筛选', + detailTitle: '采购订单详情 - {code}', + basicInfo: '基本信息', + supplierCountry: '供应商国家', + estimatedAmount: '预计金额', + orderAmount: '订单金额', + paidAmount: '已付金额', + createdAt: '创建时间', + remark: '备注', + productDetails: '商品明细', + addProduct: '添加商品', + productTotal: '商品总计:', + paymentInfo: '付款信息', + addPaymentPlan: '添加付款计划', + logisticsInfo: '物流信息', + noLogistics: '暂无物流信息', + acceptanceRecord: '验收记录', + noAcceptance: '暂无验收记录', + editProduct: '编辑商品', + addProductTitle: '添加商品', + selectProduct: '选择商品', + productNameInput: '商品名称', + specInput: '规格', + unitInput: '单位', + quantityInput: '数量', + unitPriceInput: '单价', + editPaymentPlan: '编辑付款计划', + addPaymentPlanTitle: '添加付款计划', + selectPhase: '选择阶段', + advancePayment: '预付款', + deliveryPayment: '发货款', + acceptancePayment: '验收款', + finalPayment: '尾款', + orderConfirmSuccess: '订单确认成功', + orderConfirmFailed: '订单确认失败', + orderCancelSuccess: '订单已取消', + orderCancelFailed: '取消订单失败', + orderDeleteSuccess: '订单删除成功', + orderDeleteFailed: '删除订单失败', + productUpdateSuccess: '商品更新成功', + productAddSuccess: '商品添加成功', + productDeleteSuccess: '商品删除成功', + paymentPlanUpdateSuccess: '付款计划更新成功', + paymentPlanAddSuccess: '付款计划添加成功', + paymentPlanDeleteSuccess: '付款计划删除成功', + getListFailed: '获取采购订单列表失败', + getDetailFailed: '获取订单详情失败', + }, + + product: { + thumbnail: '缩略图', + productName: '商品名称', + model: '型号', + level1Category: '一级分类', + level2Category: '二级分类', + unit: '单位', + quantity: '数量', + costPrice: '成本单价', + brand: '品牌', + action: '操作', + edit: '编辑', + delete: '删除', + confirmDelete: '确定删除此商品吗?', + confirmDeleteCategory: '确定删除此分类吗?', + addLevel2: '新增二级分类', + addLevel1: '新增一级分类', + totalProducts: '商品总数', + totalCategories: '分类总数', + searchPlaceholder: '搜索商品名称/型号/品牌', + downloadTemplate: '下载模板', + batchUpload: '批量上传', + addProduct: '新增商品', + productList: '商品列表', + filterByCategory: '按分类筛选:', + selectCategory: '选择分类', + clearFilter: '清除筛选', + totalRecords: '共 {total} 条', + categoryManagement: '分类管理', + addCategory: '新增分类', + noCategory: '暂无分类', + editProduct: '编辑商品', + addProductTitle: '新增商品', + nameRequired: '请输入商品名称', + namePlaceholder: '如:JKLYJ-120-22kV高压绝缘线', + modelPlaceholder: '如:JKLYJ-120-22kV', + selectLevel1: '选择一级分类', + level1Required: '请选择一级分类', + selectLevel2: '选择二级分类(可选)', + level2Extra: '可选,不选则使用一级分类', + selectUnit: '选择单位', + costPricePlaceholder: '默认为0', + source: '来源', + selectSource: '选择来源', + china: '中国', + laos: '老挝', + brandPlaceholder: '品牌名称', + specs: '规格参数', + specsPlaceholder: '如:120mm², 22kV', + remarkPlaceholder: '其他说明', + batchUploadTitle: '批量上传商品', + uploadInstructions: '上传说明:', + uploadStep1: '请先下载模板文件,按照模板格式填写商品信息', + uploadStep2: '支持 .xlsx 和 .xls 格式的Excel文件', + uploadStep3: '商品名称和一级分类为必填字段', + uploadStep4: '其他字段为选填,可根据实际情况填写', + uploadStep5: '来源字段默认为老挝,可选填中国/老挝', + uploading: '上传中...', + selectExcel: '选择Excel文件', + downloadImportTemplate: '下载导入模板', + editCategory: '编辑分类', + addCategoryTitle: '新增分类', + categoryName: '分类名称', + categoryNameRequired: '请输入分类名称', + categoryNamePlaceholder: '如:电线电缆', + categoryLevel: '分类级别', + selectLevel: '选择分类级别', + levelRequired: '请选择分类级别', + parentCategory: '上级分类', + selectParent: '选择上级分类(可选)', + parentCategoryExtra: '选择二级分类时,必须选择上级分类', + getListFailed: '获取商品列表失败', + categoryUpdateSuccess: '分类更新成功', + categoryCreateSuccess: '分类创建成功', + categoryDeleteSuccess: '分类删除成功', + title: '商品管理', + code: '商品编码', + codePlaceholder: '请输入商品编码', + codeRequired: '请输入商品编码', + name: '商品名称', + category: '分类', + categoryRequired: '请选择分类', + spec: '规格', + specPlaceholder: '请输入规格', + unitRequired: '请选择单位', + safetyStock: '安全库存', + safetyStockPlaceholder: '请输入安全库存', + safetyStockRequired: '请输入安全库存', + remark: '备注', + stock: '库存', + description: '描述', + createdAt: '创建时间', + piece: '个', + meter: '米', + kilometer: '公里', + ton: '吨', + pole2: '根', + set: '套', + unit2: '台', + detailTitle: '商品详情', + getDetailFailed: '获取商品详情失败', + deleteSuccess: '删除成功', + deleteFailed: '删除失败', + }, + + inventory: { + stockIn: '入库', + stockOut: '出库', + recordType: '记录类型', + product: '商品', + project: '项目', + quantity: '数量', + unitPrice: '单价', + totalAmount: '总金额', + recordDate: '记录日期', + operator: '操作人', + remark: '备注', + unitLabel: '单位', + totalStockIn: '入库总量', + totalStockOut: '出库总量', + currentStock: '当前库存', + stockRecord: '库存记录', + stockSummary: '库存汇总', + selectProduct: '选择商品筛选', + selectProject: '选择项目筛选', + selectRecordType: '选择记录类型', + stockOutBtn: '出庫', + stockOutTitle: '商品出库', + relatedProject: '关联项目', + selectProjectRequired: '请选择项目', + selectProductRequired: '请选择商品', + stockOutQuantity: '出库数量', + quantityRequired: '请输入出库数量', + inputUnitPrice: '请输入单价', + inputTotalAmount: '请输入总金额', + inputRemark: '请输入备注', + stockOutSuccess: '出库成功', + stockOutFailed: '出库失败', + getListFailed: '获取库存记录失败', + title: '库存管理', + description: '管理商品库存和出入库记录', + productCode: '商品编码', + productName: '商品名称', + spec: '规格', + unit: '单位', + stock: '库存', + safetyStock: '安全库存', + locked: '锁定', + lastIn: '最后入库', + lastOut: '最后出库', + status: '状态', + normal: '正常', + lowStock: '低库存', + outOfStock: '缺货', + tabInventory: '库存管理', + tabLog: '出入库流水', + categoryFilter: '选择分类筛选', + stockFilter: '库存筛选', + all: '全部', + time: '时间', + type: '类型', + in: '入库', + out: '出库', + before: '变更前', + after: '变更后', + orderCode: '关联订单', + relatedOrder: '关联订单', + relatedOrderPlaceholder: '输入关联订单号', + date: '日期', + dateRequired: '请选择日期', + remarkPlaceholder: '请输入备注', + quantityPlaceholder: '请输入数量', + stockInTitle: '商品入库', + stockInSuccess: '入库成功', + stockInFailed: '入库失败', + getLogFailed: '获取流水记录失败', + }, + + supplier: { + title: '供应商列表', + totalCount: '供应商总数', + totalPurchase: '采购总金额', + totalPayable: '应付总金额', + searchPlaceholder: '搜索供应商编号、名称或供应类别', + newSupplier: '新增供应商', + editSupplier: '编辑供应商', + name: '名称', + supplyCategory: '供应类别', + country: '国家', + purchaseAmount: '采购金额', + payableAmount: '应付金额', + action: '操作', + contact: '联系人', + mainContact: '主联系人', + paymentInfo: '收款信息', + addContact: '+ 添加联系人', + addPaymentInfo: '+ 添加收款信息', + accountName: '收款户名', + bankName: '开户银行', + bankAccount: '银行账号', + qrCode: '收款码', + mainAccount: '主要收款账户', + deleteContact: '删除', + deletePaymentInfo: '删除此收款信息', + nameRequired: '请输入名称', + namePlaceholder: '供应商名称', + categoryPlaceholder: '手填:如电力设备、建筑材料', + china: '中国', + laos: '老挝', + remarkPlaceholder: '备注信息', + getListFailed: '获取供应商列表失败', + confirmDeleteMsg: '确定要删除此供应商吗?', + notFound: '供应商不存在', + basicInfo: '基本信息', + code: '编号', + remarkLabel: '备注:', + returnToList: '返回列表', + ledger: '业务台账', + phoneLabel: '电话:', + positionLabel: '职位:', + qrCodeLabel: '收款码:', + accountNameLabel: '户名:', + accountNumLabel: '账号:', + bankLabel: '收款银行:', + notFoundTitle: '供应商不存在', + noContact: '暂无联系人', + noPayment: '暂无收款信息', + mainContactTag: '主联系人', + mainAccountTag: '主要收款账户', + }, + + subcontractor: { + title: '分包商列表', + totalCount: '分包商总数', + totalContract: '合同总金额', + totalPayable: '应付总金额', + searchPlaceholder: '搜索分包商编号、名称或承包范围', + newSubcontractor: '新增分包商', + editSubcontractor: '编辑分包商', + name: '名称', + scope: '承包范围', + country: '国家', + contractAmount: '合同金额', + payableAmount: '应付金额', + action: '操作', + contact: '联系人', + mainContact: '主联系人', + paymentInfo: '收款信息', + addContact: '+ 添加联系人', + addPaymentInfo: '+ 添加收款信息', + accountName: '收款户名', + bankName: '开户银行', + bankAccount: '银行账号', + qrCode: '收款码', + mainAccount: '主要收款账户', + deleteContact: '删除', + deletePaymentInfo: '删除此收款信息', + nameRequired: '请输入名称', + namePlaceholder: '分包商名称', + scopePlaceholder: '手填:如电力安装、土建工程', + china: '中国', + laos: '老挝', + feature: '特点', + featurePlaceholder: '手填:如专业团队、设备齐全、价格合理等', + remarkPlaceholder: '备注信息', + getListFailed: '获取分包商列表失败', + confirmDeleteMsg: '确定要删除此分包商吗?', + notFound: '分包商不存在', + basicInfo: '基本信息', + code: '编号', + featureLabel: '特点:', + remarkLabel: '备注:', + returnToList: '返回列表', + ledger: '业务台账', + phoneLabel: '电话:', + positionLabel: '职位:', + bankLabel: '银行:', + accountLabel: '账号:', + qrCodeLabel: '二维码:', + defaultAccount: '默认账户', + notFoundTitle: '分包商不存在', + noContact: '暂无联系人', + noPayment: '暂无收款信息', + mainContactTag: '主联系人', + }, + + customer: { + title: '客户列表', + totalCount: '客户总数', + totalContract: '合同总金额', + totalReceivable: '应收总金额', + searchPlaceholder: '搜索客户编号、名称或地址', + newCustomer: '新增客户', + editCustomer: '编辑客户', + name: '名称', + address: '地址', + mainContact: '主联系人', + paymentInfo: '收款信息', + contractAmount: '合同金额', + receivableAmount: '应收金额', + action: '操作', + contact: '联系人', + addContact: '+ 添加联系人', + addPaymentInfo: '+ 添加收款信息', + accountName: '收款户名', + bankName: '开户银行', + bankAccount: '银行账号', + qrCode: '收款码', + mainAccount: '主要收款账户', + deleteContact: '删除', + deletePaymentInfo: '删除此收款信息', + nameRequired: '请输入名称', + namePlaceholder: '客户名称', + addressPlaceholder: '客户地址', + remarkPlaceholder: '备注信息', + getListFailed: '获取客户列表失败', + confirmDeleteMsg: '确定要删除此客户吗?', + notFound: '客户不存在', + basicInfo: '基本信息', + code: '编号', + remarkLabel: '备注:', + returnToList: '返回列表', + ledger: '业务台账', + relatedBudget: '关联预算', + projectName: '项目名称', + businessManager: '业务经理', + inNegotiation: '商谈中', + signed: '已签约', + unsigned: '未签约', + quotationCount: '报价版本数', + createdAt: '创建时间', + noBudget: '暂无关联预算项目', + phoneLabel: '电话:', + positionLabel: '职位:', + accountNameLabel: '户名:', + accountNumLabel: '账号:', + mainContactTag: '主联系人', + noContact: '暂无联系人', + noPayment: '暂无收款信息', + }, + + logistics: { + title: '物流管理', + description: '管理物流合作伙伴(统一合作伙伴界面规范)', + newCompany: '新建物流公司', + editCompany: '编辑物流公司', + companyName: '公司名称', + phone: '联系电话', + quoteDescription: '报价描述', + createdAt: '创建时间', + action: '操作', + confirmDelete: '确定要删除吗?', + contact: '联系人', + name: '姓名', + position: '职位', + phoneLabel: '电话', + mainContact: '主联系人', + confirmDeleteShort: '确定删除?', + paymentInfo: '收款信息', + accountName: '收款户名', + bankAccount: '银行账号', + bankName: '开户银行', + defaultAccount: '默认', + defaultLabel: '默认', + orders: '订单', + trackingNumber: '物流单号', + purchaseOrder: '采购订单', + deliveryDate: '发货日期', + freight1: '一次运费', + freight1Status: '一次运费状态', + freight2: '二次运费', + freight2Status: '二次运费状态', + status: '状态', + pendingPayment: '待付款', + applied: '已申请', + paid: '已支付', + addContact: '添加联系人', + addPaymentInfo: '添加收款信息', + basicInfo: '基本信息', + email: '邮箱', + remark: '备注', + paymentTab: '收款信息', + ledger: '业务台账', + editContact: '编辑联系人', + addContactTitle: '添加联系人', + editPaymentInfo: '编辑收款信息', + addPaymentInfoTitle: '添加收款信息', + nameRequired: '请输入姓名', + positionRequired: '请输入职位', + phoneRequired: '请输入电话', + accountNameRequired: '请输入收款户名', + bankAccountRequired: '请输入银行账号', + bankNameRequired: '请输入开户银行', + qrCodeRequired: '请输入收款码图片URL', + isMainContact: '是否为主联系人', + isDefaultAccount: '是否为默认账户', + address: '地址', + addressPlaceholder: '请输入地址', + quotePlaceholder: '请输入报价描述(如:中国-老挝陆运报价、时效等)', + remarkPlaceholder: '请输入备注', + getListFailed: '获取物流公司列表失败', + getDetailFailed: '获取物流公司详情失败', + contactUpdateSuccess: '联系人更新成功', + contactAddSuccess: '联系人添加成功', + contactDeleteSuccess: '联系人删除成功', + paymentInfoUpdateSuccess: '收款信息更新成功', + paymentInfoAddSuccess: '收款信息添加成功', + paymentInfoDeleteSuccess: '收款信息删除成功', + detailTitle: '物流公司详情 - {name}', + qrCode: '收款码', + }, + + businessLedger: { + contractTotal: '合同总金额', + totalPaid: '已付总额', + totalUnpaid: '未付总额', + projectCount: '项目数量', + purchaseTotal: '采购总额', + totalReceived: '已收总额', + totalReceivable: '应收总额', + orderCount: '订单数量', + freight1Total: '一次运费总额', + logisticsCount: '物流单数量', + code: '编号', + name: '名称', + contractAmount: '合同金额', + status: '状态', + purchaseAmount: '采购金额', + project: '项目', + freight1: '一次运费', + freight1Status: '一次运费状态', + completed: '已完成', + inProgress: '进行中', + planning: '规划中', + pending: '待处理', + approved: '已批准', + paid: '已支付', + applied: '已申请', + noRecord: '暂无业务记录', + }, + + exchangeRate: { + title: '汇率管理', + description: '设置各币种汇率,输入任意一侧自动计算', + CNYLAK: '中老汇率', + CNY: '人民币', + LAK: '老挝基普', + CNYUSD: '中美汇率', + USD: '美元', + CNYTHB: '中泰汇率', + THB: '泰铢', + USDLAK: '美老汇率', + THBLAK: '泰老汇率', + ratePair: '汇率对', + rate: '汇率', + effectiveDate: '生效日期', + setTime: '设置时间', + setBy: '设置人', + lastUpdated: '上次更新: ', + actualRate: '实际汇率: ', + confirmSave: '确认保存汇率', + historyRate: '历史汇率记录', + tipText: '提示:输入任意一侧数值,另一侧会自动计算。实际汇率实时显示为 1左侧币种 = X右侧币种。点击"确认保存汇率"按钮保存当前设置到数据库。', + getRateFailed: '获取汇率失败', + noChange: '没有汇率发生变化', + saveSuccess: '汇率保存成功', + saveFailed: '保存汇率失败', + inputFrom: '输入{from}金额', + inputTo: '输入{to}金额', + }, + + projectCost: { + title: '项目成本', + selectProject: '请选择项目查看成本统计', + contractAmount: '合同金额', + purchaseCost: '采购成本', + paymentExpense: '付款支出', + totalIncome: '总收入', + totalExpense: '总支出', + profit: '利润', + profitRate: '利润率', + totalCostBreakdown: '总成本构成', + totalCost: '总成本', + costProgress: '成本进度', + costRatio: '成本占合同比', + purchaseCategoryBreakdown: '采购成本分类', + incomeBreakdown: '收入明细', + expenseBreakdown: '支出明细', + expenseByLevel1: '支出大类汇总', + overviewTab: '成本概览', + detailsTab: '流水明细', + detail: '明细', + material: '材料', + equipment: '设备', + pole: '电杆', + other: '其他', + noData: '暂无数据', + getDataFailed: '获取成本统计失败', + }, + + systemLogs: { + title: '系统日志', + description: '查看系统操作记录和审计日志', + logId: '日志ID', + time: '时间', + level: '级别', + module: '模块', + operator: '操作人', + operation: '操作', + ipAddress: 'IP地址', + detail: '详情', + logLevel: '日志级别', + selectModule: '模块', + export: '导出', + clear: '清理', + searchPlaceholder: '搜索日志内容', + moduleUser: '用户管理', + moduleProject: '项目管理', + moduleFinance: '财务管理', + moduleSystem: '系统', + login: '用户登录', + createProject: '创建项目', + approveAdvance: '审批预支', + dataBackup: '数据备份', + }, + + about: { + title: '关于系统', + description: '系统信息与版本', + systemInfo: '系统信息', + systemName: '系统名称', + systemNameValue: '轻远电力老挝ERP', + version: '系统版本', + versionValue: 'V1.0.0', + devTeam: '开发团队', + devTeamValue: '轻远电力信息技术部', + onlineDate: '上线日期', + onlineDateValue: '2026年3月', + techArchitecture: '技术架构', + deployEnv: '部署环境', + deployEnvValue: '腾讯云服务器', + frontend: '前端框架', + frontendValue: 'Vite + React + TypeScript', + backend: '后端框架', + backendValue: 'Express.js + PostgreSQL', + modules: '功能模块', + serverStatus: '服务器状态', + databaseStatus: '数据库状态', + cpuUsage: 'CPU使用率', + memoryUsage: '内存使用', + diskSpace: '磁盘空间', + serverIp: '服务器IP', + os: '操作系统', + osValue: 'OpenCloudOS 9', + nodeVersion: 'Node版本', + running: '运行正常', + dbName: '数据库名', + connectionStatus: '连接状态', + normal: '正常', + lastBackup: '最近备份', + footer: '© 2026 轻远电力老挝ERP系统 - 版本 V1.0.0', + }, + + backup: { + title: '数据备份', + description: '管理系统数据备份与恢复', + backupName: '备份名称', + backupTime: '备份时间', + fileSize: '文件大小', + backupType: '备份类型', + auto: '自动', + manual: '手动', + status: '状态', + success: '成功', + failed: '失败', + action: '操作', + download: '下载', + restore: '恢复', + delete: '删除', + totalBackups: '总备份数', + totalSize: '总大小', + lastBackup: '最近备份', + storageSpace: '存储空间', + backupList: '备份列表', + autoBackupSetting: '自动备份设置', + immediateBackup: '立即备份', + backupCreated: '备份创建成功', + }, + + processManagement: { + title: '流程管理', + description: '配置财务申请的审批流程节点,支持自定义执行角色', + flowchart: '当前流程图', + nodeConfig: '节点配置', + applicableProcess: '适用流程', + processType: '流程类型', + desc: '说明', + status: '状态', + enabled: '已启用', + disabled: '已禁用', + sequence: '顺序', + nodeName: '节点名称', + executeRole: '执行角色', + action: '操作', + edit: '编辑', + editNode: '编辑节点:{name}', + roleApplicant: '申请人(任意角色)', + roleAdmin: '管理员', + roleFinance: '财务专员', + roleManager: '项目经理', + submitApplication: '发起申请', + approvalNode: '审批', + executePayment: '执行付款', + advanceProcess: '预支申请', + advanceProcessDesc: '员工预支款项申请流程', + reimburseProcess: '报销申请', + reimburseProcessDesc: '费用报销申请流程', + paymentProcess: '付款申请', + paymentProcessDesc: '供应商付款申请流程', + verificationProcess: '核销申请', + verificationProcessDesc: '单据核销申请流程', + nodeSaved: '节点配置已保存', + selectRole: '选择执行角色', + selectRolePlaceholder: '请选择执行角色', + warning: '⚠️ 修改执行角色会影响所有使用此流程的申请。建议在公司有财务专员后再将执行节点改为财务角色。', + tipTitle: '说明:', + tipContent: '当前流程为「申请人 → 管理员审批 → 管理员执行」,后续可在下方修改执行角色为财务专员。', + }, + + processTemplate: { + title: '工程模板管理', + basicInfo: '基本信息', + designPhase: '设计阶段', + preview: '预览确认', + templateName: '模板名称', + templateDescription: '模板描述', + namePlaceholder: '例如:配电安装工程', + descPlaceholder: '描述该模板适用的工程类型', + newTemplate: '新建模板', + editTemplate: '编辑工程模板', + phaseCount: '阶段数', + desc: '描述', + action: '操作', + copy: '复制', + delete: '删除', + confirmDelete: '确定删除?', + phaseName: '阶段名称', + phaseNamePlaceholder: '例如:物资采购', + phaseType: '阶段类型', + serial: '串行(必须等依赖项完成)', + parallel: '并行(可与相邻阶段同时进行)', + dependency: '依赖关系(哪些阶段完成后才能开始)', + subItems: '子项列表(每行一个)', + subItemsPlaceholder: '电杆采购\n变压器采购\n电缆采购', + dependencyLabel: '依赖:', + emptySubItems: '无子项', + noPhase: '暂无阶段,请点击下方添加', + addPhase: '添加阶段', + saveEdit: '保存修改', + confirmCreate: '确认创建', + save: '保存', + cancel: '取消', + prev: '上一步', + next: '下一步', + getListFailed: '获取模板列表失败', + copySuccess: '复制成功', + copyFailed: '复制失败', + deleteSuccess: '删除成功', + deleteFailed: '删除失败', + nameRequired: '请输入模板名称', + phaseRequired: '请至少添加一个阶段', + updateSuccess: '模板更新成功', + createSuccess: '模板创建成功', + phaseNameEmpty: '阶段名称不能为空', + systemPreset: '系统预设', + serialLabel: '串行', + parallelLabel: '并行', + dependencyLabelShort: '依赖:', + phaseEdit: '阶段编辑', + }, + + expenseCategory: { + title: '财务分类管理', + editCategory: '编辑分类', + addCategory: '新增分类', + id: 'ID', + level1: '一级分类', + level2Code: '二级编码', + displayName: '显示名称', + desc: '说明', + order: '排序', + status: '状态', + action: '操作', + refresh: '刷新', + add: '新增分类', + income: '收入', + projectExpense: '项目支出', + companyExpense: '公司支出', + getFailed: '获取分类失败', + enabled: '已启用', + disabled: '已禁用', + selectLevel1: '请选择', + inputLevel2: '请输入', + codePlaceholder: '如 material, salary 等', + namePlaceholder: '如 材料采购', + }, + + excelImport: { + title: 'Excel 批量导入财务记录', + templateRequirements: 'Excel 模板格式要求', + columnOrder: '列顺序:日期 | 收支类型 | 一级分类 | 二级分类 | 项目名称 | 金额 | 币种 | 汇率 | 等效人民币 | 对方名称 | 对方类型 | 人员姓名 | 描述 | 凭证编号', + formatRequirements: '收支类型:收入 / 支出 | 一级分类:收入 / 项目支出 / 公司支出 | 币种:CNY / USD / LAK / THB', + categoryRequirements: '二级分类必须使用系统中已有的分类名称(如:材料采购、工资薪酬等)', + selectFile: '选择 Excel 文件', + downloadTemplate: '下载模板文件', + templateFileName: '财务记账导入模板.xlsx', + noData: 'Excel文件没有数据行', + invalidDate: '日期为空', + invalidType: '收支类型无效: {type}', + invalidLevel1: '一级分类无效: {level1}', + invalidLevel2: '二级分类无效: {level2}', + amountPositive: '金额必须大于0', + projectRequired: '项目支出/项目收入必须填写项目名称', + parseComplete: '解析完成,共 {count} 条记录', + parseFailed: '解析Excel失败: ', + noValidData: '没有有效的数据可导入', + importComplete: '导入完成:成功 {success} 条,失败 {fail} 条', + importFailed: '导入失败: ', + rowNum: '行号', + date: '日期', + incomeExpense: '收支', + income: '收入', + expense: '支出', + level1: '一级', + projectLabel: '项目', + companyLabel: '公司', + level2: '二级', + amount: '金额', + currency: '币种', + rate: '汇率', + equivalentCNY: '等效人民币', + desc: '描述', + validation: '校验', + countPrefix: '共', + countSuffix: '条', + validPrefix: '有效', + errorPrefix: '有误', + importPrefix: '导入', + importSuffix: '条有效记录', + rowPrefix: '第', + rowSuffix: '行:', + }, + + errorBoundary: { + title: '页面加载出错', + description: '抱歉,页面渲染时发生了错误。请尝试刷新页面或联系管理员。', + errorInfo: '错误信息:', + stackTrace: '错误堆栈:', + refresh: '刷新页面', + }, + + fileUpload: { + upload: '上传', + uploading: '上传中...', + preview: '图片预览', + uploadSuccess: '上传成功', + uploadFailed: '上传失败', + }, + + component: { + phonePrefix: '电话: ', + wechatPrefix: '微信: ', + whatsappLabel: 'WhatsApp', + whatsappPlaceholder: '输入WhatsApp号码', + }, + + roles: { + title: '角色权限', + description: '管理系统角色和权限分配', + searchRole: '搜索角色', + newRole: '新增角色', + roleId: '角色ID', + roleName: '角色名称', + roleDesc: '角色描述', + permCount: '权限数量', + createdAt: '创建时间', + creator: '创建人', + action: '操作', + viewPerm: '查看权限', + edit: '编辑', + delete: '删除', + superAdmin: '超级管理员', + superAdminDesc: '拥有系统所有权限', + admin: '系统', + adminDesc: '项目管理、施工管理权限', + financeManager: '财务经理', + financeManagerDesc: '财务管理、审批权限', + employee: '员工', + employeeDesc: '查看和申请权限', + roleNameRequired: '请输入角色名称', + roleDescRequired: '请输入角色描述', + roleCreated: '角色已创建', + permConfig: '权限配置', + permProject: '项目管理', + permViewProject: '查看项目', + permCreateProject: '创建项目', + permEditProject: '编辑项目', + permDeleteProject: '删除项目', + permFinance: '财务管理', + permViewFinance: '查看财务', + permApproveAdvance: '预支审批', + permApproveReimburse: '报销审批', + permApprovePayment: '付款审批', + permProcurement: '采购管理', + permViewProcurement: '查看采购', + permCreateProcurement: '创建采购', + permApproveProcurement: '审批采购', + permSystem: '系统设置', + permUserManagement: '用户管理', + permRoleManagement: '角色管理', + permSystemConfig: '系统配置', + }, + + users: { + title: '用户管理', + newUser: '新增用户', + editUser: '编辑用户', + id: 'ID', + avatar: '头像', + username: '用户名', + name: '姓名', + email: '邮箱', + phone: '手机号', + role: '角色', + user: '用户', + action: '操作', + edit: '编辑', + resetPassword: '重置密码', + confirmDelete: '确认删除', + confirmDeleteMsg: '确定要删除用户 {name} 吗?', + usernamePlaceholder: '请输入用户名', + namePlaceholder: '请输入姓名', + selectRole: '选择角色', + selectRolePlaceholder: '请选择角色', + initialPassword: '初始密码', + initialPasswordPlaceholder: '请输入初始密码', + passwordMinLen: '密码至少6位', + newPasswordPlaceholder: '请输入新密码', + confirmPassword: '确认密码', + confirmPasswordPlaceholder: '请确认新密码', + reEnterPassword: '请再次输入新密码', + passwordMismatch: '两次输入的密码不一致', + getListFailed: '获取用户列表失败', + notAdmin: '权限不足,仅管理员可访问', + getUserFailed: '获取用户列表失败', + addSuccess: '用户已添加', + getListFailedLog: '获取用户列表失败: ', + unknownError: '未知错误', + }, + + userManagement: { + title: '用户管理', + testPage: '这是一个测试页面,用于检查API调用是否正常。', + refreshList: '刷新用户列表', + errorPrefix: '错误: ', + apiResult: 'API返回的数据:', + loadStatus: '加载状态:', + loadComplete: '加载完成', + apiFailed: 'API返回失败: ', + unknownError: '未知错误', + }, +} \ No newline at end of file diff --git a/frontend/src/pages/CustomerDetail.tsx b/frontend/src/pages/CustomerDetail.tsx index bc124a8..263958d 100644 --- a/frontend/src/pages/CustomerDetail.tsx +++ b/frontend/src/pages/CustomerDetail.tsx @@ -9,6 +9,7 @@ import { } from '@ant-design/icons' import apiClient from '../utils/request' import BusinessLedgerTab from '../components/BusinessLedgerTab' +import { useLanguageStore } from '../store/languageStore' const { Title, Text } = Typography @@ -77,6 +78,7 @@ interface BudgetProject { const CustomerDetail: React.FC = () => { const { id } = useParams<{ id: string }>() const navigate = useNavigate() + const { t, currentLanguage } = useLanguageStore() const [customer, setCustomer] = useState(null) const [budgetProjects, setBudgetProjects] = useState([]) const [loading, setLoading] = useState(true) @@ -109,30 +111,30 @@ const CustomerDetail: React.FC = () => { } if (loading) return - if (!customer) return + if (!customer) return const budgetProjectColumns = [ - { title: '项目名称', dataIndex: 'name', key: 'name', render: (v: string, record: BudgetProject) => ( + { title: t('customer.projectName'), dataIndex: 'name', key: 'name', render: (v: string, record: BudgetProject) => ( navigate(`/budget-projects/${record.id}`)} style={{ cursor: 'pointer', color: '#1890ff' }}>{v} ) }, - { title: '业务经理', dataIndex: 'manager_name', key: 'manager_name' }, - { title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: (v: string) => { + { title: t('customer.businessManager'), dataIndex: 'manager_name', key: 'manager_name' }, + { title: t('common.status'), dataIndex: 'status', key: 'status', align: 'center' as const, render: (v: string) => { const map: Record = { - negotiating: { status: 'processing', text: '商谈中' }, - signed: { status: 'success', text: '已签约' }, - unsigned: { status: 'error', text: '未签约' } + negotiating: { status: 'processing', text: t('customer.inNegotiation') }, + signed: { status: 'success', text: t('customer.signed') }, + unsigned: { status: 'error', text: t('customer.unsigned') } } const c = map[v] || { status: 'default', text: v } return } }, - { title: '报价版本数', dataIndex: 'quotations', key: 'quotations', align: 'center' as const, render: (q: Quotation[]) => (q || []).length }, - { title: '创建时间', dataIndex: 'created_at', key: 'created_at', render: (v: string) => v?.split('T')[0] || '-' } + { title: t('customer.quotationCount'), dataIndex: 'quotations', key: 'quotations', align: 'center' as const, render: (q: Quotation[]) => (q || []).length }, + { title: t('customer.createdAt'), dataIndex: 'created_at', key: 'created_at', render: (v: string) => v?.split('T')[0] || '-' } ] return (
@@ -143,42 +145,42 @@ const CustomerDetail: React.FC = () => { <Card style={{ borderRadius: 8 }}> <Tabs activeKey={activeTab} onChange={setActiveTab}> {/* TAB1: 基本信息 */} - <Tabs.TabPane tab={<span><UserOutlined /> 基本信息</span>} key="basic"> + <Tabs.TabPane tab={<span><UserOutlined /> {t('customer.basicInfo')}</span>} key="basic"> <Descriptions bordered column={{ xs: 1, sm: 2 }} size="small"> - <Descriptions.Item label="编号">{customer.code}</Descriptions.Item> - <Descriptions.Item label="地址">{customer.address || '-'}</Descriptions.Item> + <Descriptions.Item label={t('customer.code')}>{customer.code}</Descriptions.Item> + <Descriptions.Item label={t('customer.address')}>{customer.address || '-'}</Descriptions.Item> </Descriptions> {customer.remark && ( <div style={{ marginTop: 16 }}> - <Text type="secondary">备注:</Text> + <Text type="secondary">{t('customer.remarkLabel')}</Text> <div style={{ padding: 12, background: '#f6ffed', borderRadius: 4, border: '1px solid #b7eb8f', marginTop: 8 }}>{customer.remark}</div> </div> )} </Tabs.TabPane> {/* TAB2: 联系人 */} - <Tabs.TabPane tab={<span><PhoneOutlined /> 联系人</span>} key="contacts"> + <Tabs.TabPane tab={<span><PhoneOutlined /> {t('customer.contact')}</span>} key="contacts"> <Row gutter={[16, 16]}> {(customer.contacts || []).map((contact, i) => ( <Col key={i} xs={24} sm={12} lg={8}> <Card size="small" style={{ borderLeft: contact.is_primary ? '3px solid #52c41a' : '3px solid #d9d9d9', background: contact.is_primary ? '#f6ffed' : '#fff' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}> - <Text strong>{contact.name || '未命名'}</Text> - {contact.is_primary && <Tag color="green">主联系人</Tag>} + <Text strong>{contact.name || t('common.unnamed')}</Text> + {contact.is_primary && <Tag color="green">{t('customer.mainContactTag')}</Tag>} </div> <div style={{ color: '#666', fontSize: 13 }}> - {contact.position && <div>职位:{contact.position}</div>} - {contact.phone && <div>电话:{contact.phone}</div>} + {contact.position && <div>{t('customer.positionLabel')}{contact.position}</div>} + {contact.phone && <div>{t('customer.phoneLabel')}{contact.phone}</div>} </div> </Card> </Col> ))} </Row> - {(customer.contacts || []).length === 0 && <Empty description="暂无联系人" image={Empty.PRESENTED_IMAGE_SIMPLE} />} + {(customer.contacts || []).length === 0 && <Empty description={t('customer.noContact')} image={Empty.PRESENTED_IMAGE_SIMPLE} />} </Tabs.TabPane> {/* TAB3: 业务台账 */} - <Tabs.TabPane tab={<span><DollarOutlined /> 业务台账</span>} key="ledger"> + <Tabs.TabPane tab={<span><DollarOutlined /> {t('customer.ledger')}</span>} key="ledger"> <BusinessLedgerTab partnerType="customer" summary={customer.ledger?.summary || { item_count: 0, total_contract_amount: 0, total_received_amount: 0, total_receivable_amount: 0 }} @@ -187,11 +189,11 @@ const CustomerDetail: React.FC = () => { </Tabs.TabPane> {/* TAB4: 关联预算 */} - <Tabs.TabPane tab={<span><FileTextOutlined /> 关联预算</span>} key="budget"> + <Tabs.TabPane tab={<span><FileTextOutlined /> {t('customer.relatedBudget')}</span>} key="budget"> {budgetProjects.length > 0 ? ( <Table columns={budgetProjectColumns} dataSource={budgetProjects} rowKey="id" size="small" pagination={{ pageSize: 10 }} bordered /> ) : ( - <Empty description="暂无关联预算项目" image={Empty.PRESENTED_IMAGE_SIMPLE} /> + <Empty description={t('customer.noBudget')} image={Empty.PRESENTED_IMAGE_SIMPLE} /> )} </Tabs.TabPane> </Tabs> @@ -200,4 +202,4 @@ const CustomerDetail: React.FC = () => { ) } -export default CustomerDetail +export default CustomerDetail \ No newline at end of file diff --git a/frontend/src/pages/CustomersPage.tsx b/frontend/src/pages/CustomersPage.tsx index e402937..1013d0a 100644 --- a/frontend/src/pages/CustomersPage.tsx +++ b/frontend/src/pages/CustomersPage.tsx @@ -5,6 +5,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, HomeOutline import type { ColumnsType } from 'antd/es/table' import FileUpload from '../components/FileUpload' import useFormDraft from '../hooks/useFormDraft' +import { useLanguageStore } from '../store/languageStore' interface Contact { name: string @@ -37,6 +38,7 @@ interface Customer { const CustomerPage: React.FC = () => { const navigate = useNavigate() + const { t, currentLanguage } = useLanguageStore() const [customers, setCustomers] = useState<Customer[]>([]) const [loading, setLoading] = useState(false) const [modalVisible, setModalVisible] = useState(false) @@ -61,7 +63,7 @@ const CustomerPage: React.FC = () => { const data = await response.json() if (data.success) setCustomers(data.data || []) } catch (error) { - message.error('获取客户列表失败') + message.error(t('customer.getListFailed')) } finally { setLoading(false) } @@ -87,32 +89,32 @@ const CustomerPage: React.FC = () => { const columns: ColumnsType<Customer> = [ { - title: '名称', dataIndex: 'name', key: 'name', + title: t('customer.name'), dataIndex: 'name', key: 'name', render: (text, record) => ( <Button type="link" style={{ padding: 0, fontWeight: 'bold' }} onClick={() => navigate(`/customers/${record.id}`)}>{text}</Button> ) }, - { title: '地址', dataIndex: 'address', key: 'address', width: 150, render: (t) => t || '-' }, - { title: '主联系人', key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) }, + { title: t('customer.address'), dataIndex: 'address', key: 'address', width: 150, render: (t) => t || '-' }, + { title: t('customer.mainContact'), key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) }, { - title: '收款信息', + title: t('customer.paymentInfo'), key: 'payment_info', width: 200, render: (_, record) => { const primary = getPrimaryPaymentInfo(record.payment_infos || []) - if (!primary) return <Tag>未设置</Tag> + if (!primary) return <Tag>{t('common.notSet')}</Tag> return ( <div style={{ fontSize: 12 }}> <div><BankOutlined /> {primary.bank_name || '-'}</div> - <div>户名: {primary.account_name || '-'}</div> - <div>账号: {primary.bank_account ? primary.bank_account.slice(-4).padStart(primary.bank_account.length, '*') : '-'}</div> + <div>{t('customer.accountNameLabel')} {primary.account_name || '-'}</div> + <div>{t('customer.accountNumLabel')} {primary.bank_account ? primary.bank_account.slice(-4).padStart(primary.bank_account.length, '*') : '-'}</div> </div> ) } }, - { title: '合同金额', dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` }, - { title: '应收金额', dataIndex: 'total_receivable', key: 'total_receivable', width: 100, render: (v) => <span style={{ color: v > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(v || 0).toLocaleString()}</span> }, - { title: '操作', key: 'actions', width: 100, render: (_, record) => ( + { title: t('customer.contractAmount'), dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` }, + { title: t('customer.receivableAmount'), dataIndex: 'total_receivable', key: 'total_receivable', width: 100, render: (v) => <span style={{ color: v > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(v || 0).toLocaleString()}</span> }, + { title: t('common.action'), key: 'actions', width: 100, render: (_, record) => ( <Space> <Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} size="small" /> <Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} size="small" /> @@ -170,17 +172,17 @@ const CustomerPage: React.FC = () => { }) const data = await response.json() if (data.success) { - message.success(editingCustomer ? '更新成功' : '创建成功') + message.success(editingCustomer ? t('common.updateSuccess') : t('common.createSuccess')) clearDraft() setModalVisible(false) form.resetFields() setEditingCustomer(null) fetchCustomers() } else { - message.error(data.message || '操作失败') + message.error(data.message || t('common.operationFailed')) } } catch (error) { - message.error('操作失败') + message.error(t('common.operationFailed')) } } @@ -198,14 +200,14 @@ const CustomerPage: React.FC = () => { const handleDelete = async (id: number) => { Modal.confirm({ - title: '确认删除', content: '确定要删除此客户吗?', okText: '确定', cancelText: '取消', + title: t('common.confirmDelete'), content: t('customer.confirmDeleteMsg'), okText: t('common.confirm'), cancelText: t('common.cancel'), onOk: async () => { try { const response = await fetch(`/api/customers/${id}`, { method: 'DELETE' }) const data = await response.json() - if (data.success) { message.success('删除成功'); fetchCustomers() } - else message.error(data.message || '删除失败') - } catch (error) { message.error('删除失败') } + if (data.success) { message.success(t('common.deleteSuccess')); fetchCustomers() } + else message.error(data.message || t('common.deleteFailed')) + } catch (error) { message.error(t('common.deleteFailed')) } } }) } @@ -222,10 +224,10 @@ const CustomerPage: React.FC = () => { setTimeout(() => { if (hasDraft()) { Modal.confirm({ - title: '发现未完成的草稿', - content: '检测到上次未提交的客户信息,是否恢复?', - okText: '恢复草稿', - cancelText: '重新填写', + title: t('common.draftFound'), + content: t('common.draftRestore'), + okText: t('common.restoreDraft'), + cancelText: t('common.reFill'), onOk: () => { restoreDraft() }, @@ -245,74 +247,75 @@ const CustomerPage: React.FC = () => { return ( <div style={{ padding: 24 }}> <Row gutter={16} style={{ marginBottom: 24 }}> - <Col span={8}><Card><Statistic title="客户总数" value={stats.total} prefix={<HomeOutlined />} /></Card></Col> - <Col span={8}><Card><Statistic title="合同总金额" value={stats.totalContract} prefix="¥" valueStyle={{ color: '#1890ff' }} /></Card></Col> - <Col span={8}><Card><Statistic title="应收总金额" value={stats.totalReceivable} prefix="¥" valueStyle={{ color: stats.totalReceivable > 0 ? '#ff4d4f' : '#52c41a' }} /></Card></Col> + <Col span={8}><Card><Statistic title={t('customer.totalCount')} value={stats.total} prefix={<HomeOutlined />} /></Card></Col> + <Col span={8}><Card><Statistic title={t('customer.totalContract')} value={stats.totalContract} prefix="¥" valueStyle={{ color: '#1890ff' }} /></Card></Col> + <Col span={8}><Card><Statistic title={t('customer.totalReceivable')} value={stats.totalReceivable} prefix="¥" valueStyle={{ color: stats.totalReceivable > 0 ? '#ff4d4f' : '#52c41a' }} /></Card></Col> </Row> <Card style={{ marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> - <Input placeholder="搜索客户编号、名称或地址" prefix={<SearchOutlined />} value={searchText} onChange={(e) => setSearchText(e.target.value)} allowClear style={{ width: 350 }} /> - <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增客户</Button> + <Input placeholder={t('customer.searchPlaceholder')} prefix={<SearchOutlined />} value={searchText} onChange={(e) => setSearchText(e.target.value)} allowClear style={{ width: 350 }} /> + <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>{t('customer.newCustomer')}</Button> </div> </Card> <Card> - <Table columns={columns} dataSource={filteredCustomers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 1100 }} /> + <Table columns={columns} dataSource={filteredCustomers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => t('common.totalCount', { total }) }} scroll={{ x: 1100 }} /> </Card> - <Modal - title={editingCustomer ? '编辑客户' : '新增客户'} - open={modalVisible} + <Modal + title={editingCustomer ? t('customer.editCustomer') : t('customer.newCustomer')} + open={modalVisible} onCancel={() => { if (form.isFieldsTouched()) { Modal.confirm({ - title: '确认关闭', - content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?', - okText: '关闭', - cancelText: '继续编辑', + title: t('common.closeConfirm'), + content: t('common.closeConfirmMsg'), + okText: t('common.close'), + cancelText: t('common.continueEdit'), onOk: () => { saveDraft() + form.resetFields(); setModalVisible(false) - form.resetFields() setEditingCustomer(null) }, }) } else { + form.resetFields(); setModalVisible(false) - form.resetFields() setEditingCustomer(null) } - }} - onOk={() => form.submit()} + }} + onOk={() => form.submit()} + destroyOnClose width={800} maskClosable={false} > <Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}> - <Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}> - <Input placeholder="客户名称" /> + <Form.Item name="name" label={t('customer.name')} rules={[{ required: true, message: t('customer.nameRequired') }]}> + <Input placeholder={t('customer.namePlaceholder')} /> </Form.Item> - <Form.Item name="address" label="地址"> - <Input placeholder="客户地址" /> + <Form.Item name="address" label={t('customer.address')}> + <Input placeholder={t('customer.addressPlaceholder')} /> </Form.Item> - <Form.Item name="remark" label="备注"> - <Input.TextArea rows={2} placeholder="备注信息" /> + <Form.Item name="remark" label={t('common.remark')}> + <Input.TextArea rows={2} placeholder={t('customer.remarkPlaceholder')} /> </Form.Item> - <h4>联系人</h4> + <h4>{t('customer.contact')}</h4> <Form.List name="contacts" initialValue={[{ name: '', position: '', phone: '', is_primary: true }]}> {(fields, { add, remove }) => ( <div> {fields.map(({ key, name, ...restField }) => ( <div key={key} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}> <Form.Item {...restField} name={[name, 'name']} style={{ marginBottom: 0, flex: 1 }}> - <Input placeholder="姓名" /> + <Input placeholder={t('logistics.name')} /> </Form.Item> <Form.Item {...restField} name={[name, 'position']} style={{ marginBottom: 0, flex: 1 }}> - <Input placeholder="职位" /> + <Input placeholder={t('common.position')} /> </Form.Item> <Form.Item {...restField} name={[name, 'phone']} style={{ marginBottom: 0, flex: 1 }}> - <Input placeholder="电话" /> + <Input placeholder={t('common.phone')} /> </Form.Item> <div style={{ display: 'flex', alignItems: 'center' }}> <Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked" style={{ marginBottom: 0, marginRight: 8 }}> @@ -321,33 +324,33 @@ const CustomerPage: React.FC = () => { onChange={(e) => handleContactChange(name, 'is_primary', e.target.checked)} /> </Form.Item> - <span>主联系人</span> + <span>{t('customer.mainContact')}</span> </div> - {fields.length > 1 && <Button type="link" danger onClick={() => remove(name)}>删除</Button>} + {fields.length > 1 && <Button type="link" danger onClick={() => remove(name)}>{t('common.delete')}</Button>} </div> ))} - <Button type="dashed" onClick={() => add()} style={{ width: '100%' }}>+ 添加联系人</Button> + <Button type="dashed" onClick={() => add()} style={{ width: '100%' }}>{t('customer.addContact')}</Button> </div> )} </Form.List> - <h4 style={{ marginTop: 24 }}>收款信息</h4> + <h4 style={{ marginTop: 24 }}>{t('customer.paymentInfo')}</h4> <Form.List name="payment_infos" initialValue={[]}> {(fields, { add, remove }) => ( <div> {fields.map(({ key, name, ...restField }) => ( <div key={key} style={{ border: '1px solid #e8e8e8', padding: 16, marginBottom: 16, borderRadius: 4, backgroundColor: '#fafafa' }}> <div style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}> - <Form.Item {...restField} name={[name, 'account_name']} label="收款户名" style={{ marginBottom: 0, flex: 1 }}> - <Input placeholder="收款户名" /> + <Form.Item {...restField} name={[name, 'account_name']} label={t('customer.accountName')} style={{ marginBottom: 0, flex: 1 }}> + <Input placeholder={t('customer.accountName')} /> </Form.Item> - <Form.Item {...restField} name={[name, 'bank_name']} label="开户银行" style={{ marginBottom: 0, flex: 1 }}> - <Input placeholder="开户银行" /> + <Form.Item {...restField} name={[name, 'bank_name']} label={t('customer.bankName')} style={{ marginBottom: 0, flex: 1 }}> + <Input placeholder={t('customer.bankName')} /> </Form.Item> </div> <div style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}> - <Form.Item {...restField} name={[name, 'bank_account']} label="银行账号" style={{ marginBottom: 0, flex: 1 }}> - <Input placeholder="银行账号" /> + <Form.Item {...restField} name={[name, 'bank_account']} label={t('customer.bankAccount')} style={{ marginBottom: 0, flex: 1 }}> + <Input placeholder={t('customer.bankAccount')} /> </Form.Item> <div style={{ display: 'flex', alignItems: 'center', marginTop: 30 }}> <Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked" style={{ marginBottom: 0, marginRight: 8 }}> @@ -356,19 +359,19 @@ const CustomerPage: React.FC = () => { onChange={(e) => handlePaymentInfoChange(name, 'is_primary', e.target.checked)} /> </Form.Item> - <span>主要收款账户</span> + <span>{t('customer.mainAccount')}</span> </div> </div> - <Form.Item {...restField} name={[name, 'qr_code']} label="收款码" style={{ marginBottom: 0 }}> + <Form.Item {...restField} name={[name, 'qr_code']} label={t('customer.qrCode')} style={{ marginBottom: 0 }}> <FileUpload maxCount={1} accept="image/*" /> </Form.Item> {fields.length > 0 && ( - <Button type="link" danger onClick={() => remove(name)} style={{ marginTop: 8 }}>删除此收款信息</Button> + <Button type="link" danger onClick={() => remove(name)} style={{ marginTop: 8 }}>{t('customer.deletePaymentInfo')}</Button> )} </div> ))} <Button type="dashed" onClick={() => add({ account_name: '', bank_account: '', bank_name: '', is_primary: false })} style={{ width: '100%' }}> - + 添加收款信息 + {t('customer.addPaymentInfo')} </Button> </div> )} @@ -379,4 +382,4 @@ const CustomerPage: React.FC = () => { ) } -export default CustomerPage +export default CustomerPage \ No newline at end of file diff --git a/frontend/src/pages/ExchangeRatePage.tsx b/frontend/src/pages/ExchangeRatePage.tsx index b3fec0f..d51d6c7 100644 --- a/frontend/src/pages/ExchangeRatePage.tsx +++ b/frontend/src/pages/ExchangeRatePage.tsx @@ -1,380 +1,378 @@ -import React, { useState, useEffect } from 'react'; -import { Card, Row, Col, InputNumber, message, Typography, Divider, Spin, Button, Table, Space, Tag } from 'antd'; -import { CheckOutlined, HistoryOutlined } from '@ant-design/icons'; -import apiClient from '../utils/request'; -import dayjs from 'dayjs'; - -const { Text, Title } = Typography; - -const RATE_PAIRS = [ - { key: 'CNY_LAK', label: '中老汇率', from: 'CNY', to: 'LAK', fromLabel: '人民币', toLabel: '老挝基普' }, - { key: 'CNY_USD', label: '中美汇率', from: 'CNY', to: 'USD', fromLabel: '人民币', toLabel: '美元' }, - { key: 'CNY_THB', label: '中泰汇率', from: 'CNY', to: 'THB', fromLabel: '人民币', toLabel: '泰铢' }, - { key: 'USD_LAK', label: '美老汇率', from: 'USD', to: 'LAK', fromLabel: '美元', toLabel: '老挝基普' }, - { key: 'THB_LAK', label: '泰老汇率', from: 'THB', to: 'LAK', fromLabel: '泰铢', toLabel: '老挝基普' }, -]; - -interface RateItem { - leftValue: number; - rightValue: number; - actualRate: number; -} - -interface HistoryRate { - id: number; - pair_key: string; - rate: number; - effective_date: string; - created_at: string; - created_by_name?: string; -} - -const ExchangeRatePage: React.FC = () => { - const [rates, setRates] = useState<Record<string, RateItem>>({}); - const [initialRates, setInitialRates] = useState<Record<string, number>>({}); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [isMobile, setIsMobile] = useState(false); - const [historyRates, setHistoryRates] = useState<HistoryRate[]>([]); - const [lastUpdateTime, setLastUpdateTime] = useState<string>(''); - - useEffect(() => { - const checkMobile = () => setIsMobile(window.innerWidth <= 768); - checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); - }, []); - - useEffect(() => { - fetchRates(); - fetchHistory(); - }, []); - - const fetchRates = async () => { - setLoading(true); - try { - const res = await apiClient.get('/exchange-rates/latest'); - if (res.data.success) { - const data = res.data.data; - const newRates: Record<string, RateItem> = {}; - const newInitialRates: Record<string, number> = {}; - RATE_PAIRS.forEach(pair => { - const rate = parseFloat(data[pair.key]) || 1; - newRates[pair.key] = { leftValue: 1, rightValue: rate, actualRate: rate }; - newInitialRates[pair.key] = rate; - }); - setRates(newRates); - setInitialRates(newInitialRates); - - if (res.data.updated_at) { - setLastUpdateTime(res.data.updated_at); - } - } - } catch (error) { - message.error('获取汇率失败'); - const defaultRates: Record<string, RateItem> = {}; - const defaultInitialRates: Record<string, number> = {}; - RATE_PAIRS.forEach(pair => { - const defaultRate = pair.key === 'CNY_LAK' ? 3000 : pair.key === 'CNY_USD' ? 0.14 : pair.key === 'CNY_THB' ? 4.5 : pair.key === 'USD_LAK' ? 21000 : 670; - defaultRates[pair.key] = { leftValue: 1, rightValue: defaultRate, actualRate: defaultRate }; - defaultInitialRates[pair.key] = defaultRate; - }); - setRates(defaultRates); - setInitialRates(defaultInitialRates); - } finally { - setLoading(false); - } - }; - - const fetchHistory = async () => { - try { - const res = await apiClient.get('/exchange-rates/history?limit=20'); - if (res.data.success) { - setHistoryRates(res.data.data); - } - } catch (error) { - console.error('获取历史汇率失败:', error); - } - }; - - // 左侧输入 - 右侧自动变为1,重新计算汇率 - const handleLeftChange = (key: string, value: number | null) => { - if (value === null || value <= 0) return; - const pair = RATE_PAIRS.find(p => p.key === key); - if (!pair) return; - - // 当左侧输入值时,右侧变为1,计算新的汇率 - const newRate = 1 / value; - - setRates(prev => ({ - ...prev, - [key]: { - leftValue: value, - rightValue: 1, - actualRate: newRate - } - })); - }; - - // 右侧输入 - 左侧自动变为1,重新计算汇率 - const handleRightChange = (key: string, value: number | null) => { - if (value === null || value <= 0) return; - const pair = RATE_PAIRS.find(p => p.key === key); - if (!pair) return; - - // 当右侧输入值时,左侧变为1,计算新的汇率 - const newRate = value; - - setRates(prev => ({ - ...prev, - [key]: { - leftValue: 1, - rightValue: value, - actualRate: newRate - } - })); - }; - - // 计算实际汇率显示 - const getActualRateDisplay = (key: string) => { - const item = rates[key]; - if (!item) return '1 : 1.00'; - - const pair = RATE_PAIRS.find(p => p.key === key); - const actualRate = item.actualRate; - - // 根据汇率对选择合适的小数位数 - const decimalPlaces = pair?.key === 'CNY_USD' ? 5 : 2; - - return `1 ${pair?.from} = ${actualRate.toFixed(decimalPlaces)} ${pair?.to}`; - }; - - // 确认保存 - const handleConfirm = async () => { - setSaving(true); - try { - const savePromises = RATE_PAIRS.map(pair => { - const item = rates[pair.key]; - if (!item) return null; - - const actualRate = item.rightValue / item.leftValue; - const initialRate = initialRates[pair.key]; - - // 只保存有变化的汇率 - if (Math.abs(actualRate - initialRate) < 0.0001) { - return null; - } - - return apiClient.post('/exchange-rates', { - pair_key: pair.key, - rate: actualRate, - effective_date: dayjs().format('YYYY-MM-DD') - }); - }); - - const validPromises = savePromises.filter(Boolean) as Promise<any>[]; - - if (validPromises.length === 0) { - message.info('没有汇率发生变化'); - setSaving(false); - return; - } - - await Promise.all(validPromises); - - message.success('汇率保存成功'); - setLastUpdateTime(dayjs().format('YYYY-MM-DD HH:mm:ss')); - fetchHistory(); - // 更新初始汇率为当前汇率 - const newInitialRates: Record<string, number> = {}; - RATE_PAIRS.forEach(pair => { - const item = rates[pair.key]; - if (item) { - newInitialRates[pair.key] = item.rightValue / item.leftValue; - } - }); - setInitialRates(newInitialRates); - } catch (error) { - message.error('保存汇率失败'); - } finally { - setSaving(false); - } - }; - - // 历史汇率表格列 - const historyColumns = [ - { - title: '汇率对', - dataIndex: 'from_currency', - key: 'from_currency', - render: (_: string, record: HistoryRate) => { - const pairKey = `${record.from_currency}_${record.to_currency}`; - const pair = RATE_PAIRS.find(p => p.key === pairKey); - return pair?.label || pairKey; - } - }, - { - title: '汇率', - dataIndex: 'rate', - key: 'rate', - render: (rate: number, record: HistoryRate) => { - const pairKey = `${record.from_currency}_${record.to_currency}`; - const pair = RATE_PAIRS.find(p => p.key === pairKey); - return `1 ${record.from_currency} = ${parseFloat(rate).toFixed(pair?.key === 'CNY_USD' ? 4 : 2)} ${record.to_currency}`; - } - }, - { - title: '生效日期', - dataIndex: 'effective_date', - key: 'effective_date', - render: (date: string) => dayjs(date).format('YYYY-MM-DD') - }, - { - title: '设置时间', - dataIndex: 'created_at', - key: 'created_at', - render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm') - }, - { - title: '设置人', - dataIndex: 'created_by_name', - key: 'created_by_name', - render: (name: string) => name || '-' - } - ]; - - if (loading) { - return <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}><Spin size="large" /></div>; - } - - return ( - <div style={{ padding: isMobile ? 8 : 24 }}> - <div style={{ marginBottom: isMobile ? 12 : 24 }}> - <Title level={2} style={{ marginBottom: 8 }}>汇率管理 - - 设置各币种汇率,输入任意一侧自动计算 - {lastUpdateTime && ( - 上次更新: {lastUpdateTime} - )} - -
- - - {RATE_PAIRS.map(pair => { - const item = rates[pair.key]; - if (!item) return null; - return ( - - -
-
-
{pair.fromLabel}
- handleLeftChange(pair.key, v)} - precision={6} - size="large" - min={0.000001} - onFocus={(e) => { - if (e.target && e.target.select) { - e.target.select(); - } - }} - placeholder={`输入${pair.fromLabel}金额`} - /> -
-
=
-
-
{pair.toLabel}
- handleRightChange(pair.key, v)} - precision={pair.key === 'CNY_USD' ? 4 : 2} - size="large" - min={0.000001} - onFocus={(e) => { - if (e.target && e.target.select) { - e.target.select(); - } - }} - placeholder={`输入${pair.toLabel}金额`} - /> -
-
- -
- - 实际汇率: {getActualRateDisplay(pair.key)} - -
-
- - ); - })} -
- - {/* 确认按钮 */} -
- -
- - {/* 历史汇率表 */} - - - 历史汇率记录 - - } - style={{ marginTop: 24 }} - > - - - - - - 提示:输入任意一侧数值,另一侧会自动计算。实际汇率实时显示为 1左侧币种 = X右侧币种。点击"确认保存汇率"按钮保存当前设置到数据库。 - - - - ); -}; - -export default ExchangeRatePage; +import React, { useState, useEffect, useMemo } from 'react'; +import { Card, Row, Col, InputNumber, message, Typography, Divider, Spin, Button, Table, Space, Tag } from 'antd'; +import { CheckOutlined, HistoryOutlined } from '@ant-design/icons'; +import apiClient from '../utils/request'; +import dayjs from 'dayjs'; +import { useLanguageStore } from '../store/languageStore'; + +const { Text, Title } = Typography; + +const RATE_PAIR_KEYS = [ + { key: 'CNY_LAK', i18nKey: 'CNYLAK', from: 'CNY', to: 'LAK', fromI18n: 'CNY', toI18n: 'LAK' }, + { key: 'CNY_USD', i18nKey: 'CNYUSD', from: 'CNY', to: 'USD', fromI18n: 'CNY', toI18n: 'USD' }, + { key: 'CNY_THB', i18nKey: 'CNYTHB', from: 'CNY', to: 'THB', fromI18n: 'CNY', toI18n: 'THB' }, + { key: 'USD_LAK', i18nKey: 'USDLAK', from: 'USD', to: 'LAK', fromI18n: 'USD', toI18n: 'LAK' }, + { key: 'THB_LAK', i18nKey: 'THBLAK', from: 'THB', to: 'LAK', fromI18n: 'THB', toI18n: 'LAK' }, +]; + +interface RateItem { + leftValue: number; + rightValue: number; + actualRate: number; +} + +interface HistoryRate { + id: number; + pair_key: string; + rate: number; + effective_date: string; + created_at: string; + created_by_name?: string; +} + +const ExchangeRatePage: React.FC = () => { + const { t, currentLanguage } = useLanguageStore(); + + const RATE_PAIRS = useMemo(() => RATE_PAIR_KEYS.map(p => ({ + ...p, + label: t(`exchangeRate.${p.i18nKey}`), + fromLabel: t(`exchangeRate.${p.fromI18n}`), + toLabel: t(`exchangeRate.${p.toI18n}`), + })), [t]); + + const [rates, setRates] = useState>({}); + const [initialRates, setInitialRates] = useState>({}); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [historyRates, setHistoryRates] = useState([]); + const [lastUpdateTime, setLastUpdateTime] = useState(''); + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= 768); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + useEffect(() => { + fetchRates(); + fetchHistory(); + }, []); + + const fetchRates = async () => { + setLoading(true); + try { + const res = await apiClient.get('/exchange-rates/latest'); + if (res.data.success) { + const data = res.data.data; + const newRates: Record = {}; + const newInitialRates: Record = {}; + RATE_PAIRS.forEach(pair => { + const rate = parseFloat(data[pair.key]) || 1; + newRates[pair.key] = { leftValue: 1, rightValue: rate, actualRate: rate }; + newInitialRates[pair.key] = rate; + }); + setRates(newRates); + setInitialRates(newInitialRates); + + if (res.data.updated_at) { + setLastUpdateTime(res.data.updated_at); + } + } + } catch (error) { + message.error(t('exchangeRate.getRateFailed')); + const defaultRates: Record = {}; + const defaultInitialRates: Record = {}; + RATE_PAIRS.forEach(pair => { + const defaultRate = pair.key === 'CNY_LAK' ? 3000 : pair.key === 'CNY_USD' ? 0.14 : pair.key === 'CNY_THB' ? 4.5 : pair.key === 'USD_LAK' ? 21000 : 670; + defaultRates[pair.key] = { leftValue: 1, rightValue: defaultRate, actualRate: defaultRate }; + defaultInitialRates[pair.key] = defaultRate; + }); + setRates(defaultRates); + setInitialRates(defaultInitialRates); + } finally { + setLoading(false); + } + }; + + const fetchHistory = async () => { + try { + const res = await apiClient.get('/exchange-rates/history?limit=20'); + if (res.data.success) { + setHistoryRates(res.data.data); + } + } catch (error) { + console.error('获取历史汇率失败:', error); + } + }; + + const handleLeftChange = (key: string, value: number | null) => { + if (value === null || value <= 0) return; + const pair = RATE_PAIRS.find(p => p.key === key); + if (!pair) return; + + const newRate = 1 / value; + + setRates(prev => ({ + ...prev, + [key]: { + leftValue: value, + rightValue: 1, + actualRate: newRate + } + })); + }; + + const handleRightChange = (key: string, value: number | null) => { + if (value === null || value <= 0) return; + const pair = RATE_PAIRS.find(p => p.key === key); + if (!pair) return; + + const newRate = value; + + setRates(prev => ({ + ...prev, + [key]: { + leftValue: 1, + rightValue: value, + actualRate: newRate + } + })); + }; + + const getActualRateDisplay = (key: string) => { + const item = rates[key]; + if (!item) return '1 : 1.00'; + + const pair = RATE_PAIRS.find(p => p.key === key); + const actualRate = item.actualRate; + + const decimalPlaces = pair?.key === 'CNY_USD' ? 5 : 2; + + return `1 ${pair?.from} = ${actualRate.toFixed(decimalPlaces)} ${pair?.to}`; + }; + + const handleConfirm = async () => { + setSaving(true); + try { + const savePromises = RATE_PAIRS.map(pair => { + const item = rates[pair.key]; + if (!item) return null; + + const actualRate = item.rightValue / item.leftValue; + const initialRate = initialRates[pair.key]; + + if (Math.abs(actualRate - initialRate) < 0.0001) { + return null; + } + + return apiClient.post('/exchange-rates', { + pair_key: pair.key, + rate: actualRate, + effective_date: dayjs().format('YYYY-MM-DD') + }); + }); + + const validPromises = savePromises.filter(Boolean) as Promise[]; + + if (validPromises.length === 0) { + message.info(t('exchangeRate.noChange')); + setSaving(false); + return; + } + + await Promise.all(validPromises); + + message.success(t('exchangeRate.saveSuccess')); + setLastUpdateTime(dayjs().format('YYYY-MM-DD HH:mm:ss')); + fetchHistory(); + const newInitialRates: Record = {}; + RATE_PAIRS.forEach(pair => { + const item = rates[pair.key]; + if (item) { + newInitialRates[pair.key] = item.rightValue / item.leftValue; + } + }); + setInitialRates(newInitialRates); + } catch (error) { + message.error(t('exchangeRate.saveFailed')); + } finally { + setSaving(false); + } + }; + + const historyColumns = [ + { + title: t('exchangeRate.ratePair'), + dataIndex: 'from_currency', + key: 'from_currency', + render: (_: string, record: HistoryRate) => { + const pairKey = `${record.from_currency}_${record.to_currency}`; + const pair = RATE_PAIRS.find(p => p.key === pairKey); + return pair?.label || pairKey; + } + }, + { + title: t('exchangeRate.rate'), + dataIndex: 'rate', + key: 'rate', + render: (rate: number, record: HistoryRate) => { + const pairKey = `${record.from_currency}_${record.to_currency}`; + const pair = RATE_PAIRS.find(p => p.key === pairKey); + return `1 ${record.from_currency} = ${parseFloat(rate).toFixed(pair?.key === 'CNY_USD' ? 4 : 2)} ${record.to_currency}`; + } + }, + { + title: t('exchangeRate.effectiveDate'), + dataIndex: 'effective_date', + key: 'effective_date', + render: (date: string) => dayjs(date).format('YYYY-MM-DD') + }, + { + title: t('exchangeRate.setTime'), + dataIndex: 'created_at', + key: 'created_at', + render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm') + }, + { + title: t('exchangeRate.setBy'), + dataIndex: 'created_by_name', + key: 'created_by_name', + render: (name: string) => name || '-' + } + ]; + + if (loading) { + return
; + } + + return ( +
+
+ {t('exchangeRate.title')} + + {t('exchangeRate.description')} + {lastUpdateTime && ( + {t('exchangeRate.lastUpdated')}{lastUpdateTime} + )} + +
+ + + {RATE_PAIRS.map(pair => { + const item = rates[pair.key]; + if (!item) return null; + return ( +
+ +
+
+
{pair.fromLabel}
+ handleLeftChange(pair.key, v)} + precision={6} + size="large" + min={0.000001} + onFocus={(e) => { + if (e.target && e.target.select) { + e.target.select(); + } + }} + placeholder={t('exchangeRate.inputFrom', { from: pair.fromLabel })} + /> +
+
=
+
+
{pair.toLabel}
+ handleRightChange(pair.key, v)} + precision={pair.key === 'CNY_USD' ? 4 : 2} + size="large" + min={0.000001} + onFocus={(e) => { + if (e.target && e.target.select) { + e.target.select(); + } + }} + placeholder={t('exchangeRate.inputTo', { to: pair.toLabel })} + /> +
+
+ +
+ + {t('exchangeRate.actualRate')}{getActualRateDisplay(pair.key)} + +
+
+ + ); + })} + + +
+ +
+ + + + {t('exchangeRate.historyRate')} + + } + style={{ marginTop: 24 }} + > +
+ + + + + {t('exchangeRate.tipText')} + + + + ); +}; + +export default ExchangeRatePage; \ No newline at end of file diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx index 04197bb..4eb1dff 100644 --- a/frontend/src/pages/InventoryPage.tsx +++ b/frontend/src/pages/InventoryPage.tsx @@ -1,289 +1,116 @@ import React, { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' import { Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card, - Row, Col, Statistic, DatePicker, InputNumber, Tabs + Row, Col, DatePicker, InputNumber, Tabs, Statistic, Descriptions, Badge, Tooltip, Divider } from 'antd' import { - PlusOutlined, SearchOutlined, InboxOutlined, ExportOutlined + PlusOutlined, EditOutlined, EditFilled, DeleteOutlined, EyeOutlined, + CheckOutlined, WarningOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' +import dayjs from 'dayjs' +import { useLanguageStore } from '../store/languageStore' -// ==================== 类型定义 ==================== -interface InventoryRecord { +interface InventoryItem { id: number - record_type: string - project_id?: number | null - project_name?: string - purchase_request_id?: number | null - product_id: number - product_name?: string - quantity: number - unit_price?: number | null - total_amount?: number | null - record_date: string - operator?: string | null - remark?: string | null - created_at: string -} - -interface InventorySummary { product_id: number product_name: string - unit?: string | null - total_in: number - total_out: number - current_quantity: number + product_code: string + product_category: string + product_specification: string + product_unit: string + stock_quantity: number + safety_stock: number + locked_quantity: number + last_in_date: string + last_out_date: string +} + +interface InventoryLog { + id: number + product_id: number + product_name: string + type: string + quantity: number + before_quantity: number + after_quantity: number + purchase_order_id: number + order_code: string + remark: string + operator: string + created_at: string } interface Product { id: number name: string + code: string + category: string + specification: string + unit: string + stock_quantity: number + safety_stock: number } -interface Project { - id: number - name: string -} - -// ==================== 组件 ==================== const InventoryPage: React.FC = () => { - // 状态 - const [records, setRecords] = useState([]) - const [summary, setSummary] = useState([]) + const { t, currentLanguage } = useLanguageStore(); + const [inventory, setInventory] = useState([]) + const [logs, setLogs] = useState([]) const [loading, setLoading] = useState(false) + const [logLoading, setLogLoading] = useState(false) const [products, setProducts] = useState([]) - const [projects, setProjects] = useState([]) - // 筛选状态 - const [selectedProductId, setSelectedProductId] = useState(null) - const [selectedProjectId, setSelectedProjectId] = useState(null) - const [selectedRecordType, setSelectedRecordType] = useState(null) + const [selectedCategory, setSelectedCategory] = useState(null) + const [stockFilter, setStockFilter] = useState('all') + const [activeTab, setActiveTab] = useState('inventory') - // 弹窗状态 + const [inModalVisible, setInModalVisible] = useState(false) const [outModalVisible, setOutModalVisible] = useState(false) - const [form] = Form.useForm() + const [inForm] = Form.useForm() + const [outForm] = Form.useForm() - // Tab状态 - const [activeTab, setActiveTab] = useState('records') - - // ==================== 渲染 ==================== - - const getRecordTypeTag = (type: string) => { - if (type === 'in') { - return }>入库 - } else { - return }>出库 - } - } - - const recordColumns: ColumnsType = [ - { - title: '记录类型', - dataIndex: 'record_type', - key: 'record_type', - width: 100, - render: getRecordTypeTag - }, - { - title: '商品', - dataIndex: 'product_name', - key: 'product_name' - }, - { - title: '项目', - dataIndex: 'project_name', - key: 'project_name' - }, - { - title: '数量', - dataIndex: 'quantity', - key: 'quantity', - width: 100 - }, - { - title: '单价', - dataIndex: 'unit_price', - key: 'unit_price', - width: 120, - render: (price) => price ? price.toFixed(2) : '-' - }, - { - title: '总金额', - dataIndex: 'total_amount', - key: 'total_amount', - width: 120, - render: (amount) => amount ? amount.toFixed(2) : '-' - }, - { - title: '记录日期', - dataIndex: 'record_date', - key: 'record_date', - width: 120 - }, - { - title: '操作人', - dataIndex: 'operator', - key: 'operator', - width: 120 - }, - { - title: '备注', - dataIndex: 'remark', - key: 'remark' - } - ] - - const summaryColumns: ColumnsType = [ - { - title: '商品', - dataIndex: 'product_name', - key: 'product_name' - }, - { - title: '单位', - dataIndex: 'unit', - key: 'unit', - width: 80 - }, - { - title: '入库总量', - dataIndex: 'total_in', - key: 'total_in', - width: 120, - render: (val) => (val || 0).toFixed(2) - }, - { - title: '出库总量', - dataIndex: 'total_out', - key: 'total_out', - width: 120, - render: (val) => (val || 0).toFixed(2) - }, - { - title: '当前库存', - dataIndex: 'current_quantity', - key: 'current_quantity', - width: 120, - render: (val) => ( - - {(val || 0).toFixed(2)} - - ) - } - ] - - const tabItems = [ - { - key: 'records', - label: '库存记录', - children: ( - <> - - - - - - - - - - - - - - - -
- - ) - }, - { - key: 'summary', - label: '库存汇总', - children: ( -
- ) - } - ] - - // ==================== 数据加载 ==================== - - const fetchRecords = async () => { + const navigate = useNavigate() + + const fetchInventory = async () => { setLoading(true) try { const params = new URLSearchParams() - if (selectedProductId) params.append('product_id', selectedProductId.toString()) - if (selectedProjectId) params.append('project_id', selectedProjectId.toString()) - if (selectedRecordType) params.append('record_type', selectedRecordType) + if (selectedCategory) params.append('category', selectedCategory) + if (stockFilter && stockFilter !== 'all') params.append('stock_filter', stockFilter) const response = await fetch(`/api/inventory?${params}`) const data = await response.json() if (data.success) { - setRecords(data.data) + setInventory(data.data) } else { - message.error('获取库存记录失败') + message.error(t('inventory.getListFailed')) } } catch (error) { - console.error('获取库存记录失败:', error) - message.error('获取库存记录失败') + console.error('获取库存列表失败:', error) + message.error(t('inventory.getListFailed')) } finally { setLoading(false) } } - const fetchSummary = async () => { + const fetchLogs = async () => { + setLogLoading(true) try { - const response = await fetch('/api/inventory/summary') + const response = await fetch('/api/inventory?limit=100') const data = await response.json() if (data.success) { - setSummary(data.data) + setLogs(data.data) + } else { + message.error(t('inventory.getLogFailed')) } } catch (error) { - console.error('获取库存汇总失败:', error) + console.error('获取库存流水失败:', error) + message.error(t('inventory.getLogFailed')) + } finally { + setLogLoading(false) } } @@ -295,127 +122,455 @@ const InventoryPage: React.FC = () => { setProducts(data.data) } } catch (error) { - console.error('获取商品列表失败:', error) - } - } - - const fetchProjects = async () => { - try { - const response = await fetch('/api/projects') - const data = await response.json() - if (data.success) { - setProjects(data.data) - } - } catch (error) { - console.error('获取项目列表失败:', error) + console.error('获取产品列表失败:', error) } } useEffect(() => { fetchProducts() - fetchProjects() }, []) useEffect(() => { - if (activeTab === 'records') { - fetchRecords() - } else { - fetchSummary() - } - }, [activeTab, selectedProductId, selectedProjectId, selectedRecordType]) + fetchInventory() + }, [selectedCategory, stockFilter]) - // ==================== 操作函数 ==================== + useEffect(() => { + fetchLogs() + }, [activeTab]) - const handleOutModalOk = async () => { + const handleStockIn = () => { + inForm.resetFields() + inForm.setFieldsValue({ type: 'in', date: dayjs() }) + setInModalVisible(true) + } + + const handleStockOut = () => { + outForm.resetFields() + outForm.setFieldsValue({ type: 'out', date: dayjs() }) + setOutModalVisible(true) + } + + const handleSaveStockIn = async () => { try { - const values = await form.validateFields() + const values = await inForm.validateFields() + const data = { + ...values, + date: values.date.format('YYYY-MM-DD HH:mm:ss') + } + + const response = await fetch('/api/inventory/in', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + const result = await response.json() + + if (result.success) { + message.success(t('inventory.stockInSuccess')) + setInModalVisible(false) + fetchInventory() + } else { + message.error(result.error || t('common.operationFailed')) + } + } catch (error) { + console.error('入库失败:', error) + message.error(t('inventory.stockInFailed')) + } + } + + const handleSaveStockOut = async () => { + try { + const values = await outForm.validateFields() + const data = { + ...values, + date: values.date.format('YYYY-MM-DD HH:mm:ss') + } const response = await fetch('/api/inventory/out', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ...values, - operator: '系统管理员' - }) + body: JSON.stringify(data) }) + const result = await response.json() - const data = await response.json() - - if (data.success) { - message.success('出库成功') + if (result.success) { + message.success(t('inventory.stockOutSuccess')) setOutModalVisible(false) - form.resetFields() - fetchRecords() - fetchSummary() + fetchInventory() } else { - message.error('出库失败') + message.error(result.error || t('common.operationFailed')) } } catch (error) { console.error('出库失败:', error) - message.error('出库失败') + message.error(t('inventory.stockOutFailed')) } } + + const getStockStatus = (item: InventoryItem) => { + if (item.stock_quantity <= 0) return 'out_of_stock' + if (item.stock_quantity <= item.safety_stock) return 'low_stock' + return 'normal' + } + + const columns: ColumnsType = [ + { + title: t('inventory.productCode'), + dataIndex: 'product_code', + key: 'product_code', + width: 110 + }, + { + title: t('inventory.productName'), + dataIndex: 'product_name', + key: 'product_name', + width: 150 + }, + { + title: t('inventory.spec'), + dataIndex: 'product_specification', + key: 'product_specification', + width: 130, + ellipsis: true + }, + { + title: t('inventory.unit'), + dataIndex: 'product_unit', + key: 'product_unit', + width: 60 + }, + { + title: t('inventory.stock'), + dataIndex: 'stock_quantity', + key: 'stock_quantity', + width: 100, + align: 'right', + render: (v: number, r: InventoryItem) => { + const status = getStockStatus(r) + const color = status === 'out_of_stock' ? '#ff4d4f' : (status === 'low_stock' ? '#faad14' : '#52c41a') + return {v} + } + }, + { + title: t('inventory.safetyStock'), + dataIndex: 'safety_stock', + key: 'safety_stock', + width: 80, + align: 'right' + }, + { + title: t('inventory.locked'), + dataIndex: 'locked_quantity', + key: 'locked_quantity', + width: 80, + align: 'right', + render: (v: number) => v || 0 + }, + { + title: t('inventory.lastIn'), + dataIndex: 'last_in_date', + key: 'last_in_date', + width: 100, + render: (v: string) => v ? dayjs(v).format('MM-DD') : '-' + }, + { + title: t('inventory.lastOut'), + dataIndex: 'last_out_date', + key: 'last_out_date', + width: 100, + render: (v: string) => v ? dayjs(v).format('MM-DD') : '-' + }, + { + title: t('inventory.status'), + key: 'status', + width: 80, + align: 'center', + render: (_, r: InventoryItem) => { + const status = getStockStatus(r) + const map: Record = { + normal: { icon: , text: t('inventory.normal'), color: 'success' }, + low_stock: { icon: , text: t('inventory.lowStock'), color: 'warning' }, + out_of_stock: { icon: , text: t('inventory.outOfStock'), color: 'error' } + } + const info = map[status] || map.normal + return {info.text} + } + } + ] + const logColumns: ColumnsType = [ + { + title: t('inventory.time'), + dataIndex: 'created_at', + key: 'created_at', + width: 160, + render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm') + }, + { + title: t('inventory.product'), + dataIndex: 'product_name', + key: 'product_name', + width: 120 + }, + { + title: t('inventory.type'), + dataIndex: 'type', + key: 'type', + width: 60, + align: 'center', + render: (v: string) => ( + + {v === 'in' ? t('inventory.in') : t('inventory.out')} + + ) + }, + { + title: t('inventory.quantity'), + dataIndex: 'quantity', + key: 'quantity', + width: 80, + align: 'right' + }, + { + title: t('inventory.before'), + dataIndex: 'before_quantity', + key: 'before_quantity', + width: 80, + align: 'right' + }, + { + title: t('inventory.after'), + dataIndex: 'after_quantity', + key: 'after_quantity', + width: 80, + align: 'right' + }, + { + title: t('inventory.orderCode'), + dataIndex: 'order_code', + key: 'order_code', + width: 120, + render: (v: string) => v || '-' + }, + { + title: t('inventory.remark'), + dataIndex: 'remark', + key: 'remark', + width: 150, + ellipsis: true, + render: (v: string) => v || '-' + }, + { + title: t('inventory.operator'), + dataIndex: 'operator', + key: 'operator', + width: 80 + } + ] + + const categories = React.useMemo(() => { + const cats = new Set() + inventory.forEach(item => { + if (item.product_category) cats.add(item.product_category) + }) + return Array.from(cats) + }, [inventory]) + return (
- - - +
+

{t('inventory.title')}

+

{t('inventory.description')}

+
+ + + + + + } + > + + + +
+ + + + + + +
+ + +
+ + + + + {/* 入库弹窗 */} + { + inForm.resetFields(); + setInModalVisible(false); + }} + destroyOnClose + width={500} + > +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + {/* 出库弹窗 */} setOutModalVisible(false)} + onOk={handleSaveStockOut} + onCancel={() => { + outForm.resetFields(); + setOutModalVisible(false); + }} + destroyOnClose + width={500} > -
- - + + +
+ + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + @@ -423,4 +578,4 @@ const InventoryPage: React.FC = () => { ) } -export default InventoryPage +export default InventoryPage \ No newline at end of file diff --git a/frontend/src/pages/LogisticsCompaniesPage.tsx b/frontend/src/pages/LogisticsCompaniesPage.tsx index b7ea420..dc047c2 100644 --- a/frontend/src/pages/LogisticsCompaniesPage.tsx +++ b/frontend/src/pages/LogisticsCompaniesPage.tsx @@ -22,6 +22,7 @@ import type { ColumnsType } from 'antd/es/table' import dayjs from 'dayjs' import BusinessLedgerTab from '../components/BusinessLedgerTab' import useFormDraft from '../hooks/useFormDraft' +import { useLanguageStore } from '../store/languageStore' interface LogisticsCompany { id: number @@ -85,6 +86,7 @@ interface OrderRecord { } const LogisticsCompaniesPage: React.FC = () => { + const { t, currentLanguage } = useLanguageStore() const [companies, setCompanies] = useState([]) const [loading, setLoading] = useState(false) @@ -123,11 +125,11 @@ const LogisticsCompaniesPage: React.FC = () => { if (data.success) { setCompanies(data.data) } else { - message.error('获取物流公司列表失败') + message.error(t('logistics.getListFailed')) } } catch (error) { console.error('获取物流公司列表失败:', error) - message.error('获取物流公司列表失败') + message.error(t('logistics.getListFailed')) } finally { setLoading(false) } @@ -142,11 +144,11 @@ const LogisticsCompaniesPage: React.FC = () => { setDetailModalVisible(true) setActiveDetailTab('basic') } else { - message.error('获取物流公司详情失败') + message.error(t('logistics.getDetailFailed')) } } catch (error) { console.error('获取物流公司详情失败:', error) - message.error('获取物流公司详情失败') + message.error(t('logistics.getDetailFailed')) } } @@ -162,10 +164,10 @@ const LogisticsCompaniesPage: React.FC = () => { setTimeout(() => { if (hasDraft()) { Modal.confirm({ - title: '发现未完成的草稿', - content: '检测到上次未提交的物流公司信息,是否恢复?', - okText: '恢复草稿', - cancelText: '重新填写', + title: t('common.draftFound'), + content: t('common.draftRestore'), + okText: t('common.restoreDraft'), + cancelText: t('common.reFill'), onOk: () => { restoreDraft() }, @@ -189,14 +191,14 @@ const LogisticsCompaniesPage: React.FC = () => { const response = await fetch(`/api/logistics-companies/${id}`, { method: 'DELETE' }) const data = await response.json() if (data.success) { - message.success('删除成功') + message.success(t('common.deleteSuccess')) fetchCompanies() } else { - message.error(data.message || '删除失败') + message.error(data.message || t('common.deleteFailed')) } } catch (error) { console.error('删除失败:', error) - message.error('删除失败') + message.error(t('common.deleteFailed')) } } @@ -230,12 +232,12 @@ const LogisticsCompaniesPage: React.FC = () => { } } } - message.success(editingCompany ? '更新成功' : '创建成功') + message.success(editingCompany ? t('common.updateSuccess') : t('common.createSuccess')) clearDraft() setModalVisible(false) fetchCompanies() } else { - message.error('保存失败') + message.error(t('common.saveFailed')) } } catch (error) { console.error('保存失败:', error) @@ -270,11 +272,11 @@ const LogisticsCompaniesPage: React.FC = () => { const data = await response.json() if (data.success) { - message.success(editingContact ? '联系人更新成功' : '联系人添加成功') + message.success(editingContact ? t('logistics.contactUpdateSuccess') : t('logistics.contactAddSuccess')) setContactModalVisible(false) fetchCompanyDetail(currentCompany!.id) } else { - message.error('操作失败') + message.error(t('common.operationFailed')) } } catch (error) { console.error('保存联系人失败:', error) @@ -288,10 +290,10 @@ const LogisticsCompaniesPage: React.FC = () => { }) const data = await response.json() if (data.success) { - message.success('联系人删除成功') + message.success(t('logistics.contactDeleteSuccess')) fetchCompanyDetail(currentCompany!.id) } else { - message.error('删除失败') + message.error(t('common.deleteFailed')) } } catch (error) { console.error('删除联系人失败:', error) @@ -326,11 +328,11 @@ const LogisticsCompaniesPage: React.FC = () => { const data = await response.json() if (data.success) { - message.success(editingPayment ? '收款信息更新成功' : '收款信息添加成功') + message.success(editingPayment ? t('logistics.paymentInfoUpdateSuccess') : t('logistics.paymentInfoAddSuccess')) setPaymentModalVisible(false) fetchCompanyDetail(currentCompany!.id) } else { - message.error('操作失败') + message.error(t('common.operationFailed')) } } catch (error) { console.error('保存收款信息失败:', error) @@ -344,10 +346,10 @@ const LogisticsCompaniesPage: React.FC = () => { }) const data = await response.json() if (data.success) { - message.success('收款信息删除成功') + message.success(t('logistics.paymentInfoDeleteSuccess')) fetchCompanyDetail(currentCompany!.id) } else { - message.error('删除失败') + message.error(t('common.deleteFailed')) } } catch (error) { console.error('删除收款信息失败:', error) @@ -356,9 +358,9 @@ const LogisticsCompaniesPage: React.FC = () => { const getFreightStatusTag = (status: string) => { const statusMap: Record = { - pending: { color: 'default', text: '待付款' }, - requested: { color: 'blue', text: '已申请' }, - paid: { color: 'green', text: '已支付' } + pending: { color: 'default', text: t('logistics.pendingPayment') }, + requested: { color: 'blue', text: t('logistics.applied') }, + paid: { color: 'green', text: t('logistics.paid') } } const info = statusMap[status] || { color: 'default', text: status } return {info.text} @@ -366,7 +368,7 @@ const LogisticsCompaniesPage: React.FC = () => { const columns: ColumnsType = [ { - title: '公司名称', + title: t('logistics.companyName'), dataIndex: 'name', key: 'name', width: 180, @@ -375,13 +377,13 @@ const LogisticsCompaniesPage: React.FC = () => { ) }, { - title: '联系电话', + title: t('logistics.phone'), dataIndex: 'phone', key: 'phone', width: 120 }, { - title: '报价描述', + title: t('logistics.quoteDescription'), dataIndex: 'quotation_description', key: 'quotation_description', width: 200, @@ -389,14 +391,14 @@ const LogisticsCompaniesPage: React.FC = () => { render: (v: string) => v || '-' }, { - title: '创建时间', + title: t('logistics.createdAt'), dataIndex: 'created_at', key: 'created_at', width: 100, render: (v: string) => v ? dayjs(v).format('MM-DD') : '-' }, { - title: '操作', + title: t('logistics.action'), key: 'actions', width: 150, fixed: 'right', @@ -404,7 +406,7 @@ const LogisticsCompaniesPage: React.FC = () => { }> + } onClick={handleCreate}>{t('logistics.newCompany')}}>
{/* 编辑/新建弹窗 */} { if (form.isFieldsTouched()) { Modal.confirm({ - title: '确认关闭', - content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?', - okText: '关闭', - cancelText: '继续编辑', + title: t('common.closeConfirm'), + content: t('common.closeConfirmMsg'), + okText: t('common.close'), + cancelText: t('common.continueEdit'), onOk: () => { saveDraft() + form.resetFields(); setModalVisible(false) }, }) } else { + form.resetFields(); setModalVisible(false) } }} + destroyOnClose width={700} maskClosable={false} >
- - + + - - + + - - + + - - + + {!editingCompany && ( {(fields, { add, remove }) => ( <>
- 联系人 - + {t('logistics.contact')} +
{fields.map(({ key, name, ...restField }) => (
- - + + - + - + - 主联系人 + {t('logistics.mainContact')} @@ -555,7 +560,7 @@ const LogisticsCompaniesPage: React.FC = () => { {/* 详情弹窗 - 多TAB */} setDetailModalVisible(false)} footer={null} @@ -564,32 +569,32 @@ const LogisticsCompaniesPage: React.FC = () => { {currentCompany && ( {/* TAB1: 基本信息 */} - 基本信息} key="basic"> + {t('logistics.basicInfo')}} key="basic"> - {currentCompany.name} - {currentCompany.phone || '-'} - {currentCompany.email || '-'} - {currentCompany.created_at} - {currentCompany.address || '-'} - {currentCompany.quotation_description || '-'} - {currentCompany.remark && {currentCompany.remark}} + {currentCompany.name} + {currentCompany.phone || '-'} + {currentCompany.email || '-'} + {currentCompany.created_at} + {currentCompany.address || '-'} + {currentCompany.quotation_description || '-'} + {currentCompany.remark && {currentCompany.remark}} {/* TAB2: 联系人 */} - 联系人} key="contacts"> - + {t('logistics.contact')}} key="contacts"> +
{/* TAB3: 收款信息 */} - 收款信息} key="payment"> - + {t('logistics.paymentTab')}} key="payment"> +
{/* TAB4: 业务台账 */} - 业务台账} key="orders"> + {t('logistics.ledger')}} key="orders"> { {/* 联系人编辑弹窗 */} setContactModalVisible(false)} + onCancel={() => { + contactForm.resetFields(); + setContactModalVisible(false); + }} + destroyOnClose width={500} > - - + + - - + + - - + + - - + {t('common.is')} + {t('common.no')} @@ -639,35 +648,39 @@ const LogisticsCompaniesPage: React.FC = () => { {/* 收款信息编辑弹窗 */} setPaymentModalVisible(false)} + onCancel={() => { + paymentForm.resetFields(); + setPaymentModalVisible(false); + }} + destroyOnClose width={500} >
- - + +
- - + + - - + + - - + + - - + {t('common.is')} + {t('common.no')} @@ -676,4 +689,4 @@ const LogisticsCompaniesPage: React.FC = () => { ) } -export default LogisticsCompaniesPage +export default LogisticsCompaniesPage \ No newline at end of file diff --git a/frontend/src/pages/PaymentPlansPage.tsx b/frontend/src/pages/PaymentPlansPage.tsx index 7fedba7..c41af9b 100644 --- a/frontend/src/pages/PaymentPlansPage.tsx +++ b/frontend/src/pages/PaymentPlansPage.tsx @@ -10,6 +10,7 @@ import { } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' import dayjs from 'dayjs' +import { useLanguageStore } from '../store/languageStore' // ==================== 类型定义 ==================== interface PaymentPlan { @@ -37,6 +38,7 @@ interface PurchaseOrder { // ==================== 组件 ==================== const PaymentPlansPage: React.FC = () => { + const { t, currentLanguage } = useLanguageStore(); // 状态 const [paymentPlans, setPaymentPlans] = useState([]) const [loading, setLoading] = useState(false) @@ -63,11 +65,11 @@ const PaymentPlansPage: React.FC = () => { if (data.success) { setPaymentPlans(data.data) } else { - message.error('获取付款计划列表失败') + message.error(t('paymentPlan.getListFailed')) } } catch (error) { console.error('获取付款计划列表失败:', error) - message.error('获取付款计划列表失败') + message.error(t('paymentPlan.getListFailed')) } finally { setLoading(false) } @@ -93,11 +95,11 @@ const PaymentPlansPage: React.FC = () => { setViewingPlan(data.data) setDetailModalVisible(true) } else { - message.error('获取付款计划详情失败') + message.error(t('paymentPlan.getDetailFailed')) } } catch (error) { console.error('获取付款计划详情失败:', error) - message.error('获取付款计划详情失败') + message.error(t('paymentPlan.getDetailFailed')) } } @@ -119,7 +121,7 @@ const PaymentPlansPage: React.FC = () => { currency: 'CNY', payment_type: 'partial', status: 'pending', - created_by: '系统管理员' + created_by: t('common.systemAdmin') }) setModalVisible(true) } @@ -150,15 +152,15 @@ const PaymentPlansPage: React.FC = () => { payment_type: fullRecord.payment_type || 'partial', status: fullRecord.status || 'pending', description: fullRecord.description, - created_by: fullRecord.created_by || '系统管理员' + created_by: fullRecord.created_by || t('common.systemAdmin') }); }, 100); } else { - message.error('获取付款计划详情失败') + message.error(t('paymentPlan.getDetailFailed')) } } catch (error) { console.error('获取付款计划详情失败:', error) - message.error('获取付款计划详情失败') + message.error(t('paymentPlan.getDetailFailed')) } } @@ -191,15 +193,15 @@ const PaymentPlansPage: React.FC = () => { const data = await response.json() if (data.success) { - message.success(editingPlan ? '保存成功' : '创建成功') + message.success(editingPlan ? t('common.saveSuccess') : t('common.createSuccess')) setModalVisible(false) fetchPaymentPlans() } else { - message.error(editingPlan ? '保存失败' : '创建失败') + message.error(editingPlan ? t('common.saveFailed') : t('common.operationFailed')) } } catch (error) { console.error('保存失败:', error) - message.error('保存失败') + message.error(t('common.saveFailed')) } } @@ -207,10 +209,10 @@ const PaymentPlansPage: React.FC = () => { const getStatusTag = (status: string) => { const statusMap: Record = { - pending: { color: 'blue', text: '待处理' }, - approved: { color: 'green', text: '已审批' }, - executed: { color: 'purple', text: '已执行' }, - cancelled: { color: 'red', text: '已取消' } + pending: { color: 'blue', text: t('paymentPlan.pending') }, + approved: { color: 'green', text: t('paymentPlan.approved') }, + executed: { color: 'purple', text: t('paymentPlan.executed') }, + cancelled: { color: 'red', text: t('paymentPlan.cancelled') } } const info = statusMap[status] || { color: 'default', text: status } return {info.text} @@ -218,8 +220,8 @@ const PaymentPlansPage: React.FC = () => { const getPaymentTypeTag = (type: string) => { const typeMap: Record = { - partial: { color: 'blue', text: '部分付款' }, - full: { color: 'green', text: '全额付款' } + partial: { color: 'blue', text: t('paymentPlan.partialPayment') }, + full: { color: 'green', text: t('paymentPlan.fullPayment') } } const info = typeMap[type] || { color: 'default', text: type } return {info.text} @@ -227,14 +229,14 @@ const PaymentPlansPage: React.FC = () => { const columns: ColumnsType = [ { - title: '计划编号', + title: t('paymentPlan.planCode'), dataIndex: 'code', key: 'code', width: 150, ellipsis: true }, { - title: '采购订单', + title: t('paymentPlan.purchaseOrder'), dataIndex: 'purchase_order_id', key: 'purchase_order_id', width: 140, @@ -244,14 +246,14 @@ const PaymentPlansPage: React.FC = () => { } }, { - title: '付款日期', + title: t('paymentPlan.paymentDate'), dataIndex: 'payment_date', key: 'payment_date', width: 110, render: (date) => dayjs(date).format('MM-DD') }, { - title: '金额', + title: t('paymentPlan.amount'), dataIndex: 'amount', key: 'amount', width: 120, @@ -263,7 +265,7 @@ const PaymentPlansPage: React.FC = () => { ) }, { - title: '付款类型', + title: t('paymentPlan.paymentType'), dataIndex: 'payment_type', key: 'payment_type', width: 100, @@ -271,7 +273,7 @@ const PaymentPlansPage: React.FC = () => { render: getPaymentTypeTag }, { - title: '状态', + title: t('paymentPlan.status'), dataIndex: 'status', key: 'status', width: 90, @@ -279,13 +281,13 @@ const PaymentPlansPage: React.FC = () => { render: getStatusTag }, { - title: '创建人', + title: t('paymentPlan.creator'), dataIndex: 'created_by', key: 'created_by', width: 100 }, { - title: '操作', + title: t('paymentPlan.action'), key: 'actions', width: 150, fixed: 'right', @@ -297,7 +299,7 @@ const PaymentPlansPage: React.FC = () => { icon={} onClick={() => fetchPlanDetail(record.id)} > - 详情 + {t('common.detail')} ) @@ -315,11 +317,11 @@ const PaymentPlansPage: React.FC = () => { return (
-

付款计划

-

管理采购订单的付款计划

+

{t('paymentPlan.title')}

+

{t('paymentPlan.description')}

- } onClick={handleCreate}>新建付款计划}> + } onClick={handleCreate}>{t('paymentPlan.newPlan')}}>
{ {/* 编辑/新建弹窗 */} { - setModalVisible(false) - setEditingPlan(null) + form.resetFields(); + setModalVisible(false); + setEditingPlan(null); }} footer={[ , - + form.resetFields(); + setModalVisible(false); + setEditingPlan(null); + }}>{t('common.cancel')}, + ]} + destroyOnClose width={600} >
@@ -353,10 +358,10 @@ const PaymentPlansPage: React.FC = () => {
- {purchaseOrders.map(order => ( {order.code} - {order.supplier_name} ({order.currency} {order.total_amount}) @@ -371,8 +376,8 @@ const PaymentPlansPage: React.FC = () => { @@ -380,10 +385,10 @@ const PaymentPlansPage: React.FC = () => { - + @@ -392,26 +397,26 @@ const PaymentPlansPage: React.FC = () => { - + {t('paymentPlan.currencyCNY')} + {t('paymentPlan.currencyUSD')} + {t('paymentPlan.currencyLAK')} + {t('paymentPlan.currencyTHB')} - + {t('paymentPlan.partialPayment')} + {t('paymentPlan.fullPayment')} @@ -421,40 +426,40 @@ const PaymentPlansPage: React.FC = () => { - + {t('paymentPlan.pending')} + {t('paymentPlan.approved')} + {t('paymentPlan.executed')} + {t('paymentPlan.cancelled')} - + - + {/* 详情弹窗 */} setDetailModalVisible(false)} footer={null} @@ -463,20 +468,20 @@ const PaymentPlansPage: React.FC = () => { {viewingPlan && ( <> - {viewingPlan.code} - {getStatusTag(viewingPlan.status)} - + {viewingPlan.code} + {getStatusTag(viewingPlan.status)} + {(() => { const order = purchaseOrders.find(o => o.id == viewingPlan.purchase_order_id) return order ? order.code : viewingPlan.purchase_order_id })()} - {getPaymentTypeTag(viewingPlan.payment_type)} - {viewingPlan.payment_date} - {viewingPlan.currency} - {viewingPlan.amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} - {viewingPlan.description || '-'} - {viewingPlan.created_by} + {getPaymentTypeTag(viewingPlan.payment_type)} + {viewingPlan.payment_date} + {viewingPlan.currency} + {viewingPlan.amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + {viewingPlan.description || '-'} + {viewingPlan.created_by} )} diff --git a/frontend/src/pages/PaymentRequestsPage.tsx b/frontend/src/pages/PaymentRequestsPage.tsx index da97875..486b6e0 100644 --- a/frontend/src/pages/PaymentRequestsPage.tsx +++ b/frontend/src/pages/PaymentRequestsPage.tsx @@ -5,6 +5,7 @@ import dayjs from 'dayjs'; import { useAuthStore } from '../store/authStore'; import FileUpload from '../components/FileUpload'; import useFormDraft from '../hooks/useFormDraft'; +import { useLanguageStore } from '../store/languageStore'; const { Option } = Select; const { TextArea } = Input; @@ -59,6 +60,7 @@ interface PayeeEntity { } const PaymentRequestsPage: React.FC = () => { + const { t, currentLanguage } = useLanguageStore(); const { user } = useAuthStore(); const [requests, setRequests] = useState([]); const [completedRequests, setCompletedRequests] = useState([]); @@ -116,7 +118,7 @@ const PaymentRequestsPage: React.FC = () => { } } catch (error) { console.error('获取付款申请列表失败:', error); - message.error('获取付款申请列表失败'); + message.error(t('paymentRequest.getListFailed')); } finally { setLoading(false); } @@ -217,19 +219,21 @@ const PaymentRequestsPage: React.FC = () => { form.setFieldsValue({ application_date: dayjs(), currency: 'CNY', - applicant: user?.name || user?.username || '当前用户', + applicant: user?.name || user?.username || t('common.currentUser'), attachments: [], payee_type: 'other', expense_type: 'company' }); + setWatchedAmount(null); + setWatchedCurrency('CNY'); setModalVisible(true); setTimeout(() => { if (hasDraft()) { Modal.confirm({ - title: '发现未完成的草稿', - content: '检测到上次未提交的付款申请,是否恢复?', - okText: '恢复草稿', - cancelText: '重新填写', + title: t('common.draftFound'), + content: t('common.draftRestore'), + okText: t('common.restoreDraft'), + cancelText: t('common.reFill'), onOk: () => { restoreDraft(); }, @@ -239,7 +243,7 @@ const PaymentRequestsPage: React.FC = () => { form.setFieldsValue({ application_date: dayjs(), currency: 'CNY', - applicant: user?.name || user?.username || '当前用户', + applicant: user?.name || user?.username || t('common.currentUser'), attachments: [], payee_type: 'other', expense_type: 'company' @@ -257,6 +261,8 @@ const PaymentRequestsPage: React.FC = () => { application_date: record.application_date ? dayjs(record.application_date) : (record.payment_date ? dayjs(record.payment_date) : null), attachments: record.attachments || [] }); + setWatchedAmount(record.amount || null); + setWatchedCurrency(record.currency || 'CNY'); setModalVisible(true); }; @@ -267,15 +273,15 @@ const PaymentRequestsPage: React.FC = () => { const handleDelete = async (id: number) => { Modal.confirm({ - title: '确认删除', - content: '确定要删除这条付款申请吗?', + title: t('common.deleteConfirm'), + content: t('paymentRequest.deleteConfirmMsg') || t('common.confirmDeleteMsg'), onOk: async () => { try { await fetch('/api/payment-requests/' + id, { method: 'DELETE' }); - message.success('删除成功'); + message.success(t('paymentRequest.deleteSuccess')); fetchRequests(); } catch (error) { - message.error('删除失败'); + message.error(t('paymentRequest.deleteFailed')); } } }); @@ -283,15 +289,15 @@ const PaymentRequestsPage: React.FC = () => { const handleWithdraw = async (id: number) => { Modal.confirm({ - title: '确认撤回', - content: '撤回后可重新编辑提交,确认撤回吗?', + title: t('paymentRequest.withdrawConfirm'), + content: t('paymentRequest.withdrawConfirmMsg'), onOk: async () => { try { await fetch('/api/payment-requests/' + id + '/withdraw', { method: 'POST' }); - message.success('已撤回,可重新编辑'); + message.success(t('paymentRequest.withdrawSuccess')); fetchRequests(); } catch (error) { - message.error('撤回失败'); + message.error(t('paymentRequest.withdrawFailed')); } } }); @@ -342,32 +348,56 @@ const PaymentRequestsPage: React.FC = () => { }); const result = await res.json(); if (result.success) { - message.success(editingId ? '更新成功' : '创建成功'); + message.success(editingId ? t('common.updateSuccess') : t('common.createSuccess')); clearDraft(); setModalVisible(false); fetchRequests(); } else { - message.error(result.error || '操作失败'); + message.error(result.error || t('common.operationFailed')); } } catch (error) { - message.error('操作失败'); + message.error(t('common.operationFailed')); } }; const convertToCNY = (amount: number, curr: string): number => { if (curr === "CNY") return amount; + // 先尝试 XXX_CNY 格式 const rateKey = curr + "_CNY"; - const rate = exchangeRates[rateKey] || 1; - return amount * rate; + if (exchangeRates[rateKey]) { + return amount * exchangeRates[rateKey]; + } + // 尝试 CNY_XXX 格式的倒数 + const reverseKey = "CNY_" + curr; + if (exchangeRates[reverseKey]) { + return amount / exchangeRates[reverseKey]; + } + // 尝试通过 USD 中转: XXX -> USD -> CNY + const xxxUsdKey = curr + "_USD"; + const usdCnyKey = "USD_CNY"; + const cnyUsdKey = "CNY_USD"; + if (exchangeRates[xxxUsdKey]) { + const usdAmount = amount * exchangeRates[xxxUsdKey]; + if (exchangeRates[usdCnyKey]) return usdAmount * exchangeRates[usdCnyKey]; + if (exchangeRates[cnyUsdKey]) return usdAmount / exchangeRates[cnyUsdKey]; + } + // 通过 LAK 中转 + const xxxLakKey = curr + "_LAK"; + const cnyLakKey = "CNY_LAK"; + if (exchangeRates[xxxLakKey] && exchangeRates[cnyLakKey]) { + const lakAmount = amount * exchangeRates[xxxLakKey]; + return lakAmount / exchangeRates[cnyLakKey]; + } + return amount; }; const getStatusTag = (status: string) => { const statusMap: Record = { - pending: { color: 'processing', text: '待审批' }, - approved: { color: 'success', text: '已批准' }, - rejected: { color: 'error', text: '已退回' }, - withdrawn: { color: 'default', text: '已撤回' }, - paid: { color: 'blue', text: '已付款' }, + pending: { color: 'processing', text: t('paymentRequest.pendingApproval') }, + approved: { color: 'success', text: t('paymentRequest.approved') }, + rejected: { color: 'error', text: t('paymentRequest.rejected') }, + withdrawn: { color: 'default', text: t('paymentRequest.withdrawn') }, + paid: { color: 'blue', text: t('paymentRequest.paid') }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; @@ -393,48 +423,48 @@ const PaymentRequestsPage: React.FC = () => { }; const columns = [ - { title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => handleView(r)}>{v} }, - { title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 }, - { title: '收款单位', dataIndex: 'payee', key: 'payee', width: 150, ellipsis: true }, - { title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => ( + { title: t('paymentRequest.subject'), dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => handleView(r)}>{v} }, + { title: t('paymentRequest.applicant'), dataIndex: 'applicant', key: 'applicant', width: 80 }, + { title: t('paymentRequest.payee'), dataIndex: 'payee', key: 'payee', width: 150, ellipsis: true }, + { title: t('paymentRequest.amount'), dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => ( <>
{formatAmount(v, r.currency)}
{r.currency !== 'CNY' && r.amount_cny &&
≈ ¥{r.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
} ) }, - { title: '申请日期', dataIndex: 'application_date', key: 'application_date', width: 100, render: (v: string, r: any) => v || r.payment_date }, - { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (status: string) => getStatusTag(status) }, - { title: '编号', dataIndex: 'request_code', key: 'request_code', width: 120 }, + { title: t('paymentRequest.applicationDate'), dataIndex: 'application_date', key: 'application_date', width: 100, render: (v: string, r: any) => v || r.payment_date }, + { title: t('paymentRequest.status'), dataIndex: 'status', key: 'status', width: 100, render: (status: string) => getStatusTag(status) }, + { title: t('paymentRequest.code'), dataIndex: 'request_code', key: 'request_code', width: 120 }, { - title: '操作', key: 'action', width: 250, + title: t('paymentRequest.action'), key: 'action', width: 250, render: (_: any, record: any) => ( - + {record.status === 'pending' && ( <> - - + + )} {(record.status === 'rejected' || record.status === 'withdrawn') && ( - + )} - + ) } ]; - // 监听表单值变化 + // 监听表单值变化 - 使用 useState + onValuesChange 替代 Form.useWatch 以确保稳定触发 + const [watchedAmount, setWatchedAmount] = useState(null); + const [watchedCurrency, setWatchedCurrency] = useState('CNY'); const payeeType = Form.useWatch('payee_type', form); const payeeSelect = Form.useWatch('payee_select', form); const expenseType = Form.useWatch('expense_type', form); - const amount = Form.useWatch('amount', form); - const currency = Form.useWatch('currency', form); - + const amountCNY = React.useMemo(() => { - return amount && currency ? convertToCNY(amount, currency) : 0; - }, [amount, currency, exchangeRates]); + return watchedAmount && watchedCurrency ? convertToCNY(watchedAmount, watchedCurrency) : 0; + }, [watchedAmount, watchedCurrency, exchangeRates]); // 当选择收款单位时,自动填充收款信息 useEffect(() => { @@ -454,45 +484,47 @@ const PaymentRequestsPage: React.FC = () => { return (
-

付款申请

-

管理对外付款申请

+

{t('paymentRequest.title')}

+

{t('paymentRequest.description')}

- } onClick={handleCreate}>新建付款申请}> + } onClick={handleCreate}>{t('paymentRequest.newRequest')}}> - +
- +
- { + { if (form.isFieldsTouched()) { Modal.confirm({ - title: '确认关闭', - content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?', - okText: '关闭', - cancelText: '继续编辑', + title: t('common.closeConfirm'), + content: t('common.closeConfirmMsg'), + okText: t('common.close'), + cancelText: t('common.continueEdit'), onOk: () => { saveDraft(); + form.resetFields(); setModalVisible(false); }, }); } else { + form.resetFields(); setModalVisible(false); } - }} maskClosable={false} width={900}> + }} maskClosable={false} width={900} destroyOnClose>
- + {/* 第2项:支出类型和支出分类 */} - - {EXPENSE_TYPES.map(type => ( ))} @@ -501,8 +533,8 @@ const PaymentRequestsPage: React.FC = () => { {/* 项目支出 - 选择项目 */} {expenseType === 'project' && ( - - {projects.map(proj => ( ))} @@ -511,8 +543,8 @@ const PaymentRequestsPage: React.FC = () => { )} {/* 支出分类 */} - - {(expenseType === 'project' ? PROJECT_EXPENSE_CATEGORIES : COMPANY_EXPENSE_CATEGORIES).map(cat => ( ))} @@ -520,13 +552,13 @@ const PaymentRequestsPage: React.FC = () => { {/* 申请日期(原付款日期,不显示) */} - + {/* 收款单位 - 二级选择 */} - - {PAYEE_TYPES.map(type => ( ))} @@ -534,8 +566,8 @@ const PaymentRequestsPage: React.FC = () => { {payeeType === 'subcontractor' && ( - - {subcontractors.map(sub => ( ))} @@ -544,8 +576,8 @@ const PaymentRequestsPage: React.FC = () => { )} {payeeType === 'supplier' && ( - - {suppliers.map(sup => ( ))} @@ -554,8 +586,8 @@ const PaymentRequestsPage: React.FC = () => { )} {payeeType === 'customer' && ( - - {customers.map(cust => ( ))} @@ -564,102 +596,102 @@ const PaymentRequestsPage: React.FC = () => { )} {payeeType === 'other' && ( - - + + )} {/* 收款户名 - 新增字段 */} - - + + - - + + - - + + {/* 收款码 - 新增字段 */} - + - - setWatchedCurrency(value)}> + + + + {/* 金额 - 直接输入 */} - - - {amount && currency !== 'CNY' && amountCNY > 0 && ( -
- 等价人民币:¥ {amountCNY.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -
- )} + + setWatchedAmount(value)} /> + + {watchedAmount && watchedCurrency !== 'CNY' && amountCNY > 0 && ( +
+ {t('paymentRequest.equivalentCNY')}{amountCNY.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+ )} + + +