This commit is contained in:
backuppc
2025-07-14 16:30:03 +09:00
parent 032f6e4c4e
commit 479a736b80
8 changed files with 1201 additions and 75 deletions

46
Project/Dialog/fJobReport.Designer.cs generated Normal file
View File

@@ -0,0 +1,46 @@
namespace Project.Dialog
{
partial class fJobReport
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// fJobReport
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1063, 567);
this.Name = "fJobReport";
this.Text = "업무일지";
this.ResumeLayout(false);
}
#endregion
}
}

View File

@@ -0,0 +1,86 @@
using FCM0000.Mail;
using FCOMMON;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Project.Dialog
{
public partial class fJobReport : fBase
{
private WebView2 webView21;
public fJobReport()
{
InitializeComponent();
InitializeWebView2();
}
private void InitializeWebView2()
{
// 수동으로 WebView2 컨트롤 생성
this.webView21 = new WebView2();
// 기본 속성 설정
this.webView21.CreationProperties = null;
this.webView21.DefaultBackgroundColor = Color.White;
this.webView21.Dock = DockStyle.Fill;
this.webView21.Location = new Point(0, 0);
this.webView21.Name = "webView21";
this.webView21.Size = new Size(800, 600);
this.webView21.TabIndex = 0;
this.webView21.ZoomFactor = 1D;
// 폼에 추가
this.Controls.Add(this.webView21);
// 비동기 초기화
InitializeAsync();
}
private async void InitializeAsync()
{
try
{
// Fixed Version 경로 설정
string runtimePath = Path.Combine(Application.StartupPath, "WebView2Runtime");
if (Directory.Exists(runtimePath))
{
var env = await CoreWebView2Environment.CreateAsync(runtimePath);
await this.webView21.EnsureCoreWebView2Async(env);
}
else
{
// 시스템에 설치된 WebView2 사용
await this.webView21.EnsureCoreWebView2Async();
}
// OWIN 서버의 DashBoard 페이지로 연결
webView21.Source = new Uri($"{Pub.setting.WebServiceURL}/Jobreport");
}
catch (Exception ex)
{
MessageBox.Show($"WebView2 초기화 실패: {ex.Message}");
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
EnsureVisibleAndUsableSize();
}
private void label1_Click(object sender, EventArgs e)
{
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -259,6 +259,12 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Dialog\fJobReport.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialog\fJobReport.Designer.cs">
<DependentUpon>fJobReport.cs</DependentUpon>
</Compile>
<Compile Include="Dialog\fLogin_WB.cs"> <Compile Include="Dialog\fLogin_WB.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@@ -469,6 +475,9 @@
<EmbeddedResource Include="Dev\fDisableItem.resx"> <EmbeddedResource Include="Dev\fDisableItem.resx">
<DependentUpon>fDisableItem.cs</DependentUpon> <DependentUpon>fDisableItem.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Dialog\fJobReport.resx">
<DependentUpon>fJobReport.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialog\fDashboard.resx"> <EmbeddedResource Include="Dialog\fDashboard.resx">
<DependentUpon>fDashboard.cs</DependentUpon> <DependentUpon>fDashboard.cs</DependentUpon>
</EmbeddedResource> </EmbeddedResource>
@@ -645,6 +654,9 @@
<Content Include="Web\wwwroot\DashBoard\index.html"> <Content Include="Web\wwwroot\DashBoard\index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Web\wwwroot\Jobreport\index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Web\wwwroot\login.html"> <Content Include="Web\wwwroot\login.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>

View File

@@ -125,6 +125,60 @@ namespace Project.Web.Controllers
return resp; return resp;
} }
[HttpGet]
public HttpResponseMessage GetJobData(string startDate = "", string endDate = "")
{
var sql = string.Empty;
// 기본값 설정 (이번 달)
if (string.IsNullOrEmpty(startDate) || string.IsNullOrEmpty(endDate))
{
var now = DateTime.Now;
var firstDayOfMonth = new DateTime(now.Year, now.Month, 1);
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
startDate = firstDayOfMonth.ToString("yyyy-MM-dd");
endDate = lastDayOfMonth.ToString("yyyy-MM-dd");
}
sql = $" select idx,pdate,status,projectName, uid, requestpart, package,type,process,description," +
" hrs,ot,otStart,otEnd" +
" from JobReport" +
" where gcode = @gcode and uid = @uid" +
" and pdate between @startDate and @endDate" +
" order by pdate desc, wdate desc";
var cs = Properties.Settings.Default.gwcs;
var cn = new System.Data.SqlClient.SqlConnection(cs);
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
cmd.Parameters.AddWithValue("uid", FCOMMON.info.Login.no);
cmd.Parameters.AddWithValue("startDate", startDate);
cmd.Parameters.AddWithValue("endDate", endDate);
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
var dt = new System.Data.DataTable();
da.Fill(dt);
da.Dispose();
cmd.Dispose();
cn.Dispose();
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
var resp = new HttpResponseMessage()
{
Content = new StringContent(
txtjson,
System.Text.Encoding.UTF8,
"application/json")
};
return resp;
}
[HttpGet] [HttpGet]
public HttpResponseMessage GetCurrentUserCount() public HttpResponseMessage GetCurrentUserCount()

View File

@@ -288,93 +288,32 @@ namespace Project.Web.Controllers
[HttpGet] [HttpGet]
public HttpResponseMessage Index() public HttpResponseMessage Index()
{ {
//로그인이 되어있지않다면 로그인을 가져온다 // 직접 파일을 읽어서 반환
MethodResult result; var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "Jobreport", "index.html");
result = View(); var contents = string.Empty;
if (System.IO.File.Exists(filePath))
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
var model = GetGlobalModel();
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
//기본값을 찾아서 없애줘야한다
var searchkey = string.Empty;
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
var tbody = new System.Text.StringBuilder();
//테이블데이터생성
var itemcnt = 0;
//if (searchkey.isEmpty() == false)
{ {
var db = new dsMSSQLTableAdapters.vJobReportForUserTableAdapter();// EEEntitiesJobreport(); contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
var sd = DateTime.Now.ToShortDateString(); }
var ed = DateTime.Now.ToShortDateString();
var rows = db.GetByDate(FCOMMON.info.Login.gcode, FCOMMON.info.Login.no, sd, ed);
//.vJobReportForUser.AsNoTracking().Where(t => t.gcode == FCOMMON.info.Login.gcode && t.id == FCOMMON.info.Login.no && t.pdate.CompareTo(sd) >= 0 && t.pdate.CompareTo(ed) <= 1).OrderByDescending(t => t.pdate);
itemcnt = rows.Count();
foreach (var item in rows)
{
tbody.AppendLine("<tr>");
tbody.AppendLine($"<th scope='row'>{item.pdate.Substring(5)}</th>");
tbody.AppendLine($"<td>{item.ww}</td>");
tbody.AppendLine($"<td>{item.name}</td>");
if (item.status == "진행 중" || item.status.EndsWith("%"))
tbody.AppendLine($"<td class='table-info text-center'>{item.status}</td>");
else else
tbody.AppendLine($"<td class='text-center'>{item.status}</td>");
tbody.AppendLine($"<td>{item.type}</td>");
tbody.AppendLine($"<td><a href='/jobreport/edit/{item.idx}'>{item.projectName}</a></td>");
tbody.AppendLine($"<td>{item.hrs}</td>");
tbody.AppendLine($"<td>{item.ot}</td>");
tbody.AppendLine("<td><span class='d-inline-block text-truncate' style='max-width: 150px;'>");
tbody.AppendLine(item.description);
tbody.AppendLine("</span></td>");
tbody.AppendLine("</tr>");
}
}
//아잍쳄이 없는경우
if (itemcnt == 0)
{ {
tbody.AppendLine("<tr>"); // 파일이 없으면 404 에러 페이지 또는 기본 메시지
tbody.AppendLine("<th scope='row'>1</th>"); contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
tbody.AppendLine("</tr>");
} }
var contents = result.Content.Replace("{search}", searchkey);
contents = contents.Replace("{tabledata}", tbody.ToString());
contents = contents.Replace("{cnt}", itemcnt.ToString());
//공용값 적용 //공용값 적용
ApplyCommonValue(ref contents); ApplyCommonValue(ref contents);
//최종문자 적용
result.Content = contents;
var resp = new HttpResponseMessage() var resp = new HttpResponseMessage()
{ {
Content = new StringContent( Content = new StringContent(
result.Content, contents,
System.Text.Encoding.UTF8, System.Text.Encoding.UTF8,
"text/html") "text/html")
}; };
return resp; return resp;
} }
} }
} }

