feat(tkpurchase): 업무현황 다건 입력 + 작업자 시간 추적 + 종합 페이지
- DB: 유니크 제약 제거, report_seq 컬럼, work_report_workers 테이블 - API: 트랜잭션 기반 다건 생성/수정, 작업자 CRUD, 요약/엑셀 엔드포인트 - 협력업체 포탈: 다건 보고 UI, 작업자+시간 입력(자동완성), 수정 기능 - 업무현황 페이지: 보고순번/작업자 상세 표시 - 종합 페이지(NEW): 업체별/프로젝트별 취합, 엑셀 추출 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,8 +72,8 @@ async function checkIn(req, res) {
|
||||
// 체크아웃
|
||||
async function checkOut(req, res) {
|
||||
try {
|
||||
const report = await workReportModel.findByCheckin(req.params.id);
|
||||
if (!report) {
|
||||
const reports = await workReportModel.findByCheckin(req.params.id);
|
||||
if (!reports || reports.length === 0) {
|
||||
return res.status(400).json({ success: false, error: '업무현황을 먼저 입력해주세요' });
|
||||
}
|
||||
const row = await checkinModel.checkOut(req.params.id);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const workReportModel = require('../models/workReportModel');
|
||||
const checkinModel = require('../models/checkinModel');
|
||||
const ExcelJS = require('exceljs');
|
||||
|
||||
// 작업보고 목록
|
||||
async function list(req, res) {
|
||||
@@ -58,7 +59,7 @@ async function myReports(req, res) {
|
||||
// 작업보고 등록
|
||||
async function create(req, res) {
|
||||
try {
|
||||
const { checkin_id, schedule_id, company_id, report_date } = req.body;
|
||||
const { checkin_id, schedule_id, company_id, report_date, workers } = req.body;
|
||||
|
||||
if (!report_date) {
|
||||
return res.status(400).json({ success: false, error: '보고일은 필수입니다' });
|
||||
@@ -85,7 +86,8 @@ async function create(req, res) {
|
||||
const data = {
|
||||
...req.body,
|
||||
company_id: resolvedCompanyId,
|
||||
reporter_id: req.user.user_id || req.user.id
|
||||
reporter_id: req.user.user_id || req.user.id,
|
||||
workers: workers || []
|
||||
};
|
||||
const row = await workReportModel.create(data);
|
||||
res.status(201).json({ success: true, data: row });
|
||||
@@ -98,8 +100,22 @@ async function create(req, res) {
|
||||
// 작업보고 수정
|
||||
async function update(req, res) {
|
||||
try {
|
||||
const existing = await workReportModel.findById(req.params.id);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
// 소유권 검증 (협력업체 포탈에서 호출 시)
|
||||
if (req.user.partner_company_id && existing.reporter_id !== (req.user.user_id || req.user.id)) {
|
||||
return res.status(403).json({ success: false, error: '본인이 작성한 보고만 수정할 수 있습니다' });
|
||||
}
|
||||
|
||||
// 확인 완료된 보고 수정 불가
|
||||
if (existing.confirmed_by) {
|
||||
return res.status(400).json({ success: false, error: '확인 완료된 보고는 수정할 수 없습니다' });
|
||||
}
|
||||
|
||||
const row = await workReportModel.update(req.params.id, req.body);
|
||||
if (!row) return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' });
|
||||
res.json({ success: true, data: row });
|
||||
} catch (err) {
|
||||
console.error('WorkReport update error:', err);
|
||||
@@ -120,4 +136,120 @@ async function confirm(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { list, getById, myReports, create, update, confirm };
|
||||
// 종합 요약
|
||||
async function summary(req, res) {
|
||||
try {
|
||||
const { company_id, schedule_id, date_from, date_to } = req.query;
|
||||
|
||||
if (!date_from || !date_to) {
|
||||
return res.status(400).json({ success: false, error: '기간(date_from, date_to)은 필수입니다' });
|
||||
}
|
||||
|
||||
// 최대 3개월 검증
|
||||
const from = new Date(date_from);
|
||||
const to = new Date(date_to);
|
||||
const diffMs = to - from;
|
||||
if (diffMs < 0) {
|
||||
return res.status(400).json({ success: false, error: '시작일이 종료일보다 늦을 수 없습니다' });
|
||||
}
|
||||
if (diffMs > 92 * 24 * 60 * 60 * 1000) {
|
||||
return res.status(400).json({ success: false, error: '조회 기간은 최대 3개월입니다' });
|
||||
}
|
||||
|
||||
const rows = await workReportModel.findAllAggregated({
|
||||
company_id: company_id ? parseInt(company_id) : undefined,
|
||||
schedule_id: schedule_id ? parseInt(schedule_id) : undefined,
|
||||
date_from,
|
||||
date_to
|
||||
});
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (err) {
|
||||
console.error('WorkReport summary error:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 엑셀 추출
|
||||
async function exportExcel(req, res) {
|
||||
try {
|
||||
const { company_id, schedule_id, date_from, date_to } = req.query;
|
||||
|
||||
if (!date_from || !date_to) {
|
||||
return res.status(400).json({ success: false, error: '기간(date_from, date_to)은 필수입니다' });
|
||||
}
|
||||
|
||||
const from = new Date(date_from);
|
||||
const to = new Date(date_to);
|
||||
const diffMs = to - from;
|
||||
if (diffMs < 0) {
|
||||
return res.status(400).json({ success: false, error: '시작일이 종료일보다 늦을 수 없습니다' });
|
||||
}
|
||||
if (diffMs > 92 * 24 * 60 * 60 * 1000) {
|
||||
return res.status(400).json({ success: false, error: '조회 기간은 최대 3개월입니다' });
|
||||
}
|
||||
|
||||
const rows = await workReportModel.exportData({
|
||||
company_id: company_id ? parseInt(company_id) : undefined,
|
||||
schedule_id: schedule_id ? parseInt(schedule_id) : undefined,
|
||||
date_from,
|
||||
date_to
|
||||
});
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const sheet = workbook.addWorksheet('업무현황');
|
||||
|
||||
sheet.columns = [
|
||||
{ header: '보고일', key: 'report_date', width: 12 },
|
||||
{ header: '순번', key: 'report_seq', width: 6 },
|
||||
{ header: '업체', key: 'company_name', width: 18 },
|
||||
{ header: '작업장', key: 'workplace_name', width: 15 },
|
||||
{ header: '작업내용', key: 'schedule_description', width: 20 },
|
||||
{ header: '보고내용', key: 'work_content', width: 30 },
|
||||
{ header: '진행률(%)', key: 'progress_rate', width: 10 },
|
||||
{ header: '작업자', key: 'worker_name', width: 12 },
|
||||
{ header: '투입시간', key: 'hours_worked', width: 10 },
|
||||
{ header: '이슈사항', key: 'issues', width: 25 },
|
||||
{ header: '향후계획', key: 'next_plan', width: 25 },
|
||||
{ header: '보고자', key: 'reporter_name', width: 10 },
|
||||
{ header: '확인상태', key: 'confirm_status', width: 8 },
|
||||
{ header: '확인자', key: 'confirmed_by_name', width: 10 },
|
||||
];
|
||||
|
||||
// 헤더 스타일
|
||||
sheet.getRow(1).eachCell(cell => {
|
||||
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF059669' } };
|
||||
cell.font = { color: { argb: 'FFFFFFFF' }, bold: true };
|
||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
});
|
||||
|
||||
rows.forEach(r => {
|
||||
sheet.addRow({
|
||||
report_date: r.report_date ? String(r.report_date).substring(0, 10) : '',
|
||||
report_seq: r.report_seq,
|
||||
company_name: r.company_name || '',
|
||||
workplace_name: r.workplace_name || '',
|
||||
schedule_description: r.schedule_description || '',
|
||||
work_content: r.work_content || '',
|
||||
progress_rate: r.progress_rate || 0,
|
||||
worker_name: r.worker_name || '',
|
||||
hours_worked: r.hours_worked != null ? Number(r.hours_worked) : '',
|
||||
issues: r.issues || '',
|
||||
next_plan: r.next_plan || '',
|
||||
reporter_name: r.reporter_name || '',
|
||||
confirm_status: r.confirm_status || '',
|
||||
confirmed_by_name: r.confirmed_by_name || '',
|
||||
});
|
||||
});
|
||||
|
||||
const filename = `업무현황_${date_from}_${date_to}.xlsx`;
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||||
await workbook.xlsx.write(res);
|
||||
res.end();
|
||||
} catch (err) {
|
||||
console.error('WorkReport exportExcel error:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { list, getById, myReports, create, update, confirm, summary, exportExcel };
|
||||
|
||||
@@ -27,10 +27,9 @@ async function findTodayByCompany(companyId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT pc.*, ps.work_description, ps.workplace_name,
|
||||
(dwr.id IS NOT NULL) AS has_work_report
|
||||
(SELECT COUNT(*) FROM partner_work_reports WHERE checkin_id = pc.id) AS work_report_count
|
||||
FROM partner_work_checkins pc
|
||||
LEFT JOIN partner_schedules ps ON pc.schedule_id = ps.id
|
||||
LEFT JOIN partner_work_reports dwr ON dwr.checkin_id = pc.id
|
||||
WHERE pc.company_id = ? AND DATE(pc.check_in_time) = CURDATE()
|
||||
ORDER BY pc.check_in_time DESC`, [companyId]);
|
||||
return rows;
|
||||
|
||||
@@ -17,7 +17,7 @@ async function findAll({ company_id, date_from, date_to, schedule_id, confirmed,
|
||||
if (schedule_id) { sql += ' AND wr.schedule_id = ?'; params.push(schedule_id); }
|
||||
if (confirmed === 'true' || confirmed === '1') { sql += ' AND wr.confirmed_by IS NOT NULL'; }
|
||||
if (confirmed === 'false' || confirmed === '0') { sql += ' AND wr.confirmed_by IS NULL'; }
|
||||
sql += ' ORDER BY wr.report_date DESC, wr.created_at DESC';
|
||||
sql += ' ORDER BY wr.report_date DESC, wr.report_seq ASC, wr.created_at DESC';
|
||||
const offset = (page - 1) * limit;
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
@@ -36,7 +36,17 @@ async function findById(id) {
|
||||
LEFT JOIN sso_users su_reporter ON wr.reporter_id = su_reporter.user_id
|
||||
LEFT JOIN sso_users su_confirmer ON wr.confirmed_by = su_confirmer.user_id
|
||||
WHERE wr.id = ?`, [id]);
|
||||
return rows[0] || null;
|
||||
const report = rows[0] || null;
|
||||
if (report) {
|
||||
const [workers] = await db.query(
|
||||
`SELECT wrw.*, pw.position
|
||||
FROM work_report_workers wrw
|
||||
LEFT JOIN partner_workers pw ON wrw.partner_worker_id = pw.id
|
||||
WHERE wrw.report_id = ?
|
||||
ORDER BY wrw.id`, [id]);
|
||||
report.workers = workers;
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
async function findByCheckin(checkinId) {
|
||||
@@ -45,35 +55,110 @@ async function findByCheckin(checkinId) {
|
||||
`SELECT wr.*, pc.company_name
|
||||
FROM partner_work_reports wr
|
||||
LEFT JOIN partner_companies pc ON wr.company_id = pc.id
|
||||
WHERE wr.checkin_id = ?`, [checkinId]);
|
||||
return rows[0] || null;
|
||||
WHERE wr.checkin_id = ?
|
||||
ORDER BY wr.report_seq ASC`, [checkinId]);
|
||||
// 각 보고에 workers 첨부
|
||||
for (const row of rows) {
|
||||
const [workers] = await db.query(
|
||||
`SELECT wrw.*, pw.position
|
||||
FROM work_report_workers wrw
|
||||
LEFT JOIN partner_workers pw ON wrw.partner_worker_id = pw.id
|
||||
WHERE wrw.report_id = ?
|
||||
ORDER BY wrw.id`, [row.id]);
|
||||
row.workers = workers;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function countByCheckin(checkinId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT COUNT(*) AS cnt FROM partner_work_reports WHERE checkin_id = ?', [checkinId]);
|
||||
return rows[0].cnt;
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO partner_work_reports (schedule_id, checkin_id, company_id, report_date, reporter_id, actual_workers, work_content, progress_rate, issues, next_plan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[data.schedule_id || null, data.checkin_id || null, data.company_id,
|
||||
data.report_date, data.reporter_id, data.actual_workers || null,
|
||||
data.work_content || null, data.progress_rate || null,
|
||||
data.issues || null, data.next_plan || null]);
|
||||
return findById(result.insertId);
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// report_seq: 같은 checkin_id 내 최대값 + 1
|
||||
const [seqRows] = await conn.query(
|
||||
'SELECT COALESCE(MAX(report_seq), 0) + 1 AS next_seq FROM partner_work_reports WHERE checkin_id = ?',
|
||||
[data.checkin_id]);
|
||||
const reportSeq = seqRows[0].next_seq;
|
||||
|
||||
const [result] = await conn.query(
|
||||
`INSERT INTO partner_work_reports (schedule_id, checkin_id, company_id, report_date, report_seq, reporter_id, actual_workers, work_content, progress_rate, issues, next_plan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[data.schedule_id || null, data.checkin_id || null, data.company_id,
|
||||
data.report_date, reportSeq, data.reporter_id, data.actual_workers || null,
|
||||
data.work_content || null, data.progress_rate || null,
|
||||
data.issues || null, data.next_plan || null]);
|
||||
|
||||
const reportId = result.insertId;
|
||||
|
||||
// workers 삽입
|
||||
if (data.workers && data.workers.length > 0) {
|
||||
for (const w of data.workers) {
|
||||
await conn.query(
|
||||
`INSERT INTO work_report_workers (report_id, partner_worker_id, worker_name, hours_worked)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[reportId, w.partner_worker_id || null, w.worker_name, w.hours_worked ?? 8.0]);
|
||||
}
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
return findById(reportId);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
if (data.actual_workers !== undefined) { fields.push('actual_workers = ?'); values.push(data.actual_workers || null); }
|
||||
if (data.work_content !== undefined) { fields.push('work_content = ?'); values.push(data.work_content || null); }
|
||||
if (data.progress_rate !== undefined) { fields.push('progress_rate = ?'); values.push(data.progress_rate || null); }
|
||||
if (data.issues !== undefined) { fields.push('issues = ?'); values.push(data.issues || null); }
|
||||
if (data.next_plan !== undefined) { fields.push('next_plan = ?'); values.push(data.next_plan || null); }
|
||||
if (fields.length === 0) return findById(id);
|
||||
values.push(id);
|
||||
await db.query(`UPDATE partner_work_reports SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
return findById(id);
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
if (data.actual_workers !== undefined) { fields.push('actual_workers = ?'); values.push(data.actual_workers || null); }
|
||||
if (data.work_content !== undefined) { fields.push('work_content = ?'); values.push(data.work_content || null); }
|
||||
if (data.progress_rate !== undefined) { fields.push('progress_rate = ?'); values.push(data.progress_rate || null); }
|
||||
if (data.issues !== undefined) { fields.push('issues = ?'); values.push(data.issues || null); }
|
||||
if (data.next_plan !== undefined) { fields.push('next_plan = ?'); values.push(data.next_plan || null); }
|
||||
|
||||
if (fields.length > 0) {
|
||||
values.push(id);
|
||||
await conn.query(`UPDATE partner_work_reports SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
}
|
||||
|
||||
// workers 교체 (있으면)
|
||||
if (data.workers !== undefined) {
|
||||
await conn.query('DELETE FROM work_report_workers WHERE report_id = ?', [id]);
|
||||
if (data.workers && data.workers.length > 0) {
|
||||
for (const w of data.workers) {
|
||||
await conn.query(
|
||||
`INSERT INTO work_report_workers (report_id, partner_worker_id, worker_name, hours_worked)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[id, w.partner_worker_id || null, w.worker_name, w.hours_worked ?? 8.0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
return findById(id);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm(id, confirmedBy) {
|
||||
@@ -84,4 +169,69 @@ async function confirm(id, confirmedBy) {
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
module.exports = { findAll, findById, findByCheckin, create, update, confirm };
|
||||
async function findAllAggregated({ company_id, schedule_id, date_from, date_to } = {}) {
|
||||
const db = getPool();
|
||||
let sql = `SELECT
|
||||
wr.report_date,
|
||||
wr.company_id,
|
||||
pc.company_name,
|
||||
ps.id AS schedule_id,
|
||||
ps.work_description AS schedule_description,
|
||||
ps.workplace_name,
|
||||
COUNT(wr.id) AS report_count,
|
||||
(SELECT COUNT(DISTINCT wrw.worker_name) FROM work_report_workers wrw WHERE wrw.report_id IN (
|
||||
SELECT wr2.id FROM partner_work_reports wr2
|
||||
WHERE wr2.report_date = wr.report_date AND wr2.company_id = wr.company_id AND wr2.schedule_id = wr.schedule_id
|
||||
)) AS total_workers,
|
||||
(SELECT COALESCE(SUM(wrw.hours_worked), 0) FROM work_report_workers wrw WHERE wrw.report_id IN (
|
||||
SELECT wr2.id FROM partner_work_reports wr2
|
||||
WHERE wr2.report_date = wr.report_date AND wr2.company_id = wr.company_id AND wr2.schedule_id = wr.schedule_id
|
||||
)) AS total_hours,
|
||||
ROUND(AVG(wr.progress_rate), 1) AS avg_progress,
|
||||
SUM(CASE WHEN wr.confirmed_by IS NOT NULL THEN 1 ELSE 0 END) AS confirmed_count
|
||||
FROM partner_work_reports wr
|
||||
LEFT JOIN partner_companies pc ON wr.company_id = pc.id
|
||||
LEFT JOIN partner_schedules ps ON wr.schedule_id = ps.id
|
||||
WHERE 1=1`;
|
||||
const params = [];
|
||||
if (company_id) { sql += ' AND wr.company_id = ?'; params.push(company_id); }
|
||||
if (schedule_id) { sql += ' AND wr.schedule_id = ?'; params.push(schedule_id); }
|
||||
if (date_from) { sql += ' AND wr.report_date >= ?'; params.push(date_from); }
|
||||
if (date_to) { sql += ' AND wr.report_date <= ?'; params.push(date_to); }
|
||||
sql += ' GROUP BY wr.report_date, wr.company_id, wr.schedule_id ORDER BY wr.report_date DESC, pc.company_name';
|
||||
const [rows] = await db.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function exportData({ company_id, schedule_id, date_from, date_to } = {}) {
|
||||
const db = getPool();
|
||||
let sql = `SELECT
|
||||
wr.report_date, wr.report_seq,
|
||||
pc.company_name,
|
||||
ps.work_description AS schedule_description,
|
||||
ps.workplace_name,
|
||||
wr.work_content, wr.progress_rate, wr.issues, wr.next_plan,
|
||||
wr.actual_workers,
|
||||
su_reporter.name AS reporter_name,
|
||||
wrw.worker_name, wrw.hours_worked,
|
||||
CASE WHEN wr.confirmed_by IS NOT NULL THEN '확인' ELSE '미확인' END AS confirm_status,
|
||||
su_confirmer.name AS confirmed_by_name,
|
||||
wr.confirmed_at
|
||||
FROM partner_work_reports wr
|
||||
LEFT JOIN partner_companies pc ON wr.company_id = pc.id
|
||||
LEFT JOIN partner_schedules ps ON wr.schedule_id = ps.id
|
||||
LEFT JOIN sso_users su_reporter ON wr.reporter_id = su_reporter.user_id
|
||||
LEFT JOIN sso_users su_confirmer ON wr.confirmed_by = su_confirmer.user_id
|
||||
LEFT JOIN work_report_workers wrw ON wrw.report_id = wr.id
|
||||
WHERE 1=1`;
|
||||
const params = [];
|
||||
if (company_id) { sql += ' AND wr.company_id = ?'; params.push(company_id); }
|
||||
if (schedule_id) { sql += ' AND wr.schedule_id = ?'; params.push(schedule_id); }
|
||||
if (date_from) { sql += ' AND wr.report_date >= ?'; params.push(date_from); }
|
||||
if (date_to) { sql += ' AND wr.report_date <= ?'; params.push(date_to); }
|
||||
sql += ' ORDER BY wr.report_date DESC, pc.company_name, wr.report_seq, wrw.id';
|
||||
const [rows] = await db.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
module.exports = { findAll, findById, findByCheckin, countByCheckin, create, update, confirm, findAllAggregated, exportData };
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"mysql2": "^3.14.1",
|
||||
"node-cron": "^3.0.3"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ router.use(requireAuth);
|
||||
|
||||
router.get('/', ctrl.list);
|
||||
router.get('/my', ctrl.myReports); // partner portal
|
||||
router.get('/summary', requirePage('purchasing_workreport'), ctrl.summary);
|
||||
router.get('/export', requirePage('purchasing_workreport'), ctrl.exportExcel);
|
||||
router.get('/:id', ctrl.getById);
|
||||
router.post('/', ctrl.create); // partner can create
|
||||
router.put('/:id', ctrl.update);
|
||||
|
||||
Reference in New Issue
Block a user