근태(Holyday) API 추가 및 일별/업무형태별 집계 다이얼로그 구현, OT 시작/종료시간 필드 추가
This commit is contained in:
@@ -361,6 +361,7 @@
|
|||||||
<Compile Include="Web\MachineBridge\MachineBridge.User.cs" />
|
<Compile Include="Web\MachineBridge\MachineBridge.User.cs" />
|
||||||
<Compile Include="Web\MachineBridge\MachineBridge.UserList.cs" />
|
<Compile Include="Web\MachineBridge\MachineBridge.UserList.cs" />
|
||||||
<Compile Include="Web\MachineBridge\MachineBridge.Holiday.cs" />
|
<Compile Include="Web\MachineBridge\MachineBridge.Holiday.cs" />
|
||||||
|
<Compile Include="Web\MachineBridge\MachineBridge.Holyday.cs" />
|
||||||
<Compile Include="Web\MachineBridge\MachineBridge.MailForm.cs" />
|
<Compile Include="Web\MachineBridge\MachineBridge.MailForm.cs" />
|
||||||
<Compile Include="Web\MachineBridge\MachineBridge.UserGroup.cs" />
|
<Compile Include="Web\MachineBridge\MachineBridge.UserGroup.cs" />
|
||||||
<Compile Include="Web\MachineBridge\MachineBridge.UserAuth.cs" />
|
<Compile Include="Web\MachineBridge\MachineBridge.UserAuth.cs" />
|
||||||
|
|||||||
447
Project/Web/MachineBridge/MachineBridge.Holyday.cs
Normal file
447
Project/Web/MachineBridge/MachineBridge.Holyday.cs
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using FCOMMON;
|
||||||
|
|
||||||
|
namespace Project.Web
|
||||||
|
{
|
||||||
|
public partial class MachineBridge
|
||||||
|
{
|
||||||
|
#region Holyday (근태) API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 목록 조회 (사용자 선택 가능)
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_GetList(string sd, string ed, string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 권한 확인
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
|
||||||
|
var sql = @"SELECT h.*, u.name as UserName
|
||||||
|
FROM Holyday h WITH (nolock)
|
||||||
|
LEFT JOIN Users u ON h.uid = u.id
|
||||||
|
WHERE h.gcode = @gcode";
|
||||||
|
|
||||||
|
var parameters = new List<SqlParameter>();
|
||||||
|
parameters.Add(new SqlParameter("@gcode", info.Login.gcode));
|
||||||
|
|
||||||
|
// 권한에 따라 사용자 필터링
|
||||||
|
if (curLevel < 5)
|
||||||
|
{
|
||||||
|
// 일반 사용자는 본인 것만
|
||||||
|
sql += " AND h.uid = @uid";
|
||||||
|
parameters.Add(new SqlParameter("@uid", info.Login.no));
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(uid) && uid != "%")
|
||||||
|
{
|
||||||
|
// 관리자가 특정 사용자 선택
|
||||||
|
sql += " AND h.uid = @uid";
|
||||||
|
parameters.Add(new SqlParameter("@uid", uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sd))
|
||||||
|
{
|
||||||
|
sql += " AND h.sdate >= @sd";
|
||||||
|
parameters.Add(new SqlParameter("@sd", sd));
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(ed))
|
||||||
|
{
|
||||||
|
sql += " AND h.sdate <= @ed";
|
||||||
|
parameters.Add(new SqlParameter("@ed", ed));
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += " ORDER BY h.sdate DESC, h.idx DESC";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddRange(parameters.ToArray());
|
||||||
|
using (var da = new SqlDataAdapter(cmd))
|
||||||
|
{
|
||||||
|
var dt = new DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
return JsonConvert.SerializeObject(new { Success = true, Data = dt });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 상세 조회
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_GetDetail(int idx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = @"SELECT h.*, u.name as UserName
|
||||||
|
FROM Holyday h WITH (nolock)
|
||||||
|
LEFT JOIN Users u ON h.uid = u.id
|
||||||
|
WHERE h.idx = @idx AND h.gcode = @gcode";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@idx", idx);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
using (var da = new SqlDataAdapter(cmd))
|
||||||
|
{
|
||||||
|
var dt = new DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
if (dt.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
var row = dt.Rows[0];
|
||||||
|
var data = new Dictionary<string, object>();
|
||||||
|
foreach (DataColumn col in dt.Columns)
|
||||||
|
{
|
||||||
|
data[col.ColumnName] = row[col] == DBNull.Value ? null : row[col];
|
||||||
|
}
|
||||||
|
return JsonConvert.SerializeObject(new { Success = true, Data = data });
|
||||||
|
}
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 추가
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_Add(string cate, string sdate, string edate, double term, double crtime,
|
||||||
|
double termDr, double drTime, string contents, string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 권한 확인
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
if (curLevel < 5)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마감 체크
|
||||||
|
var smon = sdate.Substring(0, 7);
|
||||||
|
if (DBM.GetMagamStatus(smon))
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"INSERT INTO Holyday (gcode, cate, sdate, edate, term, crtime, termDr, DrTime, contents, uid, wdate, wuid)
|
||||||
|
VALUES (@gcode, @cate, @sdate, @edate, @term, @crtime, @termDr, @drTime, @contents, @uid, GETDATE(), @wuid);
|
||||||
|
SELECT SCOPE_IDENTITY();";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("@cate", cate ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@sdate", sdate);
|
||||||
|
cmd.Parameters.AddWithValue("@edate", edate);
|
||||||
|
cmd.Parameters.AddWithValue("@term", term);
|
||||||
|
cmd.Parameters.AddWithValue("@crtime", crtime);
|
||||||
|
cmd.Parameters.AddWithValue("@termDr", termDr);
|
||||||
|
cmd.Parameters.AddWithValue("@drTime", drTime);
|
||||||
|
cmd.Parameters.AddWithValue("@contents", contents ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@uid", uid);
|
||||||
|
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
var newId = Convert.ToInt32(cmd.ExecuteScalar());
|
||||||
|
return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 수정
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_Edit(int idx, string cate, string sdate, string edate, double term, double crtime,
|
||||||
|
double termDr, double drTime, string contents)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 권한 확인
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
if (curLevel < 5)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마감 체크
|
||||||
|
var smon = sdate.Substring(0, 7);
|
||||||
|
if (DBM.GetMagamStatus(smon))
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 외부 연동 데이터 체크
|
||||||
|
var checkSql = "SELECT extcate, extidx FROM Holyday WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
{
|
||||||
|
using (var cmd = new SqlCommand(checkSql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@idx", idx);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
cn.Open();
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
var extcate = reader["extcate"] != DBNull.Value ? reader["extcate"].ToString() : "";
|
||||||
|
var extidx = reader["extidx"] != DBNull.Value ? Convert.ToInt32(reader["extidx"]) : -1;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(extcate) && extidx > 0)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new {
|
||||||
|
Success = false,
|
||||||
|
Message = $"이 자료는 외부에서 자동생성된 자료입니다. (소스: {extcate}:{extidx})"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"UPDATE Holyday SET
|
||||||
|
cate = @cate, sdate = @sdate, edate = @edate, term = @term, crtime = @crtime,
|
||||||
|
termDr = @termDr, DrTime = @drTime, contents = @contents, wuid = @wuid, wdate = GETDATE()
|
||||||
|
WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@idx", idx);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("@cate", cate ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@sdate", sdate);
|
||||||
|
cmd.Parameters.AddWithValue("@edate", edate);
|
||||||
|
cmd.Parameters.AddWithValue("@term", term);
|
||||||
|
cmd.Parameters.AddWithValue("@crtime", crtime);
|
||||||
|
cmd.Parameters.AddWithValue("@termDr", termDr);
|
||||||
|
cmd.Parameters.AddWithValue("@drTime", drTime);
|
||||||
|
cmd.Parameters.AddWithValue("@contents", contents ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
||||||
|
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "수정되었습니다." : "수정에 실패했습니다." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 삭제
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_Delete(int idx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 권한 확인
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
if (curLevel < 5)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 외부 연동 데이터 및 마감 체크
|
||||||
|
var checkSql = "SELECT extcate, extidx, sdate FROM Holyday WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
{
|
||||||
|
using (var cmd = new SqlCommand(checkSql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@idx", idx);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
cn.Open();
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
var extcate = reader["extcate"] != DBNull.Value ? reader["extcate"].ToString() : "";
|
||||||
|
var extidx = reader["extidx"] != DBNull.Value ? Convert.ToInt32(reader["extidx"]) : -1;
|
||||||
|
var sdate = reader["sdate"] != DBNull.Value ? reader["sdate"].ToString() : "";
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(extcate) && extidx > 0)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new {
|
||||||
|
Success = false,
|
||||||
|
Message = $"이 자료는 외부에서 자동생성된 자료입니다. (소스: {extcate}:{extidx})"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sdate) && sdate.Length >= 7)
|
||||||
|
{
|
||||||
|
var smon = sdate.Substring(0, 7);
|
||||||
|
if (DBM.GetMagamStatus(smon))
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = "DELETE FROM Holyday WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@idx", idx);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 사용자 목록 조회 (기간 내 데이터가 있는 사용자)
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_GetUserList(string sd, string ed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 권한 확인
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
|
||||||
|
var sql = @"SELECT DISTINCT h.uid, u.name as UserName
|
||||||
|
FROM Holyday h WITH (nolock)
|
||||||
|
LEFT JOIN Users u ON h.uid = u.id
|
||||||
|
WHERE h.gcode = @gcode";
|
||||||
|
|
||||||
|
var parameters = new List<SqlParameter>();
|
||||||
|
parameters.Add(new SqlParameter("@gcode", info.Login.gcode));
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(sd))
|
||||||
|
{
|
||||||
|
sql += " AND h.sdate >= @sd";
|
||||||
|
parameters.Add(new SqlParameter("@sd", sd));
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(ed))
|
||||||
|
{
|
||||||
|
sql += " AND h.sdate <= @ed";
|
||||||
|
parameters.Add(new SqlParameter("@ed", ed));
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += " ORDER BY u.name";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddRange(parameters.ToArray());
|
||||||
|
using (var da = new SqlDataAdapter(cmd))
|
||||||
|
{
|
||||||
|
var dt = new DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
return JsonConvert.SerializeObject(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Holyday_GetUserList 오류: {ex.Message}");
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 권한 정보 조회
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_GetPermission()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
bool canManage = curLevel >= 5;
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
CurrentUserId = info.Login.no,
|
||||||
|
Level = curLevel,
|
||||||
|
CanManage = canManage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 근태 잔량 조회 (연도별)
|
||||||
|
/// </summary>
|
||||||
|
public string Holyday_GetBalance(string year, string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 권한 확인
|
||||||
|
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday));
|
||||||
|
|
||||||
|
// 본인 확인 또는 관리자 권한 확인
|
||||||
|
if (uid != info.Login.no && curLevel < 5)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"SELECT
|
||||||
|
cate,
|
||||||
|
ISNULL(SUM(termDr), 0) as TotalGenDays,
|
||||||
|
ISNULL(SUM(DrTime), 0) as TotalGenHours,
|
||||||
|
ISNULL(SUM(term), 0) as TotalUseDays,
|
||||||
|
ISNULL(SUM(crtime), 0) as TotalUseHours
|
||||||
|
FROM Holyday WITH (nolock)
|
||||||
|
WHERE gcode = @gcode AND uid = @uid AND sdate LIKE @year + '%'
|
||||||
|
GROUP BY cate";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
using (var cn = new SqlConnection(cs))
|
||||||
|
using (var cmd = new SqlCommand(sql, cn))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("@uid", uid);
|
||||||
|
cmd.Parameters.AddWithValue("@year", year);
|
||||||
|
|
||||||
|
using (var da = new SqlDataAdapter(cmd))
|
||||||
|
{
|
||||||
|
var dt = new DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
return JsonConvert.SerializeObject(new { Success = true, Data = dt });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,10 +99,10 @@ namespace Project.Web
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 뷰에서 기본 정보 조회, 원본 테이블에서 jobgrp, tag 추가 조회
|
// 뷰에서 기본 정보 조회, 원본 테이블에서 jobgrp, tag, otStart, otEnd 추가 조회
|
||||||
var sql = @"SELECT v.idx, v.pidx, v.pdate, v.id, v.name, v.type, v.svalue, v.hrs, v.ot,
|
var sql = @"SELECT v.idx, v.pidx, v.pdate, v.id, v.name, v.type, v.svalue, v.hrs, v.ot,
|
||||||
v.requestpart, v.package, v.userprocess, v.status, v.projectName, v.description,
|
v.requestpart, v.package, v.userprocess, v.status, v.projectName, v.description,
|
||||||
v.ww, v.otpms, v.process, j.jobgrp, j.tag
|
v.ww, v.otpms, v.process, j.jobgrp, j.tag, j.otStart, j.otEnd
|
||||||
FROM vJobReportForUser v WITH (nolock)
|
FROM vJobReportForUser v WITH (nolock)
|
||||||
INNER JOIN JobReport j WITH (nolock) ON v.idx = j.idx
|
INNER JOIN JobReport j WITH (nolock) ON v.idx = j.idx
|
||||||
WHERE v.idx = @idx AND v.gcode = @gcode";
|
WHERE v.idx = @idx AND v.gcode = @gcode";
|
||||||
@@ -141,7 +141,8 @@ namespace Project.Web
|
|||||||
/// 업무일지 추가 (JobReport 테이블)
|
/// 업무일지 추가 (JobReport 테이블)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Jobreport_Add(string pdate, string projectName, int pidx, string requestpart, string package,
|
public string Jobreport_Add(string pdate, string projectName, int pidx, string requestpart, string package,
|
||||||
string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag)
|
string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag,
|
||||||
|
string otStart, string otEnd)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -153,9 +154,9 @@ namespace Project.Web
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sql = @"INSERT INTO JobReport (gcode, uid, pdate, projectName, requestpart, package,
|
var sql = @"INSERT INTO JobReport (gcode, uid, pdate, projectName, requestpart, package,
|
||||||
type, process, status, description, hrs, ot, jobgrp, tag, wuid, wdate, pidx)
|
type, process, status, description, hrs, ot, jobgrp, tag, wuid, wdate, pidx, otStart, otEnd)
|
||||||
VALUES (@gcode, @uid, @pdate, @projectName, @requestpart, @package,
|
VALUES (@gcode, @uid, @pdate, @projectName, @requestpart, @package,
|
||||||
@type, @process, @status, @description, @hrs, @ot, @jobgrp, @tag, @wuid, GETDATE(), @pidx);
|
@type, @process, @status, @description, @hrs, @ot, @jobgrp, @tag, @wuid, GETDATE(), @pidx, @otStart, @otEnd);
|
||||||
SELECT SCOPE_IDENTITY();";
|
SELECT SCOPE_IDENTITY();";
|
||||||
|
|
||||||
var cs = Properties.Settings.Default.gwcs;
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
@@ -179,6 +180,20 @@ namespace Project.Web
|
|||||||
cmd.Parameters.AddWithValue("@tag", tag ?? "");
|
cmd.Parameters.AddWithValue("@tag", tag ?? "");
|
||||||
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
||||||
|
|
||||||
|
// otStart, otEnd 처리 (HH:mm 형식을 datetime으로 변환)
|
||||||
|
if (!string.IsNullOrEmpty(otStart) && !string.IsNullOrEmpty(otEnd))
|
||||||
|
{
|
||||||
|
var otStartDateTime = DateTime.Parse($"{pdate} {otStart}:00");
|
||||||
|
var otEndDateTime = DateTime.Parse($"{pdate} {otEnd}:00");
|
||||||
|
cmd.Parameters.AddWithValue("@otStart", otStartDateTime);
|
||||||
|
cmd.Parameters.AddWithValue("@otEnd", otEndDateTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@otStart", DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@otEnd", DBNull.Value);
|
||||||
|
}
|
||||||
|
|
||||||
cn.Open();
|
cn.Open();
|
||||||
var newId = Convert.ToInt32(cmd.ExecuteScalar());
|
var newId = Convert.ToInt32(cmd.ExecuteScalar());
|
||||||
return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } });
|
return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } });
|
||||||
@@ -194,7 +209,8 @@ namespace Project.Web
|
|||||||
/// 업무일지 수정 (JobReport 테이블)
|
/// 업무일지 수정 (JobReport 테이블)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Jobreport_Edit(int idx, string pdate, string projectName, int pidx, string requestpart, string package,
|
public string Jobreport_Edit(int idx, string pdate, string projectName, int pidx, string requestpart, string package,
|
||||||
string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag)
|
string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag,
|
||||||
|
string otStart, string otEnd)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -231,7 +247,7 @@ namespace Project.Web
|
|||||||
pdate = @pdate, projectName = @projectName, pidx = @pidx, requestpart = @requestpart,
|
pdate = @pdate, projectName = @projectName, pidx = @pidx, requestpart = @requestpart,
|
||||||
package = @package, type = @type, process = @process, status = @status,
|
package = @package, type = @type, process = @process, status = @status,
|
||||||
description = @description, hrs = @hrs, ot = @ot, jobgrp = @jobgrp, tag = @tag,
|
description = @description, hrs = @hrs, ot = @ot, jobgrp = @jobgrp, tag = @tag,
|
||||||
wuid = @wuid, wdate = GETDATE()
|
otStart = @otStart, otEnd = @otEnd, wuid = @wuid, wdate = GETDATE()
|
||||||
WHERE idx = @idx AND gcode = @gcode";
|
WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
|
||||||
var cs = Properties.Settings.Default.gwcs;
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
@@ -255,6 +271,20 @@ namespace Project.Web
|
|||||||
cmd.Parameters.AddWithValue("@tag", tag ?? "");
|
cmd.Parameters.AddWithValue("@tag", tag ?? "");
|
||||||
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
||||||
|
|
||||||
|
// otStart, otEnd 처리 (HH:mm 형식을 datetime으로 변환)
|
||||||
|
if (!string.IsNullOrEmpty(otStart) && !string.IsNullOrEmpty(otEnd))
|
||||||
|
{
|
||||||
|
var otStartDateTime = DateTime.Parse($"{pdate} {otStart}:00");
|
||||||
|
var otEndDateTime = DateTime.Parse($"{pdate} {otEnd}:00");
|
||||||
|
cmd.Parameters.AddWithValue("@otStart", otStartDateTime);
|
||||||
|
cmd.Parameters.AddWithValue("@otEnd", otEndDateTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@otStart", DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@otEnd", DBNull.Value);
|
||||||
|
}
|
||||||
|
|
||||||
cn.Open();
|
cn.Open();
|
||||||
var result = cmd.ExecuteNonQuery();
|
var result = cmd.ExecuteNonQuery();
|
||||||
return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "수정되었습니다." : "수정에 실패했습니다." });
|
return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "수정되었습니다." : "수정에 실패했습니다." });
|
||||||
|
|||||||
@@ -646,7 +646,9 @@ namespace Project.Web
|
|||||||
double ot = json.ot ?? 0.0;
|
double ot = json.ot ?? 0.0;
|
||||||
string jobgrp = json.jobgrp ?? "";
|
string jobgrp = json.jobgrp ?? "";
|
||||||
string tag = json.tag ?? "";
|
string tag = json.tag ?? "";
|
||||||
string result = _bridge.Jobreport_Add(pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag);
|
string otStart = json.otStart ?? "00:00";
|
||||||
|
string otEnd = json.otEnd ?? "00:00";
|
||||||
|
string result = _bridge.Jobreport_Add(pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd);
|
||||||
var response = new { type = "JOBREPORT_ADDED", data = JsonConvert.DeserializeObject(result) };
|
var response = new { type = "JOBREPORT_ADDED", data = JsonConvert.DeserializeObject(result) };
|
||||||
await Send(socket, JsonConvert.SerializeObject(response));
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
}
|
}
|
||||||
@@ -668,7 +670,9 @@ namespace Project.Web
|
|||||||
double ot = json.ot ?? 0.0;
|
double ot = json.ot ?? 0.0;
|
||||||
string jobgrp = json.jobgrp ?? "";
|
string jobgrp = json.jobgrp ?? "";
|
||||||
string tag = json.tag ?? "";
|
string tag = json.tag ?? "";
|
||||||
string result = _bridge.Jobreport_Edit(idx, pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag);
|
string otStart = json.otStart ?? "00:00";
|
||||||
|
string otEnd = json.otEnd ?? "00:00";
|
||||||
|
string result = _bridge.Jobreport_Edit(idx, pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd);
|
||||||
var response = new { type = "JOBREPORT_EDITED", data = JsonConvert.DeserializeObject(result) };
|
var response = new { type = "JOBREPORT_EDITED", data = JsonConvert.DeserializeObject(result) };
|
||||||
await Send(socket, JsonConvert.SerializeObject(response));
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
}
|
}
|
||||||
@@ -729,6 +733,99 @@ namespace Project.Web
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ===== Holyday (근태) API =====
|
||||||
|
case "HOLYDAY_GET_LIST":
|
||||||
|
{
|
||||||
|
string sd = json.sd ?? "";
|
||||||
|
string ed = json.ed ?? "";
|
||||||
|
string uid = json.uid ?? "%";
|
||||||
|
string result = _bridge.Holyday_GetList(sd, ed, uid);
|
||||||
|
var response = new { type = "HOLYDAY_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_GET_DETAIL":
|
||||||
|
{
|
||||||
|
int idx = json.idx ?? 0;
|
||||||
|
string result = _bridge.Holyday_GetDetail(idx);
|
||||||
|
var response = new { type = "HOLYDAY_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_ADD":
|
||||||
|
{
|
||||||
|
string cate = json.cate ?? "";
|
||||||
|
string sdate = json.sdate ?? "";
|
||||||
|
string edate = json.edate ?? "";
|
||||||
|
double term = json.term ?? 0.0;
|
||||||
|
double crtime = json.crtime ?? 0.0;
|
||||||
|
double termDr = json.termDr ?? 0.0;
|
||||||
|
double drTime = json.drTime ?? 0.0;
|
||||||
|
string contents = json.contents ?? "";
|
||||||
|
string uid = json.uid ?? "";
|
||||||
|
string result = _bridge.Holyday_Add(cate, sdate, edate, term, crtime, termDr, drTime, contents, uid);
|
||||||
|
var response = new { type = "HOLYDAY_ADDED", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_EDIT":
|
||||||
|
{
|
||||||
|
int idx = json.idx ?? 0;
|
||||||
|
string cate = json.cate ?? "";
|
||||||
|
string sdate = json.sdate ?? "";
|
||||||
|
string edate = json.edate ?? "";
|
||||||
|
double term = json.term ?? 0.0;
|
||||||
|
double crtime = json.crtime ?? 0.0;
|
||||||
|
double termDr = json.termDr ?? 0.0;
|
||||||
|
double drTime = json.drTime ?? 0.0;
|
||||||
|
string contents = json.contents ?? "";
|
||||||
|
string result = _bridge.Holyday_Edit(idx, cate, sdate, edate, term, crtime, termDr, drTime, contents);
|
||||||
|
var response = new { type = "HOLYDAY_EDITED", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_DELETE":
|
||||||
|
{
|
||||||
|
int idx = json.idx ?? 0;
|
||||||
|
string result = _bridge.Holyday_Delete(idx);
|
||||||
|
var response = new { type = "HOLYDAY_DELETED", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_GET_USERLIST":
|
||||||
|
{
|
||||||
|
string sd = json.sd ?? "";
|
||||||
|
string ed = json.ed ?? "";
|
||||||
|
string result = _bridge.Holyday_GetUserList(sd, ed);
|
||||||
|
var response = new { type = "HOLYDAY_USERLIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_GET_PERMISSION":
|
||||||
|
{
|
||||||
|
string result = _bridge.Holyday_GetPermission();
|
||||||
|
var response = new { type = "HOLYDAY_PERMISSION_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "HOLYDAY_GET_BALANCE":
|
||||||
|
{
|
||||||
|
string year = json.year ?? "";
|
||||||
|
string uid = json.uid ?? "";
|
||||||
|
string result = _bridge.Holyday_GetBalance(year, uid);
|
||||||
|
var response = new { type = "HOLYDAY_BALANCE_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||||
|
await Send(socket, JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
// ===== Holiday API (월별근무표) =====
|
// ===== Holiday API (월별근무표) =====
|
||||||
case "HOLIDAY_GET_LIST":
|
case "HOLIDAY_GET_LIST":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { MachineBridgeInterface, ApiResponse, TodoModel, PurchaseCount, HolyUser, HolyRequestUser, PurchaseItem, KuntaeModel, LoginStatusResponse, LoginResult, UserGroup, PreviousLoginInfo, UserInfoDetail, GroupUser, UserLevelInfo, UserFullData, JobReportItem, JobReportUser, CommonCodeGroup, CommonCode, ItemInfo, ItemDetail, SupplierStaff, PurchaseHistoryItem, JobReportPermission, AppVersionInfo, JobTypeItem, HolidayItem, MailFormItem, UserGroupItem, PermissionInfo, AuthItem, AuthFieldInfo, CheckAuthResponse, MyAuthInfo, AuthType, ProjectSearchItem } from './types';
|
// WebView2 환경 감지
|
||||||
|
const isWebView = typeof window !== 'undefined' &&
|
||||||
|
window.chrome?.webview?.hostObjects !== undefined;
|
||||||
|
|
||||||
// WebView2 환경인지 체크
|
|
||||||
const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview;
|
|
||||||
|
|
||||||
// 비동기 프록시 캐싱 (한 번만 초기화)
|
|
||||||
const machine: MachineBridgeInterface | null = isWebView
|
const machine: MachineBridgeInterface | null = isWebView
|
||||||
? window.chrome!.webview!.hostObjects.machine
|
? window.chrome!.webview!.hostObjects.machine
|
||||||
: null;
|
: null;
|
||||||
@@ -308,6 +306,50 @@ class CommunicationLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Holyday (근태) API =====
|
||||||
|
|
||||||
|
public async getHolydayList(sd: string, ed: string, uid: string = '%'): Promise<ApiResponse<KuntaeModel[]>> {
|
||||||
|
return this.wsRequest<ApiResponse<KuntaeModel[]>>('HOLYDAY_GET_LIST', 'HOLYDAY_LIST_DATA', { sd, ed, uid });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHolydayDetail(idx: number): Promise<ApiResponse<KuntaeModel>> {
|
||||||
|
return this.wsRequest<ApiResponse<KuntaeModel>>('HOLYDAY_GET_DETAIL', 'HOLYDAY_DETAIL_DATA', { idx });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addHolyday(
|
||||||
|
cate: string, sdate: string, edate: string, term: number, crtime: number,
|
||||||
|
termDr: number, drTime: number, contents: string, uid: string
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
return this.wsRequest<ApiResponse>('HOLYDAY_ADD', 'HOLYDAY_ADDED', {
|
||||||
|
cate, sdate, edate, term, crtime, termDr, drTime, contents, uid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async editHolyday(
|
||||||
|
idx: number, cate: string, sdate: string, edate: string, term: number, crtime: number,
|
||||||
|
termDr: number, drTime: number, contents: string
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
return this.wsRequest<ApiResponse>('HOLYDAY_EDIT', 'HOLYDAY_EDITED', {
|
||||||
|
idx, cate, sdate, edate, term, crtime, termDr, drTime, contents
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteHolyday(idx: number): Promise<ApiResponse> {
|
||||||
|
return this.wsRequest<ApiResponse>('HOLYDAY_DELETE', 'HOLYDAY_DELETED', { idx });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHolydayUserList(sd: string, ed: string): Promise<ApiResponse> {
|
||||||
|
return this.wsRequest<ApiResponse>('HOLYDAY_GET_USERLIST', 'HOLYDAY_USERLIST_DATA', { sd, ed });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHolydayPermission(): Promise<ApiResponse> {
|
||||||
|
return this.wsRequest<ApiResponse>('HOLYDAY_GET_PERMISSION', 'HOLYDAY_PERMISSION_DATA');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHolydayBalance(year: string, uid: string): Promise<ApiResponse<HolydayBalance[]>> {
|
||||||
|
return this.wsRequest<ApiResponse<HolydayBalance[]>>('HOLYDAY_GET_BALANCE', 'HOLYDAY_BALANCE_DATA', { year, uid });
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Favorite API =====
|
// ===== Favorite API =====
|
||||||
|
|
||||||
public async getFavoriteList(): Promise<ApiResponse> {
|
public async getFavoriteList(): Promise<ApiResponse> {
|
||||||
@@ -701,14 +743,14 @@ class CommunicationLayer {
|
|||||||
public async addJobReport(
|
public async addJobReport(
|
||||||
pdate: string, projectName: string, pidx: number | null, requestpart: string, package_: string,
|
pdate: string, projectName: string, pidx: number | null, requestpart: string, package_: string,
|
||||||
type: string, process: string, status: string, description: string,
|
type: string, process: string, status: string, description: string,
|
||||||
hrs: number, ot: number, jobgrp: string, tag: string
|
hrs: number, ot: number, jobgrp: string, tag: string, otStart: string, otEnd: string
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
if (isWebView && machine) {
|
if (isWebView && machine) {
|
||||||
const result = await machine.Jobreport_Add(pdate, projectName, pidx ?? -1, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag);
|
const result = await machine.Jobreport_Add(pdate, projectName, pidx ?? -1, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd);
|
||||||
return JSON.parse(result);
|
return JSON.parse(result);
|
||||||
} else {
|
} else {
|
||||||
return this.wsRequest<ApiResponse>('JOBREPORT_ADD', 'JOBREPORT_ADDED', {
|
return this.wsRequest<ApiResponse>('JOBREPORT_ADD', 'JOBREPORT_ADDED', {
|
||||||
pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: type, process, status, description, hrs, ot, jobgrp, tag
|
pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: type, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,14 +758,14 @@ class CommunicationLayer {
|
|||||||
public async editJobReport(
|
public async editJobReport(
|
||||||
idx: number, pdate: string, projectName: string, pidx: number | null, requestpart: string, package_: string,
|
idx: number, pdate: string, projectName: string, pidx: number | null, requestpart: string, package_: string,
|
||||||
type: string, process: string, status: string, description: string,
|
type: string, process: string, status: string, description: string,
|
||||||
hrs: number, ot: number, jobgrp: string, tag: string
|
hrs: number, ot: number, jobgrp: string, tag: string, otStart: string, otEnd: string
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
if (isWebView && machine) {
|
if (isWebView && machine) {
|
||||||
const result = await machine.Jobreport_Edit(idx, pdate, projectName, pidx ?? -1, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag);
|
const result = await machine.Jobreport_Edit(idx, pdate, projectName, pidx ?? -1, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd);
|
||||||
return JSON.parse(result);
|
return JSON.parse(result);
|
||||||
} else {
|
} else {
|
||||||
return this.wsRequest<ApiResponse>('JOBREPORT_EDIT', 'JOBREPORT_EDITED', {
|
return this.wsRequest<ApiResponse>('JOBREPORT_EDIT', 'JOBREPORT_EDITED', {
|
||||||
idx, pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: type, process, status, description, hrs, ot, jobgrp, tag
|
idx, pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: type, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -746,6 +788,15 @@ class CommunicationLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getJobReportTypeList(sd: string, ed: string, uid: string = ''): Promise<ApiResponse<JobReportTypeItem[]>> {
|
||||||
|
if (isWebView && machine) {
|
||||||
|
const result = await machine.Jobreport_GetTypeList(sd, ed, uid);
|
||||||
|
return JSON.parse(result);
|
||||||
|
} else {
|
||||||
|
return this.wsRequest<ApiResponse<JobReportTypeItem[]>>('JOBREPORT_GET_TYPE_LIST', 'JOBREPORT_TYPE_LIST_DATA', { sd, ed, uid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getJobTypes(process: string = ''): Promise<ApiResponse<JobTypeItem[]>> {
|
public async getJobTypes(process: string = ''): Promise<ApiResponse<JobTypeItem[]>> {
|
||||||
if (isWebView && machine) {
|
if (isWebView && machine) {
|
||||||
const result = await machine.Jobreport_GetJobTypes(process);
|
const result = await machine.Jobreport_GetJobTypes(process);
|
||||||
@@ -1012,12 +1063,12 @@ class CommunicationLayer {
|
|||||||
* 현재 사용자의 프로젝트 목록 조회 (업무일지 콤보박스용)
|
* 현재 사용자의 프로젝트 목록 조회 (업무일지 콤보박스용)
|
||||||
* @returns ApiResponse<{idx: number, name: string, status: string}[]>
|
* @returns ApiResponse<{idx: number, name: string, status: string}[]>
|
||||||
*/
|
*/
|
||||||
public async getUserProjects(): Promise<ApiResponse<{idx: number, name: string, status: string}[]>> {
|
public async getUserProjects(): Promise<ApiResponse<{ idx: number, name: string, status: string }[]>> {
|
||||||
if (isWebView && machine) {
|
if (isWebView && machine) {
|
||||||
const result = await machine.Project_GetUserProjects();
|
const result = await machine.Project_GetUserProjects();
|
||||||
return JSON.parse(result);
|
return JSON.parse(result);
|
||||||
} else {
|
} else {
|
||||||
return this.wsRequest<ApiResponse<{idx: number, name: string, status: string}[]>>('PROJECT_GET_USER_PROJECTS', 'PROJECT_USER_PROJECTS_DATA');
|
return this.wsRequest<ApiResponse<{ idx: number, name: string, status: string }[]>>('PROJECT_GET_USER_PROJECTS', 'PROJECT_USER_PROJECTS_DATA');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
350
Project/frontend/src/components/jobreport/JobReportDayDialog.tsx
Normal file
350
Project/frontend/src/components/jobreport/JobReportDayDialog.tsx
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react';
|
||||||
|
import { comms } from '@/communication';
|
||||||
|
import { JobReportDayItem, HolidayItem } from '@/types';
|
||||||
|
|
||||||
|
interface JobReportDayDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
initialMonth?: string; // YYYY-MM format
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DayColumn {
|
||||||
|
day: number;
|
||||||
|
dayOfWeek: string;
|
||||||
|
isHoliday: boolean;
|
||||||
|
holidayMemo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserRow {
|
||||||
|
uid: string;
|
||||||
|
uname: string;
|
||||||
|
processs: string;
|
||||||
|
dailyData: Map<number, { hrs: number; ot: number; jobtype: string }>;
|
||||||
|
totalHrs: number;
|
||||||
|
totalOt: number;
|
||||||
|
totalHolidayOt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportDayDialogProps) {
|
||||||
|
const [currentMonth, setCurrentMonth] = useState(initialMonth || new Date().toISOString().substring(0, 7));
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dayColumns, setDayColumns] = useState<DayColumn[]>([]);
|
||||||
|
const [userRows, setUserRows] = useState<UserRow[]>([]);
|
||||||
|
const [currentUserId, setCurrentUserId] = useState<string>('');
|
||||||
|
const [currentUserLevel, setCurrentUserLevel] = useState<number>(0);
|
||||||
|
const [authLevel, setAuthLevel] = useState<number>(0);
|
||||||
|
const [canViewAll, setCanViewAll] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// 요일 배열
|
||||||
|
const weekDays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||||
|
|
||||||
|
// 권한 및 사용자 정보 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadAuthAndUserInfo();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const loadAuthAndUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
// 현재 로그인 사용자 정보 가져오기
|
||||||
|
const loginStatus = await comms.checkLoginStatus();
|
||||||
|
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
|
||||||
|
const userId = loginStatus.User.Id;
|
||||||
|
const userLevel = loginStatus.User.Level || 0;
|
||||||
|
setCurrentUserId(userId);
|
||||||
|
setCurrentUserLevel(userLevel);
|
||||||
|
|
||||||
|
// 업무일지(jobreport) 권한 가져오기
|
||||||
|
const authResponse = await comms.checkAuth('jobreport', 5);
|
||||||
|
const jobReportAuthLevel = authResponse.EffectiveLevel || 0;
|
||||||
|
setAuthLevel(jobReportAuthLevel);
|
||||||
|
|
||||||
|
// 유효 권한 레벨 = Max(사용자레벨, 권한레벨)
|
||||||
|
const effectiveLevel = Math.max(userLevel, jobReportAuthLevel);
|
||||||
|
setCanViewAll(effectiveLevel >= 5);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('권한 정보 로드 오류:', error);
|
||||||
|
setCanViewAll(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 데이터 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && currentUserId) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, [isOpen, currentMonth, currentUserId, canViewAll]);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 현재는 기존 API를 사용하여 월별 데이터를 가져옴
|
||||||
|
const startDate = `${currentMonth}-01`;
|
||||||
|
const year = parseInt(currentMonth.substring(0, 4));
|
||||||
|
const month = parseInt(currentMonth.substring(5, 7));
|
||||||
|
const lastDay = new Date(year, month, 0).getDate();
|
||||||
|
const endDate = `${currentMonth}-${String(lastDay).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
const [jobReportResponse, holidayResponse] = await Promise.all([
|
||||||
|
comms.getJobReportList(startDate, endDate, '', ''),
|
||||||
|
comms.getHolidayList(currentMonth)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (jobReportResponse.Success && jobReportResponse.Data) {
|
||||||
|
processData(jobReportResponse.Data, holidayResponse.Data || [], lastDay);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('데이터 로드 오류:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processData = (items: any[], holidays: HolidayItem[], lastDay: number) => {
|
||||||
|
// 날짜 컬럼 생성
|
||||||
|
const columns: DayColumn[] = [];
|
||||||
|
const year = parseInt(currentMonth.substring(0, 4));
|
||||||
|
const month = parseInt(currentMonth.substring(5, 7));
|
||||||
|
|
||||||
|
for (let day = 1; day <= lastDay; day++) {
|
||||||
|
const date = new Date(year, month - 1, day);
|
||||||
|
const dayOfWeek = weekDays[date.getDay()];
|
||||||
|
const dateStr = `${currentMonth}-${String(day).padStart(2, '0')}`;
|
||||||
|
const holiday = holidays.find(h => h.pdate === dateStr);
|
||||||
|
|
||||||
|
columns.push({
|
||||||
|
day,
|
||||||
|
dayOfWeek,
|
||||||
|
isHoliday: holiday?.free || false,
|
||||||
|
holidayMemo: holiday?.memo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDayColumns(columns);
|
||||||
|
|
||||||
|
// 사용자별 데이터 집계
|
||||||
|
const userMap = new Map<string, UserRow>();
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const uid = item.id || item.uid;
|
||||||
|
const uname = item.name || item.uname || uid;
|
||||||
|
const pdate = item.pdate?.substring(0, 10);
|
||||||
|
|
||||||
|
if (!pdate || !pdate.startsWith(currentMonth)) return;
|
||||||
|
|
||||||
|
// 권한 체크: 권한 레벨이 5 미만이고 본인이 아니면 스킵
|
||||||
|
if (!canViewAll && uid !== currentUserId) return;
|
||||||
|
|
||||||
|
const day = parseInt(pdate.substring(8, 10));
|
||||||
|
|
||||||
|
if (!userMap.has(uid)) {
|
||||||
|
userMap.set(uid, {
|
||||||
|
uid,
|
||||||
|
uname,
|
||||||
|
processs: item.userprocess || item.process || '',
|
||||||
|
dailyData: new Map(),
|
||||||
|
totalHrs: 0,
|
||||||
|
totalOt: 0,
|
||||||
|
totalHolidayOt: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRow = userMap.get(uid)!;
|
||||||
|
const existing = userRow.dailyData.get(day);
|
||||||
|
const hrs = (existing?.hrs || 0) + (item.hrs || 0);
|
||||||
|
const ot = (existing?.ot || 0) + (item.ot || 0);
|
||||||
|
const jobtype = item.type || existing?.jobtype || '';
|
||||||
|
|
||||||
|
userRow.dailyData.set(day, { hrs, ot, jobtype });
|
||||||
|
userRow.totalHrs += item.hrs || 0;
|
||||||
|
|
||||||
|
// 휴일 OT 계산
|
||||||
|
const column = columns[day - 1];
|
||||||
|
if (column?.isHoliday) {
|
||||||
|
userRow.totalHolidayOt += item.ot || 0;
|
||||||
|
} else {
|
||||||
|
userRow.totalOt += item.ot || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setUserRows(Array.from(userMap.values()).sort((a, b) => a.uname.localeCompare(b.uname)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 월 변경
|
||||||
|
const changeMonth = (delta: number) => {
|
||||||
|
const [year, month] = currentMonth.split('-').map(Number);
|
||||||
|
const newDate = new Date(year, month - 1 + delta, 1);
|
||||||
|
setCurrentMonth(`${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 셀 색상 결정
|
||||||
|
const getCellStyle = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => {
|
||||||
|
if (!data) return 'text-gray-400';
|
||||||
|
|
||||||
|
if (data.jobtype === '휴가') return 'text-red-500 font-medium';
|
||||||
|
if (isHoliday) return 'text-green-500 font-medium';
|
||||||
|
|
||||||
|
if (data.hrs > 8) return 'text-blue-500 font-medium';
|
||||||
|
if (data.hrs < 8) return 'text-red-500';
|
||||||
|
if (data.ot > 0) return 'text-purple-500 font-medium';
|
||||||
|
|
||||||
|
return 'text-white';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 셀 내용 포맷
|
||||||
|
const formatCellContent = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => {
|
||||||
|
if (!data) return '--';
|
||||||
|
|
||||||
|
if (data.jobtype === '휴가' && data.hrs + data.ot === 8) return '휴가';
|
||||||
|
|
||||||
|
const prefix = isHoliday ? '*' : '';
|
||||||
|
return `${prefix}${data.hrs}+${data.ot}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 엑셀 내보내기 (간단한 CSV)
|
||||||
|
const exportToExcel = () => {
|
||||||
|
let csv = '사원명,' + dayColumns.map(c => `${c.day}(${c.dayOfWeek})`).join(',') + ',합계\n';
|
||||||
|
|
||||||
|
userRows.forEach(row => {
|
||||||
|
const cells = dayColumns.map(col => {
|
||||||
|
const data = row.dailyData.get(col.day);
|
||||||
|
return formatCellContent(data, col.isHoliday);
|
||||||
|
});
|
||||||
|
const summary = `${row.totalHrs}+${row.totalOt}(*${row.totalHolidayOt})`;
|
||||||
|
csv += `${row.uname},${cells.join(',')},${summary}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `업무일지_일별집계_${currentMonth}.csv`;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-gradient-to-br from-gray-900 to-gray-800 rounded-2xl shadow-2xl w-full max-w-7xl max-h-[90vh] flex flex-col">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<h2 className="text-2xl font-bold text-white">일별 근무시간 집계</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => changeMonth(-1)}
|
||||||
|
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5 text-white" />
|
||||||
|
</button>
|
||||||
|
<span className="text-lg font-medium text-white min-w-[100px] text-center">
|
||||||
|
{currentMonth}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => changeMonth(1)}
|
||||||
|
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={exportToExcel}
|
||||||
|
className="px-4 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
내보내기
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6 text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 */}
|
||||||
|
<div className="flex-1 overflow-auto p-6">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-white/50">데이터를 불러오는 중...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead className="sticky top-0 bg-gray-800 z-10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-white/70 uppercase border border-white/10 bg-gray-800">
|
||||||
|
사원명
|
||||||
|
</th>
|
||||||
|
{dayColumns.map(col => (
|
||||||
|
<th
|
||||||
|
key={col.day}
|
||||||
|
className={`px-2 py-2 text-center text-xs font-medium uppercase border border-white/10 ${col.isHoliday ? 'bg-green-900/30 text-green-400' :
|
||||||
|
col.dayOfWeek === '일' ? 'bg-red-900/30 text-red-400' :
|
||||||
|
col.dayOfWeek === '토' ? 'bg-blue-900/30 text-blue-400' :
|
||||||
|
'bg-gray-800 text-white/70'
|
||||||
|
}`}
|
||||||
|
title={col.holidayMemo}
|
||||||
|
>
|
||||||
|
{col.day}<br />({col.dayOfWeek})
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="px-3 py-2 text-center text-xs font-medium text-white/70 uppercase border border-white/10 bg-gray-800">
|
||||||
|
합계
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{userRows.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={dayColumns.length + 2} className="px-4 py-8 text-center text-white/50">
|
||||||
|
조회된 데이터가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
userRows.map(row => (
|
||||||
|
<tr key={row.uid} className="hover:bg-white/5 transition-colors">
|
||||||
|
<td className="px-3 py-2 text-sm text-white border border-white/10 whitespace-nowrap">
|
||||||
|
{row.uname}
|
||||||
|
</td>
|
||||||
|
{dayColumns.map(col => {
|
||||||
|
const data = row.dailyData.get(col.day);
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={col.day}
|
||||||
|
className={`px-2 py-2 text-center text-sm border border-white/10 ${getCellStyle(data, col.isHoliday)}`}
|
||||||
|
>
|
||||||
|
{formatCellContent(data, col.isHoliday)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<td className="px-3 py-2 text-center text-sm text-white border border-white/10 font-medium whitespace-nowrap">
|
||||||
|
{row.totalHrs}+{row.totalOt}(*{row.totalHolidayOt})
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 범례 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 bg-gray-800/50">
|
||||||
|
<div className="flex flex-wrap gap-4 text-xs text-white/70">
|
||||||
|
<div><span className="text-gray-400">--</span> : 자료없음</div>
|
||||||
|
<div><span className="text-red-500">휴가</span> : 휴가</div>
|
||||||
|
<div><span className="text-green-500">*8+2</span> : 휴일근무</div>
|
||||||
|
<div><span className="text-blue-500">9+0</span> : 8시간 초과</div>
|
||||||
|
<div><span className="text-red-500">7+0</span> : 8시간 미만</div>
|
||||||
|
<div><span className="text-purple-500">8+2</span> : 8시간+OT</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ export interface JobreportFormData {
|
|||||||
description: string;
|
description: string;
|
||||||
hrs: number;
|
hrs: number;
|
||||||
ot: number;
|
ot: number;
|
||||||
|
otStart: string; // 초과근무 시작시간 (HH:mm 형식)
|
||||||
|
otEnd: string; // 초과근무 종료시간 (HH:mm 형식)
|
||||||
jobgrp: string;
|
jobgrp: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
}
|
}
|
||||||
@@ -39,6 +41,8 @@ export const initialFormData: JobreportFormData = {
|
|||||||
description: '',
|
description: '',
|
||||||
hrs: 8,
|
hrs: 8,
|
||||||
ot: 0,
|
ot: 0,
|
||||||
|
otStart: '18:00',
|
||||||
|
otEnd: '20:00',
|
||||||
jobgrp: '',
|
jobgrp: '',
|
||||||
tag: '',
|
tag: '',
|
||||||
};
|
};
|
||||||
@@ -339,11 +343,10 @@ export function JobreportEditModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowJobTypeModal(true)}
|
onClick={() => setShowJobTypeModal(true)}
|
||||||
className={`w-full border rounded-lg px-4 py-2 text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-primary-400 transition-colors ${
|
className={`w-full border rounded-lg px-4 py-2 text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-primary-400 transition-colors ${formData.type
|
||||||
formData.type
|
? 'bg-white/20 border-white/30 text-white'
|
||||||
? 'bg-white/20 border-white/30 text-white'
|
: 'bg-pink-500/30 border-pink-400/50 text-pink-200'
|
||||||
: 'bg-pink-500/30 border-pink-400/50 text-pink-200'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span>{getJobTypeDisplayText()}</span>
|
<span>{getJobTypeDisplayText()}</span>
|
||||||
<ChevronDown className="w-4 h-4 text-white/50" />
|
<ChevronDown className="w-4 h-4 text-white/50" />
|
||||||
@@ -411,6 +414,34 @@ export function JobreportEditModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 5행: 초과근무 시간대 (OT > 0일 때만 표시) */}
|
||||||
|
{formData.ot > 0 && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">
|
||||||
|
초과근무 시작시간
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={formData.otStart}
|
||||||
|
onChange={(e) => handleFieldChange('otStart', e.target.value)}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">
|
||||||
|
초과근무 종료시간
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={formData.otEnd}
|
||||||
|
onChange={(e) => handleFieldChange('otEnd', e.target.value)}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 업무내용 */}
|
{/* 업무내용 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-white/70 text-sm font-medium mb-2">
|
<label className="block text-white/70 text-sm font-medium mb-2">
|
||||||
|
|||||||
140
Project/frontend/src/components/jobreport/JobreportTypeModal.tsx
Normal file
140
Project/frontend/src/components/jobreport/JobreportTypeModal.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { X, RefreshCw } from 'lucide-react';
|
||||||
|
import { comms } from '@/communication';
|
||||||
|
import { JobReportTypeItem } from '@/types';
|
||||||
|
|
||||||
|
interface JobreportTypeModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JobreportTypeModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
userId,
|
||||||
|
}: JobreportTypeModalProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [items, setItems] = useState<JobReportTypeItem[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && startDate && endDate) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, [isOpen, startDate, endDate, userId]);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await comms.getJobReportTypeList(startDate, endDate, userId);
|
||||||
|
if (response.Success && response.Data) {
|
||||||
|
setItems(response.Data);
|
||||||
|
} else {
|
||||||
|
setItems([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('업무형태별 집계 로드 오류:', error);
|
||||||
|
setItems([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const totalHrs = items.reduce((acc, item) => acc + (item.hrs || 0), 0);
|
||||||
|
const totalOt = items.reduce((acc, item) => acc + (item.ot || 0), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
|
||||||
|
<div className="bg-gray-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
|
||||||
|
<h3 className="text-lg font-semibold text-white">업무형태별 집계</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-white/50 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 컨텐츠 */}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-4 text-white/70 text-sm">
|
||||||
|
기간: {startDate} ~ {endDate}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-xl border border-white/10">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">업무형태</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-medium text-white/70 uppercase">건수</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-medium text-white/70 uppercase">시간</th>
|
||||||
|
<th className="px-4 py-3 text-right text-xs font-medium text-white/70 uppercase">OT</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="px-4 py-8 text-center">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
||||||
|
<span className="text-white/50">데이터를 불러오는 중...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="px-4 py-8 text-center text-white/50">
|
||||||
|
데이터가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<tr key={index} className="hover:bg-white/5 transition-colors">
|
||||||
|
<td className="px-4 py-3 text-white text-sm">{item.type || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm text-right">{item.count}</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm text-right">{item.hrs}h</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm text-right">
|
||||||
|
{item.ot > 0 ? <span className="text-warning-400">{item.ot}h</span> : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{/* 합계 */}
|
||||||
|
<tr className="bg-white/5 font-semibold">
|
||||||
|
<td className="px-4 py-3 text-white text-sm">합계</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm text-right">
|
||||||
|
{items.reduce((acc, item) => acc + item.count, 0)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm text-right">{totalHrs}h</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm text-right">
|
||||||
|
{totalOt > 0 ? <span className="text-warning-400">{totalOt}h</span> : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-end bg-white/5">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
295
Project/frontend/src/components/kuntae/KuntaeEditModal.tsx
Normal file
295
Project/frontend/src/components/kuntae/KuntaeEditModal.tsx
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { X, Save, Calendar, Clock, FileText, User } from 'lucide-react';
|
||||||
|
import { KuntaeModel } from '@/types';
|
||||||
|
|
||||||
|
interface KuntaeEditModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (data: KuntaeFormData) => Promise<void>;
|
||||||
|
initialData?: KuntaeModel | null;
|
||||||
|
mode: 'add' | 'edit' | 'copy';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KuntaeFormData {
|
||||||
|
idx?: number;
|
||||||
|
cate: string;
|
||||||
|
sdate: string;
|
||||||
|
edate: string;
|
||||||
|
term: number;
|
||||||
|
crtime: number;
|
||||||
|
termDr: number;
|
||||||
|
drTime: number;
|
||||||
|
contents: string;
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATE_OPTIONS = ['연차', '대체', '공가', '경조', '병가', '오전반차', '오후반차', '조퇴', '외출', '지각', '결근', '휴직', '교육', '출장', '재택', '특근', '당직', '기타'];
|
||||||
|
|
||||||
|
export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }: KuntaeEditModalProps) {
|
||||||
|
const [formData, setFormData] = useState<KuntaeFormData>({
|
||||||
|
cate: '연차',
|
||||||
|
sdate: new Date().toISOString().split('T')[0],
|
||||||
|
edate: new Date().toISOString().split('T')[0],
|
||||||
|
term: 1,
|
||||||
|
crtime: 0,
|
||||||
|
termDr: 0,
|
||||||
|
drTime: 0,
|
||||||
|
contents: '',
|
||||||
|
uid: '',
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (initialData) {
|
||||||
|
setFormData({
|
||||||
|
idx: mode === 'edit' ? initialData.idx : undefined,
|
||||||
|
cate: initialData.cate || '연차',
|
||||||
|
sdate: initialData.sdate || new Date().toISOString().split('T')[0],
|
||||||
|
edate: initialData.edate || new Date().toISOString().split('T')[0],
|
||||||
|
term: initialData.term || 0,
|
||||||
|
crtime: initialData.crtime || 0,
|
||||||
|
termDr: initialData.termDr || 0,
|
||||||
|
drTime: initialData.DrTime || 0,
|
||||||
|
contents: initialData.contents || '',
|
||||||
|
uid: initialData.uid || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 초기화
|
||||||
|
setFormData({
|
||||||
|
cate: '연차',
|
||||||
|
sdate: new Date().toISOString().split('T')[0],
|
||||||
|
edate: new Date().toISOString().split('T')[0],
|
||||||
|
term: 1,
|
||||||
|
crtime: 0,
|
||||||
|
termDr: 0,
|
||||||
|
drTime: 0,
|
||||||
|
contents: '',
|
||||||
|
uid: '', // 상위 컴포넌트에서 현재 사용자 ID를 주입받거나 여기서 처리해야 함
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, initialData, mode]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: name === 'term' || name === 'crtime' || name === 'termDr' || name === 'drTime'
|
||||||
|
? parseFloat(value) || 0
|
||||||
|
: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await onSave(formData);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
alert('저장 중 오류가 발생했습니다.');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 날짜 변경 시 기간 자동 계산 (단순 1일 차이)
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData.sdate && formData.edate) {
|
||||||
|
const start = new Date(formData.sdate);
|
||||||
|
const end = new Date(formData.edate);
|
||||||
|
const diffTime = Math.abs(end.getTime() - start.getTime());
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||||
|
|
||||||
|
// 연차/휴가 등일 때만 자동 계산
|
||||||
|
if (['연차', '공가', '경조', '병가', '교육', '출장'].includes(formData.cate)) {
|
||||||
|
setFormData(prev => ({ ...prev, term: diffDays }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [formData.sdate, formData.edate, formData.cate]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const title = mode === 'add' ? '근태 등록' : mode === 'edit' ? '근태 수정' : '근태 복사 등록';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
|
||||||
|
<div className="bg-[#1e1e2e] rounded-2xl shadow-2xl w-full max-w-lg border border-white/10 overflow-hidden">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex justify-between items-center bg-white/5">
|
||||||
|
<h2 className="text-xl font-bold text-white flex items-center">
|
||||||
|
<Calendar className="w-5 h-5 mr-2 text-primary-400" />
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<button onClick={onClose} className="text-white/50 hover:text-white transition-colors">
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 폼 */}
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||||
|
|
||||||
|
{/* 구분 및 사용자 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">구분</label>
|
||||||
|
<select
|
||||||
|
name="cate"
|
||||||
|
value={formData.cate}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
>
|
||||||
|
{CATE_OPTIONS.map(opt => (
|
||||||
|
<option key={opt} value={opt} className="bg-[#1e1e2e]">{opt}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">사용자 ID</label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="uid"
|
||||||
|
value={formData.uid}
|
||||||
|
onChange={handleChange}
|
||||||
|
readOnly={mode === 'edit'} // 수정 시에는 사용자 변경 불가
|
||||||
|
className={`w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 ${mode === 'edit' ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
placeholder="사번 입력"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 기간 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">시작일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="sdate"
|
||||||
|
value={formData.sdate}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">종료일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="edate"
|
||||||
|
value={formData.edate}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 사용량 (일/시간) */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">사용 (일)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="term"
|
||||||
|
value={formData.term}
|
||||||
|
onChange={handleChange}
|
||||||
|
step="0.5"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">사용 (시간)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="crtime"
|
||||||
|
value={formData.crtime}
|
||||||
|
onChange={handleChange}
|
||||||
|
step="0.5"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 발생량 (일/시간) - 대체근무 등 발생 시 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">발생 (일)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="termDr"
|
||||||
|
value={formData.termDr}
|
||||||
|
onChange={handleChange}
|
||||||
|
step="0.5"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">발생 (시간)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="drTime"
|
||||||
|
value={formData.drTime}
|
||||||
|
onChange={handleChange}
|
||||||
|
step="0.5"
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 내용 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-1">내용</label>
|
||||||
|
<div className="relative">
|
||||||
|
<FileText className="absolute left-3 top-3 w-4 h-4 text-white/50" />
|
||||||
|
<textarea
|
||||||
|
name="contents"
|
||||||
|
value={formData.contents}
|
||||||
|
onChange={handleChange}
|
||||||
|
rows={3}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none"
|
||||||
|
placeholder="근태 사유 또는 내용 입력"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 버튼 */}
|
||||||
|
<div className="flex justify-end space-x-3 pt-4 border-t border-white/10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
|
||||||
|
저장 중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
저장
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,10 +5,13 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Copy,
|
Copy,
|
||||||
Plus,
|
Plus,
|
||||||
|
Calendar,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { comms } from '@/communication';
|
import { comms } from '@/communication';
|
||||||
import { JobReportItem, JobReportUser } from '@/types';
|
import { JobReportItem, JobReportUser } from '@/types';
|
||||||
import { JobreportEditModal, JobreportFormData, initialFormData } from '@/components/jobreport/JobreportEditModal';
|
import { JobreportEditModal, JobreportFormData, initialFormData } from '@/components/jobreport/JobreportEditModal';
|
||||||
|
import { JobReportDayDialog } from '@/components/jobreport/JobReportDayDialog';
|
||||||
|
import { JobreportTypeModal } from '@/components/jobreport/JobreportTypeModal';
|
||||||
|
|
||||||
export function Jobreport() {
|
export function Jobreport() {
|
||||||
const [jobreportList, setJobreportList] = useState<JobReportItem[]>([]);
|
const [jobreportList, setJobreportList] = useState<JobReportItem[]>([]);
|
||||||
@@ -24,6 +27,8 @@ export function Jobreport() {
|
|||||||
|
|
||||||
// 모달 상태
|
// 모달 상태
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [showDayReportModal, setShowDayReportModal] = useState(false);
|
||||||
|
const [showTypeReportModal, setShowTypeReportModal] = useState(false);
|
||||||
const [editingItem, setEditingItem] = useState<JobReportItem | null>(null);
|
const [editingItem, setEditingItem] = useState<JobReportItem | null>(null);
|
||||||
const [formData, setFormData] = useState<JobreportFormData>(initialFormData);
|
const [formData, setFormData] = useState<JobreportFormData>(initialFormData);
|
||||||
|
|
||||||
@@ -200,6 +205,8 @@ export function Jobreport() {
|
|||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
hrs: 0, // 시간 초기화
|
hrs: 0, // 시간 초기화
|
||||||
ot: 0, // OT 초기화
|
ot: 0, // OT 초기화
|
||||||
|
otStart: data.otStart ? data.otStart.substring(11, 16) : '18:00',
|
||||||
|
otEnd: data.otEnd ? data.otEnd.substring(11, 16) : '20:00',
|
||||||
jobgrp: '',
|
jobgrp: '',
|
||||||
tag: '',
|
tag: '',
|
||||||
});
|
});
|
||||||
@@ -230,6 +237,8 @@ export function Jobreport() {
|
|||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
hrs: data.hrs || 0,
|
hrs: data.hrs || 0,
|
||||||
ot: data.ot || 0,
|
ot: data.ot || 0,
|
||||||
|
otStart: data.otStart ? data.otStart.substring(11, 16) : '18:00',
|
||||||
|
otEnd: data.otEnd ? data.otEnd.substring(11, 16) : '20:00',
|
||||||
jobgrp: data.jobgrp || '',
|
jobgrp: data.jobgrp || '',
|
||||||
tag: data.tag || '',
|
tag: data.tag || '',
|
||||||
});
|
});
|
||||||
@@ -276,7 +285,9 @@ export function Jobreport() {
|
|||||||
formData.hrs || 0,
|
formData.hrs || 0,
|
||||||
formData.ot || 0,
|
formData.ot || 0,
|
||||||
formData.jobgrp || '',
|
formData.jobgrp || '',
|
||||||
formData.tag || ''
|
formData.tag || '',
|
||||||
|
formData.otStart || '18:00',
|
||||||
|
formData.otEnd || '20:00'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
response = await comms.addJobReport(
|
response = await comms.addJobReport(
|
||||||
@@ -292,7 +303,9 @@ export function Jobreport() {
|
|||||||
formData.hrs || 0,
|
formData.hrs || 0,
|
||||||
formData.ot || 0,
|
formData.ot || 0,
|
||||||
formData.jobgrp || '',
|
formData.jobgrp || '',
|
||||||
formData.tag || ''
|
formData.tag || '',
|
||||||
|
formData.otStart || '18:00',
|
||||||
|
formData.otEnd || '20:00'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +432,7 @@ export function Jobreport() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 버튼 영역: 우측 수직 배치 */}
|
{/* 버튼 영역: 우측 수직 배치 */}
|
||||||
<div className="grid grid-rows-2 gap-y-3">
|
<div className="flex flex-col gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleSearchWithReset}
|
onClick={handleSearchWithReset}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -443,6 +456,24 @@ export function Jobreport() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 중앙: 집계 메뉴 */}
|
||||||
|
<div className="flex-shrink-0 flex flex-col gap-3 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDayReportModal(true)}
|
||||||
|
className="h-12 bg-indigo-500 hover:bg-indigo-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
일별 집계
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTypeReportModal(true)}
|
||||||
|
className="h-12 bg-purple-500 hover:bg-purple-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
업무형태별 집계
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 우측: 오늘 근무시간 */}
|
{/* 우측: 오늘 근무시간 */}
|
||||||
<div className="flex-shrink-0 w-48">
|
<div className="flex-shrink-0 w-48">
|
||||||
<div className="bg-white/10 rounded-xl p-4 h-full flex flex-col justify-center">
|
<div className="bg-white/10 rounded-xl p-4 h-full flex flex-col justify-center">
|
||||||
@@ -523,9 +554,8 @@ export function Jobreport() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.type || '-'}</td>
|
<td className="px-4 py-3 text-white text-sm">{item.type || '-'}</td>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
<span className={`px-2 py-1 rounded text-xs ${
|
<span className={`px-2 py-1 rounded text-xs ${item.status?.includes('완료') ? 'bg-green-500/20 text-green-400' : 'bg-white/20 text-white/70'
|
||||||
item.status?.includes('완료') ? 'bg-green-500/20 text-green-400' : 'bg-white/20 text-white/70'
|
}`}>
|
||||||
}`}>
|
|
||||||
{item.status || '-'}
|
{item.status || '-'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -602,6 +632,22 @@ export function Jobreport() {
|
|||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 일별 집계 모달 */}
|
||||||
|
<JobReportDayDialog
|
||||||
|
isOpen={showDayReportModal}
|
||||||
|
onClose={() => setShowDayReportModal(false)}
|
||||||
|
initialMonth={startDate.substring(0, 7)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 업무형태별 집계 모달 */}
|
||||||
|
<JobreportTypeModal
|
||||||
|
isOpen={showTypeReportModal}
|
||||||
|
onClose={() => setShowTypeReportModal(false)}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
userId={selectedUser}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Search,
|
Search,
|
||||||
@@ -8,9 +8,17 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Copy,
|
||||||
|
User,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Filter
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { comms } from '@/communication';
|
import { comms } from '@/communication';
|
||||||
import { KuntaeModel } from '@/types';
|
import { KuntaeModel, HolydayPermission, HolydayUser, HolydayBalance } from '@/types';
|
||||||
|
import { KuntaeEditModal, KuntaeFormData } from '@/components/kuntae/KuntaeEditModal';
|
||||||
|
|
||||||
export function Kuntae() {
|
export function Kuntae() {
|
||||||
const [kuntaeList, setKuntaeList] = useState<KuntaeModel[]>([]);
|
const [kuntaeList, setKuntaeList] = useState<KuntaeModel[]>([]);
|
||||||
@@ -20,66 +28,132 @@ export function Kuntae() {
|
|||||||
// 검색 조건
|
// 검색 조건
|
||||||
const [startDate, setStartDate] = useState('');
|
const [startDate, setStartDate] = useState('');
|
||||||
const [endDate, setEndDate] = useState('');
|
const [endDate, setEndDate] = useState('');
|
||||||
|
const [selectedUser, setSelectedUser] = useState('%');
|
||||||
|
const [filterText, setFilterText] = useState(''); // 클라이언트 필터
|
||||||
|
|
||||||
// 통계
|
// 권한 및 사용자 목록
|
||||||
const [stats, setStats] = useState({
|
const [permission, setPermission] = useState<HolydayPermission | null>(null);
|
||||||
holyUsed: 0,
|
const [userList, setUserList] = useState<HolydayUser[]>([]);
|
||||||
alternateUsed: 0,
|
|
||||||
holyRemain: 0,
|
|
||||||
alternateRemain: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 날짜 초기화 (현재 월)
|
// 모달 상태
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<'add' | 'edit' | 'copy'>('add');
|
||||||
|
const [selectedItem, setSelectedItem] = useState<KuntaeModel | null>(null);
|
||||||
|
|
||||||
|
// 통계 (잔량 정보)
|
||||||
|
const [balances, setBalances] = useState<HolydayBalance[]>([]);
|
||||||
|
|
||||||
|
// 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const now = new Date();
|
const init = async () => {
|
||||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
// 날짜 초기화 (현재 월)
|
||||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
const now = new Date();
|
||||||
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
|
|
||||||
setStartDate(startOfMonth.toISOString().split('T')[0]);
|
const sd = startOfMonth.toISOString().split('T')[0];
|
||||||
setEndDate(endOfMonth.toISOString().split('T')[0]);
|
const ed = endOfMonth.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
setStartDate(sd);
|
||||||
|
setEndDate(ed);
|
||||||
|
|
||||||
|
// 권한 조회
|
||||||
|
try {
|
||||||
|
const permResponse = await comms.getHolydayPermission();
|
||||||
|
if (permResponse.Success && permResponse.Data) {
|
||||||
|
// @ts-ignore - API 응답 타입 불일치 해결
|
||||||
|
setPermission(permResponse.Data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('권한 조회 오류:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 데이터 로드
|
// 사용자 목록 로드 (기간 변경 시)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadUsers = async () => {
|
||||||
|
if (!startDate || !endDate) return;
|
||||||
|
try {
|
||||||
|
const response = await comms.getHolydayUserList(startDate, endDate);
|
||||||
|
// @ts-ignore - API 응답이 배열로 옴
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
setUserList(response);
|
||||||
|
} else if (response.Success && response.Data) {
|
||||||
|
// @ts-ignore
|
||||||
|
setUserList(response.Data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사용자 목록 로드 오류:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadUsers();
|
||||||
|
}, [startDate, endDate]);
|
||||||
|
|
||||||
|
// 데이터 및 잔량 로드
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
if (!startDate || !endDate) return;
|
if (!startDate || !endDate) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await comms.getKuntaeList(startDate, endDate);
|
// 1. 목록 조회
|
||||||
if (response.Success && response.Data) {
|
const listResponse = await comms.getHolydayList(startDate, endDate, selectedUser);
|
||||||
setKuntaeList(response.Data);
|
if (listResponse.Success && listResponse.Data) {
|
||||||
updateStats(response.Data);
|
setKuntaeList(listResponse.Data);
|
||||||
} else {
|
} else {
|
||||||
setKuntaeList([]);
|
setKuntaeList([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 잔량 조회 (연도 기준)
|
||||||
|
const year = startDate.substring(0, 4);
|
||||||
|
// 전체 사용자(%) 선택 시에는 로그인한 사용자 기준 잔량을 보여주거나, 비워두는 게 맞음
|
||||||
|
// 여기서는 선택된 사용자가 있으면 그 사용자, 없으면 본인(권한 없으면 에러나겠지만 API에서 처리)
|
||||||
|
const targetUid = selectedUser === '%' ? (permission?.CurrentUserId || '') : selectedUser;
|
||||||
|
|
||||||
|
if (targetUid) {
|
||||||
|
const balanceResponse = await comms.getHolydayBalance(year, targetUid);
|
||||||
|
if (balanceResponse.Success && balanceResponse.Data) {
|
||||||
|
setBalances(balanceResponse.Data);
|
||||||
|
} else {
|
||||||
|
setBalances([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('근태 목록 로드 오류:', error);
|
console.error('데이터 로드 오류:', error);
|
||||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [startDate, endDate]);
|
}, [startDate, endDate, selectedUser, permission]);
|
||||||
|
|
||||||
// 통계 업데이트
|
// 초기 로드 및 검색 조건 변경 시 로드
|
||||||
const updateStats = (data: KuntaeModel[]) => {
|
useEffect(() => {
|
||||||
const holyUsed = data.filter(item => item.cate === '연차' || item.cate === '휴가').length;
|
|
||||||
const alternateUsed = data.filter(item => item.cate === '대체').length;
|
|
||||||
|
|
||||||
setStats({
|
|
||||||
holyUsed,
|
|
||||||
alternateUsed,
|
|
||||||
holyRemain: 15 - holyUsed, // 예시 값
|
|
||||||
alternateRemain: 5 - alternateUsed, // 예시 값
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 검색
|
|
||||||
const handleSearch = () => {
|
|
||||||
if (new Date(startDate) > new Date(endDate)) {
|
|
||||||
alert('시작일은 종료일보다 늦을 수 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadData();
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
// 필터링된 목록
|
||||||
|
const filteredList = useMemo(() => {
|
||||||
|
if (!filterText) return kuntaeList;
|
||||||
|
const lowerText = filterText.toLowerCase();
|
||||||
|
return kuntaeList.filter(item =>
|
||||||
|
(item.cate && item.cate.toLowerCase().includes(lowerText)) ||
|
||||||
|
(item.contents && item.contents.toLowerCase().includes(lowerText)) ||
|
||||||
|
(item.UserName && item.UserName.toLowerCase().includes(lowerText))
|
||||||
|
);
|
||||||
|
}, [kuntaeList, filterText]);
|
||||||
|
|
||||||
|
// 월 이동
|
||||||
|
const moveMonth = (offset: number) => {
|
||||||
|
const current = new Date(startDate);
|
||||||
|
current.setMonth(current.getMonth() + offset);
|
||||||
|
|
||||||
|
const startOfMonth = new Date(current.getFullYear(), current.getMonth(), 1);
|
||||||
|
const endOfMonth = new Date(current.getFullYear(), current.getMonth() + 1, 0);
|
||||||
|
|
||||||
|
setStartDate(startOfMonth.toISOString().split('T')[0]);
|
||||||
|
setEndDate(endOfMonth.toISOString().split('T')[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 삭제
|
// 삭제
|
||||||
@@ -88,7 +162,7 @@ export function Kuntae() {
|
|||||||
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
try {
|
try {
|
||||||
const response = await comms.deleteKuntae(id);
|
const response = await comms.deleteHolyday(id);
|
||||||
if (response.Success) {
|
if (response.Success) {
|
||||||
alert('삭제되었습니다.');
|
alert('삭제되었습니다.');
|
||||||
loadData();
|
loadData();
|
||||||
@@ -103,93 +177,218 @@ export function Kuntae() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 모달 열기
|
||||||
|
const openModal = (mode: 'add' | 'edit' | 'copy', item?: KuntaeModel) => {
|
||||||
|
setModalMode(mode);
|
||||||
|
setSelectedItem(item || null);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 저장 처리
|
||||||
|
const handleSave = async (formData: KuntaeFormData) => {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
if (modalMode === 'add' || modalMode === 'copy') {
|
||||||
|
response = await comms.addHolyday(
|
||||||
|
formData.cate,
|
||||||
|
formData.sdate,
|
||||||
|
formData.edate,
|
||||||
|
formData.term,
|
||||||
|
formData.crtime,
|
||||||
|
formData.termDr,
|
||||||
|
formData.drTime,
|
||||||
|
formData.contents,
|
||||||
|
formData.uid || (permission?.CurrentUserId || '')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (!formData.idx) return;
|
||||||
|
response = await comms.editHolyday(
|
||||||
|
formData.idx,
|
||||||
|
formData.cate,
|
||||||
|
formData.sdate,
|
||||||
|
formData.edate,
|
||||||
|
formData.term,
|
||||||
|
formData.crtime,
|
||||||
|
formData.termDr,
|
||||||
|
formData.drTime,
|
||||||
|
formData.contents
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Success) {
|
||||||
|
alert(response.Message || '저장되었습니다.');
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
alert(response.Message || '저장에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('저장 오류:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 날짜 포맷
|
// 날짜 포맷
|
||||||
const formatDate = (dateStr: string | null) => {
|
const formatDate = (dateStr: string | null) => {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
return new Date(dateStr).toLocaleDateString('ko-KR');
|
return dateStr.split('T')[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
{/* 개발중 경고 */}
|
{/* 상단 컨트롤 바 */}
|
||||||
<div className="bg-warning-500/20 border border-warning-500/30 rounded-xl p-4 flex items-center">
|
|
||||||
<AlertTriangle className="w-5 h-5 text-warning-400 mr-3 flex-shrink-0" />
|
|
||||||
<div>
|
|
||||||
<p className="text-white font-medium">개발중인 기능입니다</p>
|
|
||||||
<p className="text-white/60 text-sm">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 검색 필터 */}
|
|
||||||
<div className="glass-effect rounded-2xl p-6">
|
<div className="glass-effect rounded-2xl p-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div>
|
|
||||||
<label className="block text-white/70 text-sm font-medium mb-2">시작일</label>
|
{/* 1행: 날짜, 사용자, 조회/등록 */}
|
||||||
<input
|
<div className="flex flex-col md:flex-row gap-4 items-end md:items-center justify-between">
|
||||||
type="date"
|
{/* 날짜 선택 및 월 이동 */}
|
||||||
value={startDate}
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
<button
|
||||||
className="w-full bg-white/20 border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
onClick={() => moveMonth(-1)}
|
||||||
/>
|
className="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
className="bg-white/20 border border-white/30 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 text-sm"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
className="bg-white/20 border border-white/30 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => moveMonth(1)}
|
||||||
|
className="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 사용자 선택 (관리자용) */}
|
||||||
|
{permission?.CanManage && (
|
||||||
|
<div className="w-full md:w-64">
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
||||||
|
<select
|
||||||
|
value={selectedUser}
|
||||||
|
onChange={(e) => setSelectedUser(e.target.value)}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-lg pl-10 pr-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 appearance-none"
|
||||||
|
>
|
||||||
|
<option value="%" className="bg-[#1e1e2e]">전체 사용자</option>
|
||||||
|
{userList.map(user => (
|
||||||
|
<option key={user.uid} value={user.uid} className="bg-[#1e1e2e]">
|
||||||
|
{user.UserName} ({user.uid})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 조회 및 등록 버튼 */}
|
||||||
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => loadData()}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex-1 md:flex-none bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? <RefreshCw className="w-4 h-4 mr-2 animate-spin" /> : <Search className="w-4 h-4 mr-2" />}
|
||||||
|
조회
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openModal('add')}
|
||||||
|
className="flex-1 md:flex-none bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
등록
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-white/70 text-sm font-medium mb-2">종료일</label>
|
{/* 2행: 검색 필터 */}
|
||||||
<input
|
<div className="flex items-center gap-2">
|
||||||
type="date"
|
<div className="relative flex-1 max-w-md">
|
||||||
value={endDate}
|
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
<input
|
||||||
className="w-full bg-white/20 border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
type="text"
|
||||||
/>
|
value={filterText}
|
||||||
</div>
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
<div className="flex items-end">
|
placeholder="구분, 내용, 성명으로 검색..."
|
||||||
<button
|
className="w-full bg-white/10 border border-white/20 rounded-lg pl-10 pr-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 text-sm placeholder-white/30"
|
||||||
onClick={handleSearch}
|
/>
|
||||||
disabled={loading}
|
{filterText && (
|
||||||
className="w-full bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
|
<button
|
||||||
>
|
onClick={() => setFilterText('')}
|
||||||
{loading ? (
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-white/50 hover:text-white"
|
||||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
>
|
||||||
) : (
|
<XCircle className="w-4 h-4" />
|
||||||
<Search className="w-4 h-4 mr-2" />
|
</button>
|
||||||
)}
|
)}
|
||||||
조회
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 통계 카드 */}
|
{/* 통계 카드 (잔량 정보) */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<StatCard
|
{balances.length > 0 ? (
|
||||||
title="휴가 사용"
|
balances.map((bal, idx) => {
|
||||||
value={stats.holyUsed}
|
// 잔량 계산
|
||||||
icon={<Calendar className="w-6 h-6 text-primary-400" />}
|
const remainDays = bal.TotalGenDays - bal.TotalUseDays;
|
||||||
color="text-primary-400"
|
const remainHours = bal.TotalGenHours - bal.TotalUseHours;
|
||||||
/>
|
|
||||||
<StatCard
|
// 아이콘 및 색상 결정
|
||||||
title="대체 사용"
|
let icon = <Clock className="w-6 h-6" />;
|
||||||
value={stats.alternateUsed}
|
let color = "text-white";
|
||||||
icon={<CheckCircle className="w-6 h-6 text-success-400" />}
|
|
||||||
color="text-success-400"
|
if (bal.cate === '연차') {
|
||||||
/>
|
icon = <Calendar className="w-6 h-6 text-primary-400" />;
|
||||||
<StatCard
|
color = "text-primary-400";
|
||||||
title="잔량 (연차)"
|
} else if (bal.cate === '대체') {
|
||||||
value={stats.holyRemain}
|
icon = <RefreshCw className="w-6 h-6 text-success-400" />;
|
||||||
icon={<Clock className="w-6 h-6 text-warning-400" />}
|
color = "text-success-400";
|
||||||
color="text-warning-400"
|
} else if (bal.cate === '휴가') {
|
||||||
/>
|
icon = <CheckCircle className="w-6 h-6 text-warning-400" />;
|
||||||
<StatCard
|
color = "text-warning-400";
|
||||||
title="잔량 (대체)"
|
}
|
||||||
value={stats.alternateRemain}
|
|
||||||
icon={<XCircle className="w-6 h-6 text-danger-400" />}
|
return (
|
||||||
color="text-danger-400"
|
<StatCard
|
||||||
/>
|
key={idx}
|
||||||
|
title={`${bal.cate} 잔량`}
|
||||||
|
value={`${remainDays}일 ${remainHours > 0 ? `(${remainHours}h)` : ''}`}
|
||||||
|
subValue={`발생: ${bal.TotalGenDays} / 사용: ${bal.TotalUseDays}`}
|
||||||
|
icon={icon}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
// 데이터 없을 때 기본 카드 표시
|
||||||
|
<>
|
||||||
|
<StatCard title="연차 잔량" value="-" icon={<Calendar className="w-6 h-6 text-white/30" />} color="text-white/30" />
|
||||||
|
<StatCard title="대체 잔량" value="-" icon={<RefreshCw className="w-6 h-6 text-white/30" />} color="text-white/30" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 테이블 */}
|
{/* 데이터 테이블 */}
|
||||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-white/10">
|
<div className="px-6 py-4 border-b border-white/10 flex justify-between items-center">
|
||||||
<h3 className="text-lg font-semibold text-white">근태 상세 내역</h3>
|
<h3 className="text-lg font-semibold text-white">근태 상세 내역</h3>
|
||||||
|
<span className="text-white/50 text-sm">
|
||||||
|
총 {filteredList.length}건
|
||||||
|
{kuntaeList.length !== filteredList.length && ` (전체 ${kuntaeList.length}건 중)`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -199,56 +398,80 @@ export function Kuntae() {
|
|||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">구분</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">구분</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">시작일</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">시작일</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">종료일</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">종료일</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">사번</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">성명</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">사용(일)</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">사용(일)</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">사용(시간)</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">내용</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">내용</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">성명</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">소스</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">소스</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">등록자</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">등록일</th>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">작업</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/10">
|
<tbody className="divide-y divide-white/10">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={11} className="px-4 py-8 text-center">
|
<td colSpan={9} className="px-4 py-8 text-center">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
||||||
<span className="text-white/50">데이터를 불러오는 중...</span>
|
<span className="text-white/50">데이터를 불러오는 중...</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : kuntaeList.length === 0 ? (
|
) : filteredList.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={11} className="px-4 py-8 text-center text-white/50">
|
<td colSpan={9} className="px-4 py-8 text-center text-white/50">
|
||||||
조회된 데이터가 없습니다.
|
{filterText ? '검색 결과가 없습니다.' : '조회된 데이터가 없습니다.'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
kuntaeList.map((item) => (
|
filteredList.map((item) => (
|
||||||
<tr key={item.idx} className="hover:bg-white/5 transition-colors">
|
<tr key={item.idx} className={`hover:bg-white/5 transition-colors ${item.extidx ? 'bg-black/20' : ''}`}>
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.cate || '-'}</td>
|
<td className="px-4 py-3 text-white text-sm">
|
||||||
|
<span className={`px-2 py-1 rounded text-xs ${item.cate === '연차' ? 'bg-primary-500/20 text-primary-300' :
|
||||||
|
item.cate === '대체' ? 'bg-success-500/20 text-success-300' :
|
||||||
|
'bg-white/10 text-white/70'
|
||||||
|
}`}>
|
||||||
|
{item.cate || '-'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{formatDate(item.sdate)}</td>
|
<td className="px-4 py-3 text-white text-sm">{formatDate(item.sdate)}</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{formatDate(item.edate)}</td>
|
<td className="px-4 py-3 text-white text-sm">{formatDate(item.edate)}</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.uid || '-'}</td>
|
<td className="px-4 py-3 text-white text-sm">{item.term > 0 ? item.term : '-'}</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.uname || '-'}</td>
|
<td className="px-4 py-3 text-white text-sm">{item.crtime > 0 ? item.crtime : '-'}</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.term || '-'}</td>
|
|
||||||
<td className="px-4 py-3 text-white/80 text-sm max-w-xs truncate" title={item.contents}>
|
<td className="px-4 py-3 text-white/80 text-sm max-w-xs truncate" title={item.contents}>
|
||||||
{item.contents || '-'}
|
{item.contents || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.extcate || '-'}</td>
|
<td className="px-4 py-3 text-white text-sm">
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.wuid || '-'}</td>
|
{item.UserName || item.uname || item.uid}
|
||||||
<td className="px-4 py-3 text-white text-sm">{item.wdate || '-'}</td>
|
</td>
|
||||||
|
<td className="px-4 py-3 text-white/50 text-xs">
|
||||||
|
{item.extcate ? `${item.extcate}` : '-'}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
<button
|
<div className="flex items-center space-x-2">
|
||||||
onClick={() => handleDelete(item.idx)}
|
<button
|
||||||
disabled={processing}
|
onClick={() => openModal('edit', item)}
|
||||||
className="text-danger-400 hover:text-danger-300 transition-colors disabled:opacity-50"
|
className={`text-primary-400 hover:text-primary-300 transition-colors ${item.extidx ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
title="삭제"
|
title={item.extidx ? "외부 연동 데이터는 수정할 수 없습니다" : "수정"}
|
||||||
>
|
disabled={!!item.extidx}
|
||||||
<Trash2 className="w-4 h-4" />
|
>
|
||||||
</button>
|
<Edit className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openModal('copy', item)}
|
||||||
|
className="text-success-400 hover:text-success-300 transition-colors"
|
||||||
|
title="복사"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(item.idx)}
|
||||||
|
className={`text-danger-400 hover:text-danger-300 transition-colors ${item.extidx ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
title={item.extidx ? "외부 연동 데이터는 삭제할 수 없습니다" : "삭제"}
|
||||||
|
disabled={!!item.extidx}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@@ -257,6 +480,15 @@ export function Kuntae() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 추가/수정 모달 */}
|
||||||
|
<KuntaeEditModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onSave={handleSave}
|
||||||
|
initialData={selectedItem}
|
||||||
|
mode={modalMode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -264,12 +496,13 @@ export function Kuntae() {
|
|||||||
// 통계 카드 컴포넌트
|
// 통계 카드 컴포넌트
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
value: number;
|
value: string | number;
|
||||||
|
subValue?: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ title, value, icon, color }: StatCardProps) {
|
function StatCard({ title, value, subValue, icon, color }: StatCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="glass-effect rounded-xl p-4 card-hover">
|
<div className="glass-effect rounded-xl p-4 card-hover">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -278,7 +511,8 @@ function StatCard({ title, value, icon, color }: StatCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<p className="text-sm font-medium text-white/70">{title}</p>
|
<p className="text-sm font-medium text-white/70">{title}</p>
|
||||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
<p className={`text-xl font-bold ${color}`}>{value}</p>
|
||||||
|
{subValue && <p className="text-xs text-white/40 mt-1">{subValue}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,24 +90,47 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 근태 관련 타입
|
// 근태 관련 타입 (Holyday 테이블)
|
||||||
export interface KuntaeModel {
|
export interface KuntaeModel {
|
||||||
idx: number;
|
idx: number;
|
||||||
gcode: string;
|
gcode: string;
|
||||||
|
cate: string; // 구분 (연차, 대체, 휴가 등)
|
||||||
|
sdate: string; // 시작일
|
||||||
|
edate: string; // 종료일
|
||||||
|
term: number; // 사용(일)
|
||||||
|
crtime: number; // 사용(시간)
|
||||||
|
termDr: number; // 발생(일)
|
||||||
|
DrTime: number; // 발생(시간)
|
||||||
|
contents: string; // 내용
|
||||||
|
uid: string; // 사용자 ID
|
||||||
|
UserName?: string; // 사용자 이름 (조인)
|
||||||
|
wuid: string; // 등록자 ID
|
||||||
|
wdate: string; // 등록일
|
||||||
|
extcate?: string; // 외부 소스 카테고리
|
||||||
|
extidx?: number; // 외부 소스 인덱스
|
||||||
|
}
|
||||||
|
|
||||||
|
// 근태 권한 정보 타입
|
||||||
|
export interface HolydayPermission {
|
||||||
|
Success: boolean;
|
||||||
|
CurrentUserId: string;
|
||||||
|
Level: number;
|
||||||
|
CanManage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 근태 사용자 목록 타입
|
||||||
|
export interface HolydayUser {
|
||||||
uid: string;
|
uid: string;
|
||||||
uname: string;
|
UserName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 근태 잔량 정보 타입
|
||||||
|
export interface HolydayBalance {
|
||||||
cate: string;
|
cate: string;
|
||||||
sdate: string | null;
|
TotalGenDays: number;
|
||||||
edate: string | null;
|
TotalGenHours: number;
|
||||||
term: number;
|
TotalUseDays: number;
|
||||||
termdr: number;
|
TotalUseHours: number;
|
||||||
drtime: number;
|
|
||||||
crtime: number;
|
|
||||||
contents: string;
|
|
||||||
tag: string;
|
|
||||||
extcate: string;
|
|
||||||
wuid: string;
|
|
||||||
wdate: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 업무일지 관련 타입 (기존 - 사용 안함)
|
// 업무일지 관련 타입 (기존 - 사용 안함)
|
||||||
@@ -134,6 +157,8 @@ export interface JobReportItem {
|
|||||||
svalue: string; // 업무형태 표시값
|
svalue: string; // 업무형태 표시값
|
||||||
hrs: number;
|
hrs: number;
|
||||||
ot: number;
|
ot: number;
|
||||||
|
otStart?: string; // 초과근무 시작시간
|
||||||
|
otEnd?: string; // 초과근무 종료시간
|
||||||
requestpart: string;
|
requestpart: string;
|
||||||
package: string;
|
package: string;
|
||||||
userprocess: string; // 사용자 공정
|
userprocess: string; // 사용자 공정
|
||||||
@@ -333,8 +358,8 @@ export interface MachineBridgeInterface {
|
|||||||
Jobreport_GetList(sd: string, ed: string, uid: string, cate: string, searchKey: string): Promise<string>;
|
Jobreport_GetList(sd: string, ed: string, uid: string, cate: string, searchKey: string): Promise<string>;
|
||||||
Jobreport_GetUsers(): Promise<string>;
|
Jobreport_GetUsers(): Promise<string>;
|
||||||
Jobreport_GetDetail(id: number): Promise<string>;
|
Jobreport_GetDetail(id: number): Promise<string>;
|
||||||
Jobreport_Add(pdate: string, projectName: string, pidx: number, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string): Promise<string>;
|
Jobreport_Add(pdate: string, projectName: string, pidx: number, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string, otStart: string, otEnd: string): Promise<string>;
|
||||||
Jobreport_Edit(idx: number, pdate: string, projectName: string, pidx: number, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string): Promise<string>;
|
Jobreport_Edit(idx: number, pdate: string, projectName: string, pidx: number, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string, otStart: string, otEnd: string): Promise<string>;
|
||||||
Jobreport_Delete(id: number): Promise<string>;
|
Jobreport_Delete(id: number): Promise<string>;
|
||||||
Jobreport_GetPermission(targetUserId: string): Promise<string>;
|
Jobreport_GetPermission(targetUserId: string): Promise<string>;
|
||||||
Jobreport_GetJobTypes(process: string): Promise<string>;
|
Jobreport_GetJobTypes(process: string): Promise<string>;
|
||||||
@@ -741,3 +766,28 @@ export interface ProjectDailyMemo {
|
|||||||
wdate?: string;
|
wdate?: string;
|
||||||
wname?: string;
|
wname?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 일별 업무일지 집계 타입
|
||||||
|
export interface JobReportDayItem {
|
||||||
|
uid: string;
|
||||||
|
uname: string;
|
||||||
|
pdate: string;
|
||||||
|
hrs: number;
|
||||||
|
ot: number;
|
||||||
|
processs: string;
|
||||||
|
jobtype: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 업무형태별 집계 타입
|
||||||
|
export interface JobReportTypeItem {
|
||||||
|
type: string;
|
||||||
|
hrs: number;
|
||||||
|
ot: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobReportDayData {
|
||||||
|
items: JobReportDayItem[];
|
||||||
|
holidays: HolidayItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
this.cmb_kisuldiv = new System.Windows.Forms.ComboBox();
|
this.cmb_kisuldiv = new System.Windows.Forms.ComboBox();
|
||||||
this.textBox1 = new System.Windows.Forms.TextBox();
|
this.textBox1 = new System.Windows.Forms.TextBox();
|
||||||
this.cmb_kisullv = new System.Windows.Forms.ComboBox();
|
this.cmb_kisullv = new System.Windows.Forms.ComboBox();
|
||||||
this.dateTimePicker2 = new System.Windows.Forms.DateTimePicker();
|
this.dtOtEnd = new System.Windows.Forms.DateTimePicker();
|
||||||
this.dateTimePicker1 = new System.Windows.Forms.DateTimePicker();
|
this.dtOtStart = new System.Windows.Forms.DateTimePicker();
|
||||||
this.lbTitleTip = new System.Windows.Forms.Label();
|
this.lbTitleTip = new System.Windows.Forms.Label();
|
||||||
this.tbTag = new System.Windows.Forms.TextBox();
|
this.tbTag = new System.Windows.Forms.TextBox();
|
||||||
this.ta = new FPJ0000.dsPRJTableAdapters.JobReportTableAdapter();
|
this.ta = new FPJ0000.dsPRJTableAdapters.JobReportTableAdapter();
|
||||||
@@ -201,15 +201,6 @@
|
|||||||
label6.TabIndex = 13;
|
label6.TabIndex = 13;
|
||||||
label6.Text = "프로세스";
|
label6.Text = "프로세스";
|
||||||
//
|
//
|
||||||
// label2
|
|
||||||
//
|
|
||||||
this.label2.AutoSize = true;
|
|
||||||
this.label2.Location = new System.Drawing.Point(160, 163);
|
|
||||||
this.label2.Name = "label2";
|
|
||||||
this.label2.Size = new System.Drawing.Size(53, 12);
|
|
||||||
this.label2.TabIndex = 21;
|
|
||||||
this.label2.Text = "초과시간";
|
|
||||||
//
|
|
||||||
// label7
|
// label7
|
||||||
//
|
//
|
||||||
label7.AutoSize = true;
|
label7.AutoSize = true;
|
||||||
@@ -257,6 +248,15 @@
|
|||||||
label11.TabIndex = 25;
|
label11.TabIndex = 25;
|
||||||
label11.Text = "* 비용은 자동 계산 됩니다";
|
label11.Text = "* 비용은 자동 계산 됩니다";
|
||||||
//
|
//
|
||||||
|
// label2
|
||||||
|
//
|
||||||
|
this.label2.AutoSize = true;
|
||||||
|
this.label2.Location = new System.Drawing.Point(160, 163);
|
||||||
|
this.label2.Name = "label2";
|
||||||
|
this.label2.Size = new System.Drawing.Size(53, 12);
|
||||||
|
this.label2.TabIndex = 21;
|
||||||
|
this.label2.Text = "초과시간";
|
||||||
|
//
|
||||||
// lbSummary
|
// lbSummary
|
||||||
//
|
//
|
||||||
this.lbSummary.AutoSize = true;
|
this.lbSummary.AutoSize = true;
|
||||||
@@ -489,8 +489,8 @@
|
|||||||
this.panel1.Controls.Add(this.chkManagerAlert);
|
this.panel1.Controls.Add(this.chkManagerAlert);
|
||||||
this.panel1.Controls.Add(this.tbProjectIndex);
|
this.panel1.Controls.Add(this.tbProjectIndex);
|
||||||
this.panel1.Controls.Add(this.grpkisul);
|
this.panel1.Controls.Add(this.grpkisul);
|
||||||
this.panel1.Controls.Add(this.dateTimePicker2);
|
this.panel1.Controls.Add(this.dtOtEnd);
|
||||||
this.panel1.Controls.Add(this.dateTimePicker1);
|
this.panel1.Controls.Add(this.dtOtStart);
|
||||||
this.panel1.Controls.Add(label7);
|
this.panel1.Controls.Add(label7);
|
||||||
this.panel1.Controls.Add(this.cmbRequest);
|
this.panel1.Controls.Add(this.cmbRequest);
|
||||||
this.panel1.Controls.Add(label1);
|
this.panel1.Controls.Add(label1);
|
||||||
@@ -624,25 +624,25 @@
|
|||||||
this.cmb_kisullv.Size = new System.Drawing.Size(108, 20);
|
this.cmb_kisullv.Size = new System.Drawing.Size(108, 20);
|
||||||
this.cmb_kisullv.TabIndex = 10;
|
this.cmb_kisullv.TabIndex = 10;
|
||||||
//
|
//
|
||||||
// dateTimePicker2
|
// dtOtEnd
|
||||||
//
|
//
|
||||||
this.dateTimePicker2.CustomFormat = "yyyy-MM-dd hh:mm:ss";
|
this.dtOtEnd.CustomFormat = "yyyy-MM-dd hh:mm:ss";
|
||||||
this.dateTimePicker2.Format = System.Windows.Forms.DateTimePickerFormat.Custom;
|
this.dtOtEnd.Format = System.Windows.Forms.DateTimePickerFormat.Custom;
|
||||||
this.dateTimePicker2.Location = new System.Drawing.Point(460, 165);
|
this.dtOtEnd.Location = new System.Drawing.Point(460, 165);
|
||||||
this.dateTimePicker2.Margin = new System.Windows.Forms.Padding(4);
|
this.dtOtEnd.Margin = new System.Windows.Forms.Padding(4);
|
||||||
this.dateTimePicker2.Name = "dateTimePicker2";
|
this.dtOtEnd.Name = "dtOtEnd";
|
||||||
this.dateTimePicker2.Size = new System.Drawing.Size(153, 21);
|
this.dtOtEnd.Size = new System.Drawing.Size(153, 21);
|
||||||
this.dateTimePicker2.TabIndex = 25;
|
this.dtOtEnd.TabIndex = 25;
|
||||||
//
|
//
|
||||||
// dateTimePicker1
|
// dtOtStart
|
||||||
//
|
//
|
||||||
this.dateTimePicker1.CustomFormat = "yyyy-MM-dd hh:mm:ss";
|
this.dtOtStart.CustomFormat = "yyyy-MM-dd hh:mm:ss";
|
||||||
this.dateTimePicker1.Format = System.Windows.Forms.DateTimePickerFormat.Custom;
|
this.dtOtStart.Format = System.Windows.Forms.DateTimePickerFormat.Custom;
|
||||||
this.dateTimePicker1.Location = new System.Drawing.Point(302, 165);
|
this.dtOtStart.Location = new System.Drawing.Point(302, 165);
|
||||||
this.dateTimePicker1.Margin = new System.Windows.Forms.Padding(4);
|
this.dtOtStart.Margin = new System.Windows.Forms.Padding(4);
|
||||||
this.dateTimePicker1.Name = "dateTimePicker1";
|
this.dtOtStart.Name = "dtOtStart";
|
||||||
this.dateTimePicker1.Size = new System.Drawing.Size(153, 21);
|
this.dtOtStart.Size = new System.Drawing.Size(153, 21);
|
||||||
this.dateTimePicker1.TabIndex = 24;
|
this.dtOtStart.TabIndex = 24;
|
||||||
//
|
//
|
||||||
// lbTitleTip
|
// lbTitleTip
|
||||||
//
|
//
|
||||||
@@ -743,8 +743,8 @@
|
|||||||
private RichTextBoxEx.RichTextBoxEx richTextBoxEx1;
|
private RichTextBoxEx.RichTextBoxEx richTextBoxEx1;
|
||||||
private System.Windows.Forms.Panel panel1;
|
private System.Windows.Forms.Panel panel1;
|
||||||
private System.Windows.Forms.TextBox tbTag;
|
private System.Windows.Forms.TextBox tbTag;
|
||||||
private System.Windows.Forms.DateTimePicker dateTimePicker1;
|
private System.Windows.Forms.DateTimePicker dtOtStart;
|
||||||
private System.Windows.Forms.DateTimePicker dateTimePicker2;
|
private System.Windows.Forms.DateTimePicker dtOtEnd;
|
||||||
private System.Windows.Forms.Label lbTitleTip;
|
private System.Windows.Forms.Label lbTitleTip;
|
||||||
private System.Windows.Forms.GroupBox grpkisul;
|
private System.Windows.Forms.GroupBox grpkisul;
|
||||||
private System.Windows.Forms.ComboBox cmb_kisullv;
|
private System.Windows.Forms.ComboBox cmb_kisullv;
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ namespace FPJ0000.JobReport_
|
|||||||
EnsureVisibleAndUsableSize();
|
EnsureVisibleAndUsableSize();
|
||||||
//사용자목록
|
//사용자목록
|
||||||
this.bs.DataSource = dr;
|
this.bs.DataSource = dr;
|
||||||
this.dateTimePicker1.CustomFormat = "yyyy-MM-dd HH:mm.ss";
|
this.dtOtStart.CustomFormat = "yyyy-MM-dd HH:mm.ss";
|
||||||
this.dateTimePicker2.CustomFormat = "yyyy-MM-dd HH:mm.ss";
|
this.dtOtEnd.CustomFormat = "yyyy-MM-dd HH:mm.ss";
|
||||||
|
|
||||||
//해당 사용자에 걸린 프로젝트 목록 가져오기
|
//해당 사용자에 걸린 프로젝트 목록 가져오기
|
||||||
var userProject = FCOMMON.DBM.getUserProjectList(FCOMMON.info.Login.nameK);
|
var userProject = FCOMMON.DBM.getUserProjectList(FCOMMON.info.Login.nameK);
|
||||||
@@ -239,23 +239,23 @@ namespace FPJ0000.JobReport_
|
|||||||
textBox1.Text = dr.kisulamt.ToString();
|
textBox1.Text = dr.kisulamt.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dr.IsotStartNull()) dateTimePicker1.Value = DateTime.Now;
|
if (dr.IsotStartNull()) dtOtStart.Value = DateTime.Now;
|
||||||
else dateTimePicker1.Value = dr.otStart;
|
else dtOtStart.Value = dr.otStart;
|
||||||
if (dr.IsotEndNull()) dateTimePicker2.Value = DateTime.Now;
|
if (dr.IsotEndNull()) dtOtEnd.Value = DateTime.Now;
|
||||||
else dateTimePicker2.Value = dr.otEnd;
|
else dtOtEnd.Value = dr.otEnd;
|
||||||
|
|
||||||
//if (cmbRequest.Text == "") cmbRequest.Text = "EE1";
|
//if (cmbRequest.Text == "") cmbRequest.Text = "EE1";
|
||||||
if (cmbPackage.Text == "") cmbPackage.Text = "Common";
|
if (cmbPackage.Text == "") cmbPackage.Text = "Common";
|
||||||
|
|
||||||
if (dr.ot != 0)
|
if (dr.ot != 0)
|
||||||
{
|
{
|
||||||
dateTimePicker1.Enabled = true;
|
dtOtStart.Enabled = true;
|
||||||
dateTimePicker2.Enabled = true;
|
dtOtEnd.Enabled = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dateTimePicker1.Enabled = false;
|
dtOtStart.Enabled = false;
|
||||||
dateTimePicker2.Enabled = false;
|
dtOtEnd.Enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//프로젝트 번호 확인(프로젝트번호가 바뀌면 데이터를 업데이트 해준다)
|
//프로젝트 번호 확인(프로젝트번호가 바뀌면 데이터를 업데이트 해준다)
|
||||||
@@ -638,13 +638,13 @@ namespace FPJ0000.JobReport_
|
|||||||
|
|
||||||
if (double.TryParse(tbOt.Text, out double ot))
|
if (double.TryParse(tbOt.Text, out double ot))
|
||||||
{
|
{
|
||||||
var timeterm = dateTimePicker2.Value - dateTimePicker1.Value;
|
var timeterm = dtOtEnd.Value - dtOtStart.Value;
|
||||||
if ((timeterm.TotalMinutes + 1) * 60 <= ot)
|
if ((timeterm.TotalMinutes + 1) * 60 <= ot)
|
||||||
{
|
{
|
||||||
FCOMMON.Util.MsgE($"OT시간이 지정된 시간대보다 부족합니다.\nOT시간정보를 확인하세요\n" +
|
FCOMMON.Util.MsgE($"OT시간이 지정된 시간대보다 부족합니다.\nOT시간정보를 확인하세요\n" +
|
||||||
$"\n입력시간범위 : {dateTimePicker1.Value.ToString("HH:mm:ss")}~{dateTimePicker2.Value.ToString("HH:mm:ss")}" +
|
$"\n입력시간범위 : {dtOtStart.Value.ToString("HH:mm:ss")}~{dtOtEnd.Value.ToString("HH:mm:ss")}" +
|
||||||
$"\n입력시간(시) : {timeterm.TotalMinutes * 60}");
|
$"\n입력시간(시) : {timeterm.TotalMinutes * 60}");
|
||||||
dateTimePicker1.Focus();
|
dtOtStart.Focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,8 +657,8 @@ namespace FPJ0000.JobReport_
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
dr.ot = ot;
|
dr.ot = ot;
|
||||||
dr.otStart = dateTimePicker1.Value;
|
dr.otStart = dtOtStart.Value;
|
||||||
dr.otEnd = dateTimePicker2.Value;
|
dr.otEnd = dtOtEnd.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -950,30 +950,30 @@ namespace FPJ0000.JobReport_
|
|||||||
if (ot != 0)
|
if (ot != 0)
|
||||||
{
|
{
|
||||||
|
|
||||||
dateTimePicker1.Enabled = true;
|
dtOtStart.Enabled = true;
|
||||||
dateTimePicker2.Enabled = true;
|
dtOtEnd.Enabled = true;
|
||||||
|
|
||||||
//신규데이터라면 자동으로 시간을 설정해준다
|
//신규데이터라면 자동으로 시간을 설정해준다
|
||||||
if (dr.RowState == DataRowState.Added || dr.RowState == DataRowState.Detached)
|
if (dr.RowState == DataRowState.Added || dr.RowState == DataRowState.Detached)
|
||||||
{
|
{
|
||||||
if (dateTimePicker1.Value == dateTimePicker2.Value)
|
if (dtOtStart.Value == dtOtEnd.Value)
|
||||||
{
|
{
|
||||||
dateTimePicker1.Value = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd" + " 18:00:00"));
|
dtOtStart.Value = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd" + " 18:00:00"));
|
||||||
dateTimePicker2.Value = dateTimePicker1.Value.AddHours(ot);
|
dtOtEnd.Value = dtOtStart.Value.AddHours(ot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dateTimePicker1.Enabled = false;
|
dtOtStart.Enabled = false;
|
||||||
dateTimePicker2.Enabled = false;
|
dtOtEnd.Enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dateTimePicker1.Enabled = false;
|
dtOtStart.Enabled = false;
|
||||||
dateTimePicker2.Enabled = false;
|
dtOtEnd.Enabled = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user