View File

@@ -0,0 +1,863 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>업무일지</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#6B7280',
success: '#10B981',
danger: '#EF4444',
warning: '#F59E0B'
}
}
}
}
</script>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 헤더 -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div class="flex items-center">
<i data-feather="file-text" class="w-8 h-8 text-primary mr-3"></i>
<h1 class="text-2xl font-bold text-gray-900">업무일지</h1>
</div>
<div class="flex items-center space-x-4">
<button id="refreshBtn" class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center">
<i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
새로고침
</button>
<div class="text-sm text-gray-600">
<span id="currentDate"></span>
</div>
</div>
</div>
</div>
</header>
<!-- 메인 컨텐츠 -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 통계 카드 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-blue-100 rounded-lg">
<i data-feather="calendar" class="w-6 h-6 text-primary"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">총 업무일수</p>
<p id="totalDays" class="text-2xl font-bold text-gray-900">0</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-green-100 rounded-lg">
<i data-feather="clock" class="w-6 h-6 text-success"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">총 근무시간</p>
<p id="totalHours" class="text-2xl font-bold text-gray-900">0h</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-orange-100 rounded-lg">
<i data-feather="zap" class="w-6 h-6 text-warning"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">총 초과근무</p>
<p id="totalOT" class="text-2xl font-bold text-gray-900">0h</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center">
<div class="p-2 bg-purple-100 rounded-lg">
<i data-feather="folder" class="w-6 h-6 text-purple-600"></i>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600">진행중 프로젝트</p>
<p id="activeProjects" class="text-2xl font-bold text-gray-900">0</p>
</div>
</div>
</div>
</div>
<!-- 필터 및 검색 -->
<div class="bg-white rounded-lg shadow mb-6">
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">조회기간</label>
<div class="flex space-x-2">
<input type="date" id="startDate" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
<span class="flex items-center text-gray-500">~</span>
<input type="date" id="endDate" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
<select id="statusFilter" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="">전체</option>
<option value="진행중">진행중</option>
<option value="완료">완료</option>
<option value="대기">대기</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
<select id="projectFilter" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="">전체</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">검색</label>
<input type="text" id="searchInput" placeholder="업무 내용 검색..." class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
</div>
<div class="flex space-x-2">
<button id="clearFilterBtn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md flex items-center">
<i data-feather="x" class="w-4 h-4 mr-2"></i>
필터 초기화
</button>
<button id="exportBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md flex items-center">
<i data-feather="download" class="w-4 h-4 mr-2"></i>
엑셀 다운로드
</button>
</div>
</div>
</div>
</div>
<!-- 데이터 테이블 -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="pdate">
날짜 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="status">
상태 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="projectName">
프로젝트명 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="requestpart">
요청부서 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="package">
패키지 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="type">
타입 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="process">
프로세스 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">업무내용</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="hrs">
근무시간 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="ot">
초과근무 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">초과근무 시간</th>
</tr>
</thead>
<tbody id="jobTableBody" class="bg-white divide-y divide-gray-200">
<!-- 데이터가 여기에 동적으로 로드됩니다 -->
</tbody>
</table>
</div>
<!-- 로딩 상태 -->
<div id="loadingState" class="hidden p-8 text-center">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-gray-600">데이터를 불러오는 중...</span>
</div>
</div>
<!-- 빈 상태 -->
<div id="emptyState" class="hidden p-8 text-center">
<i data-feather="inbox" class="w-12 h-12 text-gray-400 mx-auto mb-4"></i>
<p class="text-gray-500">업무일지 데이터가 없습니다.</p>
</div>
</div>
<!-- 페이지네이션 -->
<div id="pagination" class="mt-6 flex items-center justify-between">
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-700">페이지당 행 수:</span>
<select id="pageSize" class="border border-gray-300 rounded-md px-2 py-1 text-sm">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="flex items-center space-x-2">
<button id="prevPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
이전
</button>
<span id="pageInfo" class="text-sm text-gray-700">1 / 1</span>
<button id="nextPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
다음
</button>
</div>
</div>
</main>
<!-- 상세 모달 -->
<div id="detailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">업무 상세정보</h3>
<button id="closeModal" class="text-gray-400 hover:text-gray-600">
<i data-feather="x" class="w-6 h-6"></i>
</button>
</div>
<div id="modalContent" class="space-y-4">
<!-- 모달 내용이 여기에 동적으로 로드됩니다 -->
</div>
</div>
</div>
</div>
<script>
// 전역 변수
let jobData = [];
let filteredData = [];
let currentPage = 1;
let pageSize = 25;
let sortColumn = 'pdate';
let sortDirection = 'desc';
// 초기화
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
loadJobData();
});
function initializeApp() {
// 현재 날짜 표시
const now = new Date();
document.getElementById('currentDate').textContent = now.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
// 조회기간 기본값 설정 (이번 달)
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
document.getElementById('startDate').value = currentMonth.toISOString().split('T')[0];
document.getElementById('endDate').value = lastDayOfMonth.toISOString().split('T')[0];
// 이벤트 리스너 등록
document.getElementById('refreshBtn').addEventListener('click', loadJobData);
document.getElementById('startDate').addEventListener('change', loadJobData);
document.getElementById('endDate').addEventListener('change', loadJobData);
document.getElementById('statusFilter').addEventListener('change', filterData);
document.getElementById('projectFilter').addEventListener('change', filterData);
document.getElementById('searchInput').addEventListener('input', filterData);
document.getElementById('clearFilterBtn').addEventListener('click', clearFilters);
document.getElementById('pageSize').addEventListener('change', function() {
pageSize = parseInt(this.value);
currentPage = 1;
renderTable();
});
document.getElementById('prevPage').addEventListener('click', function() {
if (currentPage > 1) {
currentPage--;
renderTable();
}
});
document.getElementById('nextPage').addEventListener('click', function() {
const maxPage = Math.ceil(filteredData.length / pageSize);
if (currentPage < maxPage) {
currentPage++;
renderTable();
}
});
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
document.getElementById('closeModal').addEventListener('click', closeModal);
// 정렬 이벤트 리스너
document.querySelectorAll('[data-sort]').forEach(th => {
th.addEventListener('click', function() {
const column = this.getAttribute('data-sort');
if (sortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = column;
sortDirection = 'asc';
}
sortData();
renderTable();
});
});
// 모달 외부 클릭 시 닫기
document.getElementById('detailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Feather 아이콘 초기화
feather.replace();
}
async function loadJobData() {
showLoading(true);
try {
// 조회기간 파라미터 가져오기
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
// API URL 구성
let url = '/DashBoard/GetJobData';
const params = new URLSearchParams();
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
if (params.toString()) {
url += '?' + params.toString();
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('데이터를 불러오는데 실패했습니다.');
}
jobData = await response.json();
filteredData = [...jobData];
updateStatistics();
updateProjectFilter();
sortData();
renderTable();
// 확장된 행들 닫기
closeAllExpandedRows();
} catch (error) {
console.error('Error loading job data:', error);
showError('데이터를 불러오는데 실패했습니다: ' + error.message);
} finally {
showLoading(false);
}
}
function updateStatistics() {
const totalDays = new Set(jobData.map(item => item.pdate)).size;
const totalHours = jobData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0);
const totalOT = jobData.reduce((sum, item) => sum + (parseFloat(item.ot) || 0), 0);
const activeProjects = new Set(jobData.filter(item => item.status === '진행중').map(item => item.projectName)).size;
document.getElementById('totalDays').textContent = totalDays;
document.getElementById('totalHours').textContent = totalHours.toFixed(1) + 'h';
document.getElementById('totalOT').textContent = totalOT.toFixed(1) + 'h';
document.getElementById('activeProjects').textContent = activeProjects;
}
function updateProjectFilter() {
const projectSelect = document.getElementById('projectFilter');
const projects = [...new Set(jobData.map(item => item.projectName).filter(Boolean))];
// 기존 옵션 제거 (전체 옵션 제외)
while (projectSelect.children.length > 1) {
projectSelect.removeChild(projectSelect.lastChild);
}
// 새 옵션 추가
projects.forEach(project => {
const option = document.createElement('option');
option.value = project;
option.textContent = project;
projectSelect.appendChild(option);
});
}
function filterData() {
const statusFilter = document.getElementById('statusFilter').value;
const projectFilter = document.getElementById('projectFilter').value;
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
filteredData = jobData.filter(item => {
const statusMatch = !statusFilter || item.status === statusFilter;
const projectMatch = !projectFilter || item.projectName === projectFilter;
const searchMatch = !searchTerm ||
(item.description && item.description.toLowerCase().includes(searchTerm)) ||
(item.projectName && item.projectName.toLowerCase().includes(searchTerm)) ||
(item.requestpart && item.requestpart.toLowerCase().includes(searchTerm));
return statusMatch && projectMatch && searchMatch;
});
currentPage = 1;
sortData();
renderTable();
}
function sortData() {
filteredData.sort((a, b) => {
let aVal = a[sortColumn];
let bVal = b[sortColumn];
// 날짜 정렬
if (sortColumn === 'pdate') {
aVal = new Date(aVal);
bVal = new Date(bVal);
}
// 숫자 정렬
else if (['hrs', 'ot'].includes(sortColumn)) {
aVal = parseFloat(aVal) || 0;
bVal = parseFloat(bVal) || 0;
}
// 문자열 정렬
else {
aVal = (aVal || '').toString().toLowerCase();
bVal = (bVal || '').toString().toLowerCase();
}
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
}
function renderTable() {
const tbody = document.getElementById('jobTableBody');
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const pageData = filteredData.slice(startIndex, endIndex);
if (pageData.length === 0) {
showEmptyState();
return;
}
hideEmptyState();
tbody.innerHTML = '';
pageData.forEach((item, index) => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 cursor-pointer';
row.setAttribute('data-item-id', item.idx || index);
row.addEventListener('click', () => toggleRowDetail(item, row));
const statusColor = getStatusColor(item.status);
const otTime = item.otStart && item.otEnd ?
`${item.otStart} ~ ${item.otEnd}` : '-';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<div class="flex items-center">
<i data-feather="chevron-right" class="w-4 h-4 mr-2 text-gray-400 expand-icon"></i>
${formatDate(item.pdate)}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
${item.status || '-'}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.projectName || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.requestpart || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.package || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.type || '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.process || '-'}
</td>
<td class="px-6 py-4 text-sm text-gray-900">
<div class="max-w-xs truncate cursor-pointer hover:text-primary hover:underline"
title="${item.description || ''}"
onclick="event.stopPropagation(); showDetailModal(${JSON.stringify(item).replace(/"/g, '&quot;')})">
${item.description || '-'}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${otTime}
</td>
`;
tbody.appendChild(row);
// 확장 행 추가 (숨겨진 상태)
const expandRow = document.createElement('tr');
expandRow.className = 'hidden expand-row';
expandRow.setAttribute('data-parent-id', item.idx || index);
expandRow.innerHTML = `
<td colspan="11" class="px-6 py-4 bg-gray-50 border-t border-gray-200">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">업무내용</label>
<div class="bg-white rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors border"
onclick="showDetailModal(${JSON.stringify(item).replace(/"/g, '&quot;')})">
<p class="text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p>
<div class="mt-2 text-xs text-gray-500 flex items-center">
<i data-feather="maximize-2" class="w-4 h-4 mr-1"></i>
클릭하여 전체 내용 보기
</div>
</div>
</div>
</td>
`;
tbody.appendChild(expandRow);
});
updatePagination();
feather.replace();
}
function getStatusColor(status) {
switch (status) {
case '진행 중': return 'bg-blue-100 text-blue-800';
case '진행 완료': return 'bg-green-100 text-green-800';
case '대기': return 'bg-yellow-100 text-yellow-800';
default: return 'bg-gray-100 text-gray-800';
}
}
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR');
}
function updatePagination() {
const maxPage = Math.ceil(filteredData.length / pageSize);
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, filteredData.length);
document.getElementById('pageInfo').textContent = `${currentPage} / ${maxPage}`;
document.getElementById('prevPage').disabled = currentPage <= 1;
document.getElementById('nextPage').disabled = currentPage >= maxPage;
}
function showDetailModal(item) {
// 문자열로 전달된 경우 JSON 파싱
if (typeof item === 'string') {
try {
item = JSON.parse(item);
} catch (e) {
console.error('Error parsing item:', e);
return;
}
}
const modal = document.getElementById('detailModal');
const content = document.getElementById('modalContent');
content.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">날짜</label>
<p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">상태</label>
<p class="mt-1">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}">
${item.status || '-'}
</span>
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">프로젝트명</label>
<p class="mt-1 text-sm text-gray-900">${item.projectName || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">요청부서</label>
<p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">패키지</label>
<p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">타입</label>
<p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">프로세스</label>
<p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">근무시간</label>
<p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">초과근무</label>
<p class="mt-1 text-sm text-gray-900">${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">초과근무 시간</label>
<p class="mt-1 text-sm text-gray-900">${item.otStart && item.otEnd ? `${item.otStart} ~ ${item.otEnd}` : '-'}</p>
</div>
</div>
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700">업무내용</label>
<p class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p>
</div>
`;
modal.classList.remove('hidden');
feather.replace();
}
function closeModal() {
document.getElementById('detailModal').classList.add('hidden');
}
function showSelectedJobDetail(item) {
const detailContainer = document.getElementById('selectedJobDetail');
const contentContainer = document.getElementById('selectedJobContent');
// 기존 선택된 행의 스타일 제거
document.querySelectorAll('#jobTableBody tr').forEach(row => {
row.classList.remove('bg-blue-50', 'border-l-4', 'border-blue-500');
});
// 현재 행에 선택 스타일 적용
const currentRow = event.target.closest('tr');
if (currentRow) {
currentRow.classList.add('bg-blue-50', 'border-l-4', 'border-blue-500');
}
contentContainer.innerHTML = `
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700">날짜</label>
<p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">상태</label>
<p class="mt-1">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}">
${item.status || '-'}
</span>
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">프로젝트명</label>
<p class="mt-1 text-sm text-gray-900">${item.projectName || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">요청부서</label>
<p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">패키지</label>
<p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">타입</label>
<p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">프로세스</label>
<p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">근무시간</label>
<p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">초과근무</label>
<p class="mt-1 text-sm text-gray-900">${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">초과근무 시간</label>
<p class="mt-1 text-sm text-gray-900">${item.otStart && item.otEnd ? `${item.otStart} ~ ${item.otEnd}` : '-'}</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">업무내용</label>
<div class="bg-gray-50 rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors"
onclick="showDetailModal(${JSON.stringify(item).replace(/"/g, '&quot;')})">
<p class="text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p>
<div class="mt-2 text-xs text-gray-500 flex items-center">
<i data-feather="maximize-2" class="w-4 h-4 mr-1"></i>
클릭하여 전체 내용 보기
</div>
</div>
</div>
`;
detailContainer.classList.remove('hidden');
feather.replace();
// 부드러운 스크롤로 상세 내용으로 이동
detailContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function toggleRowDetail(item, row) {
const itemId = item.idx || row.getAttribute('data-item-id');
const expandRow = document.querySelector(`tr[data-parent-id="${itemId}"]`);
const expandIcon = row.querySelector('.expand-icon');
if (expandRow.classList.contains('hidden')) {
// 확장
expandRow.classList.remove('hidden');
expandIcon.setAttribute('data-feather', 'chevron-down');
row.classList.add('bg-blue-50');
} else {
// 축소
expandRow.classList.add('hidden');
expandIcon.setAttribute('data-feather', 'chevron-right');
row.classList.remove('bg-blue-50');
}
// 아이콘 업데이트
feather.replace();
}
function closeAllExpandedRows() {
document.querySelectorAll('.expand-row').forEach(row => {
row.classList.add('hidden');
});
document.querySelectorAll('#jobTableBody tr').forEach(row => {
if (!row.classList.contains('expand-row')) {
row.classList.remove('bg-blue-50');
const icon = row.querySelector('.expand-icon');
if (icon) {
icon.setAttribute('data-feather', 'chevron-right');
}
}
});
feather.replace();
}
function showLoading(show) {
const loadingState = document.getElementById('loadingState');
const tableBody = document.getElementById('jobTableBody');
if (show) {
loadingState.classList.remove('hidden');
tableBody.style.display = 'none';
} else {
loadingState.classList.add('hidden');
tableBody.style.display = 'table-row-group';
}
}
function showEmptyState() {
document.getElementById('emptyState').classList.remove('hidden');
document.getElementById('jobTableBody').style.display = 'none';
}
function hideEmptyState() {
document.getElementById('emptyState').classList.add('hidden');
document.getElementById('jobTableBody').style.display = 'table-row-group';
}
function showError(message) {
// 간단한 에러 알림 (실제 구현에서는 더 나은 알림 시스템 사용)
alert(message);
}
function clearFilters() {
// 조회기간을 이번 달로 초기화
const now = new Date();
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
document.getElementById('startDate').value = currentMonth.toISOString().split('T')[0];
document.getElementById('endDate').value = lastDayOfMonth.toISOString().split('T')[0];
// 다른 필터들 초기화
document.getElementById('statusFilter').value = '';
document.getElementById('projectFilter').value = '';
document.getElementById('searchInput').value = '';
// 서버에서 새로운 데이터 가져오기
loadJobData();
// 확장된 행들 닫기
closeAllExpandedRows();
}
function exportToExcel() {
if (filteredData.length === 0) {
alert('내보낼 데이터가 없습니다.');
return;
}
// 조회기간 정보 가져오기
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const periodText = startDate && endDate ? `_${startDate}_${endDate}` : '';
// CSV 형식으로 데이터 변환
const headers = ['날짜', '상태', '프로젝트명', '요청부서', '패키지', '타입', '프로세스', '업무내용', '근무시간', '초과근무', '초과근무시작', '초과근무종료'];
const csvContent = [
headers.join(','),
...filteredData.map(item => [
formatDate(item.pdate),
item.status || '',
item.projectName || '',
item.requestpart || '',
item.package || '',
item.type || '',
item.process || '',
`"${(item.description || '').replace(/"/g, '""')}"`,
item.hrs || '',
item.ot || '',
item.otStart || '',
item.otEnd || ''
].join(','))
].join('\n');
// 파일 다운로드
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `업무일지${periodText}_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
</body>
</html>

View File

@@ -374,7 +374,13 @@ namespace Project
{ {
string formkey = "WORKBOOK"; string formkey = "WORKBOOK";
if (!ShowForm(formkey)) if (!ShowForm(formkey))
{
if (this.webok && Pub.InitWebView == 1 && System.Diagnostics.Debugger.IsAttached)
AddForm(formkey, new Dialog.fJobReport());
else
AddForm(formkey, new FPJ0000.fJobReport()); AddForm(formkey, new FPJ0000.fJobReport());
}
} }
void menu_save_cost() void menu_save_cost()