From 32459b4a1d02db1f9313bc89da293499fe09154d Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Fri, 26 Apr 2024 14:21:59 +0800 Subject: [PATCH 01/22] add test data --- .../Report/ReportSearchBox/SearchBox.tsx | 11 +++++++++++ .../Report/ReportSearchBox2/SearchBox2.tsx | 11 +++++++++++ .../Report/ReportSearchBox3/SearchBox3.tsx | 11 +++++++++++ .../Report/ReportSearchBox4/SearchBox4.tsx | 11 +++++++++++ .../Report/ReportSearchBox5/SearchBox5.tsx | 11 +++++++++++ .../Report/ReportSearchBox6/SearchBox6.tsx | 14 ++++++++++++++ 6 files changed, 69 insertions(+) diff --git a/src/components/Report/ReportSearchBox/SearchBox.tsx b/src/components/Report/ReportSearchBox/SearchBox.tsx index 83e3aab..26a5016 100644 --- a/src/components/Report/ReportSearchBox/SearchBox.tsx +++ b/src/components/Report/ReportSearchBox/SearchBox.tsx @@ -192,6 +192,17 @@ function SearchBox({ } } + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + // Find the last row of the first table + let lastRowOfFirstTable = 6; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD const filename = `AR01_Late_Start_Report_${today}.xlsx`; // Append formatted date to the filename diff --git a/src/components/Report/ReportSearchBox2/SearchBox2.tsx b/src/components/Report/ReportSearchBox2/SearchBox2.tsx index d9436a4..39364dd 100644 --- a/src/components/Report/ReportSearchBox2/SearchBox2.tsx +++ b/src/components/Report/ReportSearchBox2/SearchBox2.tsx @@ -192,6 +192,17 @@ function SearchBox({ } } + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + // Find the last row of the first table + let lastRowOfFirstTable = 6; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD const filename = `AR02_Delay_Report_${today}.xlsx`; // Append formatted date to the filename diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx index 5fe1867..bd524b2 100644 --- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx +++ b/src/components/Report/ReportSearchBox3/SearchBox3.tsx @@ -192,6 +192,17 @@ function SearchBox({ } } + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + // Find the last row of the first table + let lastRowOfFirstTable = 6; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD const filename = `AR03_Resource_Overconsumption_${today}.xlsx`; // Append formatted date to the filename diff --git a/src/components/Report/ReportSearchBox4/SearchBox4.tsx b/src/components/Report/ReportSearchBox4/SearchBox4.tsx index a0df1a3..2f57320 100644 --- a/src/components/Report/ReportSearchBox4/SearchBox4.tsx +++ b/src/components/Report/ReportSearchBox4/SearchBox4.tsx @@ -192,6 +192,17 @@ function SearchBox({ } } + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + // Find the last row of the first table + let lastRowOfFirstTable = 6; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD const filename = `AR04_Cost_and_Expense_Report_${today}.xlsx`; // Append formatted date to the filename diff --git a/src/components/Report/ReportSearchBox5/SearchBox5.tsx b/src/components/Report/ReportSearchBox5/SearchBox5.tsx index b252103..207e5ff 100644 --- a/src/components/Report/ReportSearchBox5/SearchBox5.tsx +++ b/src/components/Report/ReportSearchBox5/SearchBox5.tsx @@ -192,6 +192,17 @@ function SearchBox({ } } + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + // Find the last row of the first table + let lastRowOfFirstTable = 5; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD const filename = `AR05_Project_Completion_Report_${today}.xlsx`; // Append formatted date to the filename diff --git a/src/components/Report/ReportSearchBox6/SearchBox6.tsx b/src/components/Report/ReportSearchBox6/SearchBox6.tsx index 1f201b1..82d5df4 100644 --- a/src/components/Report/ReportSearchBox6/SearchBox6.tsx +++ b/src/components/Report/ReportSearchBox6/SearchBox6.tsx @@ -192,6 +192,20 @@ function SearchBox({ } } + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + // Find the last row of the first table + let lastRowOfFirstTable = 5; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + + // Insert the first data form into the worksheet at the desired location + XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD const filename = `AR06_Project_Completion_Report_with_Outstanding_Un-billed_Hours_${today}.xlsx`; // Append formatted date to the filename From 0e1221360e51671b078576530cae4cd2a4400e92 Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Fri, 26 Apr 2024 14:25:11 +0800 Subject: [PATCH 02/22] add new report path --- src/components/NavigationContent/NavigationContent.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 53dbb5c..a6f5596 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -105,6 +105,8 @@ const navigationItems: NavigationItem[] = [ {icon: , label:"Cost and Expense Report", path: "/analytics/CostandExpenseReport"}, {icon: , label:"Completion Report", path: "/analytics/ProjectCompletionReport"}, {icon: , label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, + {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, + {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, ], }, { From e991ff8fd7a9512b31e4c9cca12579c179d45a09 Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Fri, 26 Apr 2024 15:34:25 +0800 Subject: [PATCH 03/22] report style update --- .../Report/ReportSearchBox/SearchBox.tsx | 16 ++++++++-------- .../Report/ReportSearchBox2/SearchBox2.tsx | 16 ++++++++-------- .../Report/ReportSearchBox3/SearchBox3.tsx | 16 ++++++++-------- .../Report/ReportSearchBox4/SearchBox4.tsx | 16 ++++++++-------- .../Report/ReportSearchBox5/SearchBox5.tsx | 16 ++++++++-------- .../Report/ReportSearchBox6/SearchBox6.tsx | 16 ++++++++-------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/components/Report/ReportSearchBox/SearchBox.tsx b/src/components/Report/ReportSearchBox/SearchBox.tsx index 26a5016..67af5f2 100644 --- a/src/components/Report/ReportSearchBox/SearchBox.tsx +++ b/src/components/Report/ReportSearchBox/SearchBox.tsx @@ -151,14 +151,6 @@ function SearchBox({ }); }); - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -202,6 +194,14 @@ function SearchBox({ while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { lastRowOfFirstTable++; } + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD diff --git a/src/components/Report/ReportSearchBox2/SearchBox2.tsx b/src/components/Report/ReportSearchBox2/SearchBox2.tsx index 39364dd..7e3d6e9 100644 --- a/src/components/Report/ReportSearchBox2/SearchBox2.tsx +++ b/src/components/Report/ReportSearchBox2/SearchBox2.tsx @@ -151,14 +151,6 @@ function SearchBox({ }); }); - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -202,6 +194,14 @@ function SearchBox({ while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { lastRowOfFirstTable++; } + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx index bd524b2..b4a252d 100644 --- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx +++ b/src/components/Report/ReportSearchBox3/SearchBox3.tsx @@ -151,14 +151,6 @@ function SearchBox({ }); }); - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -202,6 +194,14 @@ function SearchBox({ while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { lastRowOfFirstTable++; } + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD diff --git a/src/components/Report/ReportSearchBox4/SearchBox4.tsx b/src/components/Report/ReportSearchBox4/SearchBox4.tsx index 2f57320..1ac6e7e 100644 --- a/src/components/Report/ReportSearchBox4/SearchBox4.tsx +++ b/src/components/Report/ReportSearchBox4/SearchBox4.tsx @@ -151,14 +151,6 @@ function SearchBox({ }); }); - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -202,6 +194,14 @@ function SearchBox({ while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { lastRowOfFirstTable++; } + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD diff --git a/src/components/Report/ReportSearchBox5/SearchBox5.tsx b/src/components/Report/ReportSearchBox5/SearchBox5.tsx index 207e5ff..2b0b7db 100644 --- a/src/components/Report/ReportSearchBox5/SearchBox5.tsx +++ b/src/components/Report/ReportSearchBox5/SearchBox5.tsx @@ -151,14 +151,6 @@ function SearchBox({ }); }); - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -202,6 +194,14 @@ function SearchBox({ while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { lastRowOfFirstTable++; } + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD diff --git a/src/components/Report/ReportSearchBox6/SearchBox6.tsx b/src/components/Report/ReportSearchBox6/SearchBox6.tsx index 82d5df4..4e7c82d 100644 --- a/src/components/Report/ReportSearchBox6/SearchBox6.tsx +++ b/src/components/Report/ReportSearchBox6/SearchBox6.tsx @@ -151,14 +151,6 @@ function SearchBox({ }); }); - // Apply calculated widths to each column, skipping column A - worksheet['!cols'] = colWidths.map((width, index) => { - if (index === 0) { - return { wch: 8 }; // Set default or specific width for column A if needed - } - return { wch: width + 2 }; // Add padding to width - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -205,6 +197,14 @@ function SearchBox({ // Insert the first data form into the worksheet at the desired location XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); // Format filename with date const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD From 18e63d7d275922e11f004ccda08823ddb9adc9cf Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Fri, 26 Apr 2024 15:47:58 +0800 Subject: [PATCH 04/22] report style update --- public/temp/AR04_Cost and Expense Report.xlsx | Bin 12817 -> 12852 bytes .../temp/AR05_Project Completion Report.xlsx | Bin 12445 -> 12469 bytes ...port with Outstanding Un-billed Hours.xlsx | Bin 12670 -> 12694 bytes .../Report/ReportSearchBox/SearchBox.tsx | 22 ++++++++-------- .../Report/ReportSearchBox2/SearchBox2.tsx | 22 ++++++++-------- .../Report/ReportSearchBox3/SearchBox3.tsx | 24 +++++++++--------- .../Report/ReportSearchBox4/SearchBox4.tsx | 22 ++++++++-------- .../Report/ReportSearchBox5/SearchBox5.tsx | 24 +++++++++--------- .../Report/ReportSearchBox6/SearchBox6.tsx | 22 ++++++++-------- 9 files changed, 68 insertions(+), 68 deletions(-) diff --git a/public/temp/AR04_Cost and Expense Report.xlsx b/public/temp/AR04_Cost and Expense Report.xlsx index a414e1afcda8192ce80e8ffcf809f49378b80ceb..3ce7f5955778e1d59746c115ddac596386b13d8d 100644 GIT binary patch delta 4021 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 delta 3965 zcmY+HXEYlO)W;LVrl?taZ&7=+NTX;pHnlfJjM}Lc)Tljz7O^WzRc*ERh#Iw?nnls7 zU9%eD?fZOqpYz^N_niO#+%NZ>`@8=}=M3lCMN$;;<%XLdF#s??4glN&002l&aIlXb z!qLYEA%*nvtTy%XDN~}mj+iEp4Fo5`BS~fhn7#&NFPdhLH-#F#83k6DgT@xHkD3;5 zBAgg9gBnz}+Ts~r?63clm7S!1Rn#2l(qX{M=7SaL(!fIUF)Y>3^YTzqG6=^zMj2Qh z6qC>PY*_j9RlphE=M}qD;S-EHJK*z3rG?mBT(jg!=0BRYM>z_$4c|+JD6!vgIq!?# zqzjf;)p(NeuGUZ>#yB5{O!Thp%71e2JE_5@EJl%A(t}Q2aF)u&bC=4`r-iSb zoSm+uNS0<;G4Y#=(OuF7{lwzysPt@9&vBYPD?#QCsr#8F1uv3Y^MrPp<7CHgTr8v; zT?1J>m#qxMip{)zY2x0?D2e^fiVcINQLSZfFTQnR+Ea1*^6N2vhv$bX@b(A+Rc&M< zr#bank@8J*#r3=Tow|+87cWzk3GGhN5B4KtdN^4hab;$A@K?#H7{bSutant%KxO`LB%@z@QR2>S`cw>}tf%Sh&`;OroAHQTw)fVM@lU2(qU!+}|+) zRm;MrP<=Ry46;5@x4EBI%V9W?KgGul1yp;WPTl`Cc)!-$?jqTH^DZI8!;{<++qTdvHCYhCb?^6LuDriE*Y)?pUCuK$;JVUm?t~ere8q|LxcI{D`QUatDxYBva<waTwCjkIH(V*+7rBK`yFAeI5EH9)>npO%^HX=<`Cp?Xd zXRh%zaB`h&x_2bj8^1CB>Z!6_OS<$OGF~66{9^i9LDi*IYjY~vlln<|aYRm!OzBF* za>|Hhx4n-qXOI=ANej}h#P)9=rz-sOPnn2JK)=A(nO3#}$@lHoC-~#|DQUh&mk_%f zxZFn;_w&&ez@!Oo*r<?`T6J^S4V?ZqiY3he`+PSJkvheO8 z71#dqLv9zmThd+xKIowJC{{MC=}T1Cez?BUD{UzvpWE0yehfEiDF{EgTF>2f4P#I) z9Z_i-inokmCXa!tQ^yp=a=RWhj4XQx(4sd|H{CL(`LZACF=sGWN}{ICH$53%(K@Nn z!+Q0c*QaM(OHXUy!J!T<;z>y8au*cHTd4r%aPh&_5~k{vK?Qh>x*lt(U^CU zA3OY~ZOhnps|!NaZXLT1j#Tt|s8v_e>5#}r4*?|X8jl20K#>kH%_6Wjo36NsbK0tl%q+(sDIvf!;PJyF-KumB;VC=EZYMTMS4Gl zk|x2O30dX`iOPKf7Ln%61A(~apF?s(vuqw8YwhX^{@^pYm}D-Y3b`cc2sS)iz65{& z%h{WozpTBv(bS)U5ksi2veR$X-hVjg7s?Zks*C2Cc#$gj9I+ZCfPfkw{z zBVl)VUsBybf^Df@g1GaY`>x@E;Yr2rdElNm5?7rkhmJz=vU#GkZ>ZWJlMJdaW`5jP zSg6xThz%%;xOWXKIll}3vdD@1x|yJ7NM4#SU;4ZyQu%mo@qw)?I_$+kT=#psQtm*Z z`U94KuT~@d04b#1QQ)u2@455XZbYVdmZ)zX_W^PKaw>dtsBZ_jbJ?>F6-*JapNMg7}bhiqyxT<0aAt)xrYKMia#Oz5n=HY@gOefBAhiGfZ7T

PuWVq4HZpitf85MYMG^Q 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 delta 3713 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 F{sS-T%v1mX diff --git a/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx b/public/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx index 419bd2e45ac75ed8a2d1048027ae33e96fb3c124..e6f1f3511e19867ed6ad3ebd7ac484dbda820132 100644 GIT binary patch delta 3738 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 E06F&;FaQ7m delta 3723 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 jKMnhHU^@T-0FM8Q_{V<2!4x#({ const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -195,6 +184,17 @@ function SearchBox({ lastRowOfFirstTable++; } + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + // Apply calculated widths to each column, skipping column A worksheet['!cols'] = colWidths.map((width, index) => { if (index === 0) { diff --git a/src/components/Report/ReportSearchBox2/SearchBox2.tsx b/src/components/Report/ReportSearchBox2/SearchBox2.tsx index 7e3d6e9..7c524af 100644 --- a/src/components/Report/ReportSearchBox2/SearchBox2.tsx +++ b/src/components/Report/ReportSearchBox2/SearchBox2.tsx @@ -140,17 +140,6 @@ function SearchBox({ const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -195,6 +184,17 @@ function SearchBox({ lastRowOfFirstTable++; } + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + // Apply calculated widths to each column, skipping column A worksheet['!cols'] = colWidths.map((width, index) => { if (index === 0) { diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx index b4a252d..aafa7d0 100644 --- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx +++ b/src/components/Report/ReportSearchBox3/SearchBox3.tsx @@ -139,18 +139,7 @@ function SearchBox({ const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - + // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -195,6 +184,17 @@ function SearchBox({ lastRowOfFirstTable++; } + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + // Apply calculated widths to each column, skipping column A worksheet['!cols'] = colWidths.map((width, index) => { if (index === 0) { diff --git a/src/components/Report/ReportSearchBox4/SearchBox4.tsx b/src/components/Report/ReportSearchBox4/SearchBox4.tsx index 1ac6e7e..bc18492 100644 --- a/src/components/Report/ReportSearchBox4/SearchBox4.tsx +++ b/src/components/Report/ReportSearchBox4/SearchBox4.tsx @@ -140,17 +140,6 @@ function SearchBox({ const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -195,6 +184,17 @@ function SearchBox({ lastRowOfFirstTable++; } + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + // Apply calculated widths to each column, skipping column A worksheet['!cols'] = colWidths.map((width, index) => { if (index === 0) { diff --git a/src/components/Report/ReportSearchBox5/SearchBox5.tsx b/src/components/Report/ReportSearchBox5/SearchBox5.tsx index 2b0b7db..c28a0a3 100644 --- a/src/components/Report/ReportSearchBox5/SearchBox5.tsx +++ b/src/components/Report/ReportSearchBox5/SearchBox5.tsx @@ -139,18 +139,7 @@ function SearchBox({ const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - + // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -195,6 +184,17 @@ function SearchBox({ lastRowOfFirstTable++; } + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + // Apply calculated widths to each column, skipping column A worksheet['!cols'] = colWidths.map((width, index) => { if (index === 0) { diff --git a/src/components/Report/ReportSearchBox6/SearchBox6.tsx b/src/components/Report/ReportSearchBox6/SearchBox6.tsx index 4e7c82d..322644f 100644 --- a/src/components/Report/ReportSearchBox6/SearchBox6.tsx +++ b/src/components/Report/ReportSearchBox6/SearchBox6.tsx @@ -140,17 +140,6 @@ function SearchBox({ const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); - // Calculate the maximum length of content in each column and set column width - const colWidths: number[] = []; - - const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; - jsonData.forEach((row: (string | number)[]) => { - row.forEach((cell: string | number, index: number) => { - const valueLength = cell.toString().length; - colWidths[index] = Math.max(colWidths[index] || 0, valueLength); - }); - }); - // Style for cell A1: Font size 16 and bold if (worksheet['A1']) { worksheet['A1'].s = { @@ -198,6 +187,17 @@ function SearchBox({ // Insert the first data form into the worksheet at the desired location XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + // Apply calculated widths to each column, skipping column A worksheet['!cols'] = colWidths.map((width, index) => { if (index === 0) { From 4dbed752e51c232ae49014f17a58b6b1e225f8f9 Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Fri, 26 Apr 2024 18:38:59 +0800 Subject: [PATCH 05/22] add project claims report --- public/temp/AR07_Project Claims Report.xlsx | Bin 0 -> 12968 bytes .../analytics/ProjectClaimsReport/page.tsx | 24 + src/app/api/report7/index.ts | 42 ++ .../ProjectClaimsReport.tsx | 17 + .../Report/ProjectClaimsReport/index.ts | 2 + .../ProjectClaimsReportGen.tsx | 44 ++ .../ProjectClaimsReportGenLoading.tsx | 41 ++ .../ProjectClaimsReportGenWrapper.tsx | 19 + .../Report/ProjectClaimsReportGen/index.ts | 2 + .../Report/ReportSearchBox7/SearchBox7.tsx | 465 ++++++++++++++++++ .../Report/ReportSearchBox7/index.ts | 3 + 11 files changed, 659 insertions(+) create mode 100644 public/temp/AR07_Project Claims Report.xlsx create mode 100644 src/app/(main)/analytics/ProjectClaimsReport/page.tsx create mode 100644 src/app/api/report7/index.ts create mode 100644 src/components/Report/ProjectClaimsReport/ProjectClaimsReport.tsx create mode 100644 src/components/Report/ProjectClaimsReport/index.ts create mode 100644 src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGen.tsx create mode 100644 src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenLoading.tsx create mode 100644 src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenWrapper.tsx create mode 100644 src/components/Report/ProjectClaimsReportGen/index.ts create mode 100644 src/components/Report/ReportSearchBox7/SearchBox7.tsx create mode 100644 src/components/Report/ReportSearchBox7/index.ts diff --git a/public/temp/AR07_Project Claims Report.xlsx b/public/temp/AR07_Project Claims Report.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..95b2c13bd92b90721a7b0d3e7d88e4ce2afa88e4 GIT binary patch literal 12968 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 LjctE%TY&!uw1_ud literal 0 HcmV?d00001 diff --git a/src/app/(main)/analytics/ProjectClaimsReport/page.tsx b/src/app/(main)/analytics/ProjectClaimsReport/page.tsx new file mode 100644 index 0000000..9f97f39 --- /dev/null +++ b/src/app/(main)/analytics/ProjectClaimsReport/page.tsx @@ -0,0 +1,24 @@ +//src\app\(main)\analytics\DelayReport\page.tsx +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import ProjectClaimsReportComponent from "@/components/Report/ProjectClaimsReport"; + +export const metadata: Metadata = { + title: "Project Claims Report", +}; + +const ProjectClaimsReport: React.FC = () => { + return ( + + + Project Claims Report + + {/* }> + + */} + + + ); +}; +export default ProjectClaimsReport; diff --git a/src/app/api/report7/index.ts b/src/app/api/report7/index.ts new file mode 100644 index 0000000..c31a754 --- /dev/null +++ b/src/app/api/report7/index.ts @@ -0,0 +1,42 @@ +//src\app\api\report\index.ts +import { cache } from "react"; + +export interface ProjectClaims { + id: number; + projectCode: string; + projectName: string; + team: string; + teamLeader: string; + startDate: string; + startDateFrom: string; + startDateTo: string; + targetEndDate: string; + client: string; + subsidiary: string; + staffName: string; +} + +export const preloadProjects = () => { + fetchProjectsProjectClaims(); +}; + +export const fetchProjectsProjectClaims = cache(async () => { + return mockProjects; +}); + +const mockProjects: ProjectClaims[] = [ + { + id: 1, + projectCode: "CUST-001", + projectName: "Client A", + team: "N/A", + teamLeader: "N/A", + startDate: "1/2/2024", + startDateFrom: "1/2/2024", + startDateTo: "1/2/2024", + targetEndDate: "30/3/2024", + client: "ss", + subsidiary: "sus", + staffName: "Leo", + }, +]; diff --git a/src/components/Report/ProjectClaimsReport/ProjectClaimsReport.tsx b/src/components/Report/ProjectClaimsReport/ProjectClaimsReport.tsx new file mode 100644 index 0000000..ca230e1 --- /dev/null +++ b/src/components/Report/ProjectClaimsReport/ProjectClaimsReport.tsx @@ -0,0 +1,17 @@ +//src\components\LateStartReport\LateStartReport.tsx +"use client"; +import * as React from "react"; +import "../../../app/global.css"; +import { Suspense } from "react"; +import ProjectClaimsReportGen from "@/components/Report/ProjectClaimsReportGen"; + +const ProjectClaimsReport: React.FC = () => { + + return ( + }> + + + ); +}; + +export default ProjectClaimsReport; \ No newline at end of file diff --git a/src/components/Report/ProjectClaimsReport/index.ts b/src/components/Report/ProjectClaimsReport/index.ts new file mode 100644 index 0000000..fc83eb4 --- /dev/null +++ b/src/components/Report/ProjectClaimsReport/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReport\index.ts +export { default } from "./ProjectClaimsReport"; diff --git a/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGen.tsx b/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGen.tsx new file mode 100644 index 0000000..204b847 --- /dev/null +++ b/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGen.tsx @@ -0,0 +1,44 @@ +//src\components\LateStartReportGen\LateStartReportGen.tsx +"use client"; +import React, { useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../ReportSearchBox7"; +import { useTranslation } from "react-i18next"; +import { ProjectClaims } from "@/app/api/report7"; +//import { DownloadReportButton } from './DownloadReportButton'; +interface Props { + projects: ProjectClaims[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ProgressByClientSearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: "Report Period From", + label2: "Report Period To", + paramName: "targetEndDate", + type: "dateRange", + }, + { label: "Project Code", paramName: "projectCode", type: "select", options: ["M1963", "M1235", "M1476"] }, + { label: "Staff Name", paramName: "staffName", type: "select", options: ["Kennith", "Tom", "Cyril"] }, + ], + [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + {/* */} + + ); +}; + +export default ProgressByClientSearch; diff --git a/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenLoading.tsx b/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenLoading.tsx new file mode 100644 index 0000000..ab04aa4 --- /dev/null +++ b/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenLoading.tsx @@ -0,0 +1,41 @@ +//src\components\LateStartReportGen\LateStartReportGenLoading.tsx +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ProjectClaimsReportGenLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectClaimsReportGenLoading; diff --git a/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenWrapper.tsx b/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenWrapper.tsx new file mode 100644 index 0000000..c5da0ec --- /dev/null +++ b/src/components/Report/ProjectClaimsReportGen/ProjectClaimsReportGenWrapper.tsx @@ -0,0 +1,19 @@ +//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx +import { fetchProjectsProjectClaims } from "@/app/api/report7"; +import React from "react"; +import ProjectClaimsReportGen from "./ProjectClaimsReportGen"; +import ProjectClaimsReportGenLoading from "./ProjectClaimsReportGenLoading"; + +interface SubComponents { + Loading: typeof ProjectClaimsReportGenLoading; +} + +const ProjectClaimsReportGenWrapper: React.FC & SubComponents = async () => { + const clentprojects = await fetchProjectsProjectClaims(); + + return ; +}; + +ProjectClaimsReportGenWrapper.Loading = ProjectClaimsReportGenLoading; + +export default ProjectClaimsReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/ProjectClaimsReportGen/index.ts b/src/components/Report/ProjectClaimsReportGen/index.ts new file mode 100644 index 0000000..dab06a1 --- /dev/null +++ b/src/components/Report/ProjectClaimsReportGen/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReportGen\index.ts +export { default } from "./ProjectClaimsReportGenWrapper"; diff --git a/src/components/Report/ReportSearchBox7/SearchBox7.tsx b/src/components/Report/ReportSearchBox7/SearchBox7.tsx new file mode 100644 index 0000000..5ad1d83 --- /dev/null +++ b/src/components/Report/ReportSearchBox7/SearchBox7.tsx @@ -0,0 +1,465 @@ +//src\components\ReportSearchBox\SearchBox.tsx +"use client"; + +import Grid from "@mui/material/Grid"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import React, { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TextField from "@mui/material/TextField"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Search from "@mui/icons-material/Search"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-hk"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Box } from "@mui/material"; +import * as XLSX from 'xlsx-js-style'; +//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; + +interface BaseCriterion { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/AR07_Project Claims Report.xlsx', { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + }); + if (!response.ok) throw new Error('Network response was not ok.'); + + const data = await response.blob(); + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target && e.target.result) { + const ab = e.target.result as ArrayBuffer; + const workbook = XLSX.read(ab, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // Add the current date to cell C2 + const cellAddress = 'C2'; + const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD + const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD + XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); + + // Style for cell A1: Font size 16 and bold + if (worksheet['A1']) { + worksheet['A1'].s = { + font: {bold: true,sz: 16, // Font size 16 + //name: 'Times New Roman' // Specify font + } + }; + } + + // Apply styles from A2 to A4 (bold) + ['A2', 'A3', 'A4'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: true } }; + } + }); + + // Formatting from A6 to G6 + // Apply styles from A5 to G5 (bold, bottom border, center alignment) + for (let col = 0; col < 7; col++) { // Columns A to G + const cellRef = XLSX.utils.encode_col(col) + '6'; + if (worksheet[cellRef]) { + worksheet[cellRef].s = { + font: { bold: true }, + alignment: { horizontal: 'center' }, + border: { + bottom: { style: 'thin', color: { auto: 1 } } + } + }; + } + } + + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + const secondTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 of second table + ['Data1', 'Data2', 'Data3'], // Row 2 of second table + // ... more rows as needed + ]; + + // Find the last row of the first table + let lastRowOfFirstTable = 6; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + + // Insert the first data form into the worksheet at the desired location + XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Update lastRowOfFirstTable to account for the new data + lastRowOfFirstTable += firstTableData.length; + // Now insert the text that goes between the two tables + + // Insert the additional text with one row of spacing after the first table + const textRow = lastRowOfFirstTable + 1; // Adjust the 1 based on how many lines of spacing you want + XLSX.utils.sheet_add_aoa(worksheet, [['AR07 - Project Claims Report (Staff)']], { origin: { c: 0, r: textRow } }); + XLSX.utils.sheet_add_aoa(worksheet, [['Report Generation Date:']], { origin: { c: 0, r: (textRow+1) } }); + XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: { c: 2, r: (textRow+1) } }); + XLSX.utils.sheet_add_aoa(worksheet, [['Report Period:']], { origin: { c: 0, r: (textRow+2) } }); + //XLSX.utils.sheet_add_aoa(worksheet, [[]], { origin: { c: 2, r: (textRow+2) } }); + XLSX.utils.sheet_add_aoa(worksheet, [['Total Claim Amount (HKD):']], { origin: { c: 0, r: (textRow+3) } }); + XLSX.utils.sheet_add_aoa(worksheet, [[]], { origin: { c: 2, r: (textRow+3) } }); + // Row 6 is the template row we want to copy + const templateRow = 6; + // This is the new row where we want to copy the template row's content and style + const newRow = textRow + 6; + // Copy content and style from each cell in row 6 to newRow + for (let col = 0; col < 7; col++) { // Adjust the 7 if there are more columns + const sourceCellRef = XLSX.utils.encode_cell({ c: col, r: templateRow - 1 }); + const targetCellRef = XLSX.utils.encode_cell({ c: col, r: newRow - 1 }); + // If the source cell exists, copy its content and style + if (worksheet[sourceCellRef]) { + // Copy cell content + worksheet[targetCellRef] = { ...worksheet[sourceCellRef] }; + // If there is a style, we need to deep clone it to avoid references to the same style object + if (worksheet[sourceCellRef].s) { + worksheet[targetCellRef].s = JSON.parse(JSON.stringify(worksheet[sourceCellRef].s)); + } + } + } + + let secondTableStartRow = textRow + 6; + // Insert the second data form into the worksheet at the new starting row + XLSX.utils.sheet_add_aoa(worksheet, secondTableData, { origin: { c: 0, r: secondTableStartRow } }); + + // Source cell coordinates + const sourceCellCoord = { c: 2, r: 2 }; // C3 (columns and rows are 0-indexed in this library) + // Target cell coordinates + const targetCellCoord = { c: 2, r: textRow + 2 }; + // Create references for source and target cells + const sourceCellRef = XLSX.utils.encode_cell(sourceCellCoord); + const targetCellRef = XLSX.utils.encode_cell(targetCellCoord); + // Copy the cell content from C3 to the target cell + if (worksheet[sourceCellRef]) { + worksheet[targetCellRef] = { ...worksheet[sourceCellRef] }; + // If the source cell has a style, deep clone it for the target cell + if (worksheet[sourceCellRef].s) { + worksheet[targetCellRef].s = JSON.parse(JSON.stringify(worksheet[sourceCellRef].s)); + } + } + // Define the range of cells to merge + const mergeRangeA1 = { + s: { c: 0, r: textRow}, // Start cell + e: { c: 3, r: textRow} // End cell + }; + // Add the range to the 'merges' array in the worksheet if it doesn't exist + if (!worksheet['!merges']) worksheet['!merges'] = []; + worksheet['!merges'].push(mergeRangeA1); + // Apply center alignment to the merged cell + const mergedCellRefA1 = XLSX.utils.encode_cell({ c: 0, r: textRow}); + if (!worksheet[mergedCellRefA1]) { + worksheet[mergedCellRefA1] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRefA1].s = { + alignment: {horizontal: "left",vertical: "center",wrapText: true} + }; + + // Define the range of cells to merge { c: 0, r: (textRow+1) } to { c: 1, r: (textRow+1) } + const mergeRangeA2 = { + s: { c: 0, r: textRow + 1 }, // Start cell + e: { c: 1, r: textRow + 1 } // End cell + }; + // Add the range to the 'merges' array in the worksheet if it doesn't exist + if (!worksheet['!merges']) worksheet['!merges'] = []; + worksheet['!merges'].push(mergeRangeA2); + // Apply center alignment to the merged cell + const mergedCellRefA2 = XLSX.utils.encode_cell({ c: 0, r: textRow + 1 }); + if (!worksheet[mergedCellRefA2]) { + worksheet[mergedCellRefA2] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRefA2].s = { + alignment: {horizontal: "left",vertical: "center",wrapText: true} + }; + + // Define the range of cells to merge + const mergeRangeA3 = { + s: { c: 0, r: textRow + 2 }, // Start cell + e: { c: 1, r: textRow + 2 } // End cell + }; + // Add the range to the 'merges' array in the worksheet if it doesn't exist + if (!worksheet['!merges']) worksheet['!merges'] = []; + worksheet['!merges'].push(mergeRangeA3); + // Apply center alignment to the merged cell + const mergedCellRefA3 = XLSX.utils.encode_cell({ c: 0, r: textRow + 2 }); + if (!worksheet[mergedCellRefA3]) { + worksheet[mergedCellRefA3] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRefA3].s = { + alignment: {horizontal: "left",vertical: "center",wrapText: true} + }; + + // Define the range of cells to merge + const mergeRangeA4 = { + s: { c: 0, r: textRow + 3 }, // Start cell + e: { c: 1, r: textRow + 3 } // End cell + }; + // Add the range to the 'merges' array in the worksheet if it doesn't exist + if (!worksheet['!merges']) worksheet['!merges'] = []; + worksheet['!merges'].push(mergeRangeA4); + // Apply center alignment to the merged cell + const mergedCellRefA4 = XLSX.utils.encode_cell({ c: 0, r: textRow + 3 }); + if (!worksheet[mergedCellRefA4]) { + worksheet[mergedCellRefA4] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRefA4].s = { + alignment: {horizontal: "left",vertical: "center",wrapText: true} + }; + + // Style for the cell at { c: 0, r: textRow } + const tStyle = { + font: { bold: true, sz: 16, color: { rgb: "000000" } }, // Example: Black, bold, 12pt font + alignment: { horizontal: "left", vertical: "center" }, + // Add any additional styling properties here + }; + + // Apply the unique style to the cell at { c: 0, r: textRow } + const cellRefUnique = XLSX.utils.encode_cell({ c: 0, r: textRow }); + worksheet[cellRefUnique].s = tStyle; + + // Style for the other cells + const stStyle = { + font: { bold: true, sz: 11, color: { rgb: "000000" } }, + alignment: { horizontal: "left", vertical: "center" }, + // Add any additional styling properties here + }; + + // Apply the same style to the cells at { c: 0, r: textRow+1 }, { c: 0, r: textRow+2 }, { c: 0, r: textRow+3 } + for (let i = 1; i <= 3; i++) { + const cellRefOther = XLSX.utils.encode_cell({ c: 0, r: textRow + i }); + if (!worksheet[cellRefOther]) { + worksheet[cellRefOther] = {}; // Create the cell if it doesn't exist + } + worksheet[cellRefOther].s = stStyle; + } + + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + jsonData.forEach((row: (string | number)[]) => { + row.forEach((cell: string | number, index: number) => { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + }); + }); + + // Apply calculated widths to each column, skipping column A + worksheet['!cols'] = colWidths.map((width, index) => { + if (index === 0) { + return { wch: 8 }; // Set default or specific width for column A if needed + } + return { wch: width + 2 }; // Add padding to width + }); + + // Format filename with date + const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD + const filename = `AR07_Project_Claims_Report_${today}.xlsx`; // Append formatted date to the filename + + // Convert workbook back to XLSX file + XLSX.writeFile(workbook, filename); + } else { + throw new Error('Failed to load file'); + } + }; + reader.readAsArrayBuffer(data); + } catch (error) { + console.error('Error downloading the file: ', error); + } + + //setIsLoading(false); + }; + return ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/Report/ReportSearchBox7/index.ts b/src/components/Report/ReportSearchBox7/index.ts new file mode 100644 index 0000000..b3a9815 --- /dev/null +++ b/src/components/Report/ReportSearchBox7/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBox7"; +export type { Criterion } from "./SearchBox7"; From afe7cac746466dbcd2484f1aa6d3bad331e72354 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 29 Apr 2024 12:19:37 +0800 Subject: [PATCH 06/22] master pages --- src/app/(main)/settings/skill/create/page.tsx | 48 +++++ src/app/(main)/settings/skill/page.tsx | 50 +++++ src/app/(main)/settings/user/page.tsx | 54 ++++++ src/app/api/skill/actions.ts | 18 +- src/app/api/skill/index.ts | 22 +++ src/app/api/staff/actions.ts | 6 +- src/app/api/team/actions.ts | 7 +- src/app/api/user/actions.ts | 27 +++ src/app/api/user/index.ts | 43 +++++ src/components/CreateSkill/CreateSkill.tsx | 122 ++++++++++++ .../CreateSkill/CreateSkillLoading.tsx | 40 ++++ .../CreateSkill/CreateSkillWrapper.tsx | 19 ++ src/components/CreateSkill/SkillInfo.tsx | 90 +++++++++ src/components/CreateSkill/index.ts | 1 + src/components/CreateTeam/CreateTeam.tsx | 2 +- src/components/CreateTeam/StaffAllocation.tsx | 174 ++++++++++-------- .../NavigationContent/NavigationContent.tsx | 4 + src/components/SkillSearch/SkillSearch.tsx | 96 ++++++++++ .../SkillSearch/SkillSearchLoading.tsx | 40 ++++ .../SkillSearch/SkillSearchWrapper.tsx | 27 +++ src/components/SkillSearch/index.ts | 1 + .../StaffSearch/ConfirmDeleteModal.tsx | 106 ----------- src/components/StaffSearch/StaffSearch.tsx | 79 +++----- .../TeamSearch/ConfirmDeleteModal.tsx | 105 ----------- src/components/TeamSearch/TeamSearch.tsx | 169 ++++++++--------- src/components/UserSearch/UserSearch.tsx | 98 ++++++++++ .../UserSearch/UserSearchLoading.tsx | 40 ++++ .../UserSearch/UserSearchWrapper.tsx | 19 ++ src/components/UserSearch/index.ts | 1 + 29 files changed, 1062 insertions(+), 446 deletions(-) create mode 100644 src/app/(main)/settings/skill/create/page.tsx create mode 100644 src/app/(main)/settings/skill/page.tsx create mode 100644 src/app/(main)/settings/user/page.tsx create mode 100644 src/app/api/skill/index.ts create mode 100644 src/app/api/user/actions.ts create mode 100644 src/app/api/user/index.ts create mode 100644 src/components/CreateSkill/CreateSkill.tsx create mode 100644 src/components/CreateSkill/CreateSkillLoading.tsx create mode 100644 src/components/CreateSkill/CreateSkillWrapper.tsx create mode 100644 src/components/CreateSkill/SkillInfo.tsx create mode 100644 src/components/CreateSkill/index.ts create mode 100644 src/components/SkillSearch/SkillSearch.tsx create mode 100644 src/components/SkillSearch/SkillSearchLoading.tsx create mode 100644 src/components/SkillSearch/SkillSearchWrapper.tsx create mode 100644 src/components/SkillSearch/index.ts delete mode 100644 src/components/StaffSearch/ConfirmDeleteModal.tsx delete mode 100644 src/components/TeamSearch/ConfirmDeleteModal.tsx create mode 100644 src/components/UserSearch/UserSearch.tsx create mode 100644 src/components/UserSearch/UserSearchLoading.tsx create mode 100644 src/components/UserSearch/UserSearchWrapper.tsx create mode 100644 src/components/UserSearch/index.ts diff --git a/src/app/(main)/settings/skill/create/page.tsx b/src/app/(main)/settings/skill/create/page.tsx new file mode 100644 index 0000000..c98f993 --- /dev/null +++ b/src/app/(main)/settings/skill/create/page.tsx @@ -0,0 +1,48 @@ +// 'use client'; +import { I18nProvider, getServerI18n } from "@/i18n"; +import CustomInputForm from "@/components/CustomInputForm"; +import Check from "@mui/icons-material/Check"; +import Close from "@mui/icons-material/Close"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Tab from "@mui/material/Tab"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Task, TaskTemplate } from "@/app/api/tasks"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; +import { Error } from "@mui/icons-material"; +import { ProjectCategory } from "@/app/api/projects"; +import { Grid, Typography } from "@mui/material"; +import CreateStaffForm from "@/components/CreateStaff/CreateStaff"; +import CreateSkill from "@/components/CreateSkill"; + +// const Title = ["title1", "title2"]; + +const CreateStaff: React.FC = async () => { + const { t } = await getServerI18n("staff"); + + const title = ['', t('Additional Info')] + // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") + // console.log(regex) + + return ( + <> + {t("Create Skill")} + + + + + ); +}; + +export default CreateStaff; diff --git a/src/app/(main)/settings/skill/page.tsx b/src/app/(main)/settings/skill/page.tsx new file mode 100644 index 0000000..f263c87 --- /dev/null +++ b/src/app/(main)/settings/skill/page.tsx @@ -0,0 +1,50 @@ +import { preloadClaims } from "@/app/api/claims"; +// import { preloadSkill, preloadTeamLeads } from "@/app/api/staff"; +import SkillSearch from "@/components/SkillSearch"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Skill", +}; + +const Skill: React.FC = async () => { + const { t } = await getServerI18n("skill"); +// preloadTeamLeads(); +// preloadSkill(); + return ( + <> + + + {t("Skill")} + + + + + }> + + + + + ); +}; + +export default Skill; diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx new file mode 100644 index 0000000..95973ab --- /dev/null +++ b/src/app/(main)/settings/user/page.tsx @@ -0,0 +1,54 @@ +import { preloadClaims } from "@/app/api/claims"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import StaffSearch from "@/components/StaffSearch"; +import TeamSearch from "@/components/TeamSearch"; +import UserSearch from "@/components/UserSearch"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; + + +export const metadata: Metadata = { + title: "User", + }; + + + const User: React.FC = async () => { + const { t } = await getServerI18n("User"); + // preloadTeamLeads(); + // preloadStaff(); + return ( + <> + + + {t("User")} + + + + + }> + + + + + ); + }; + + export default User; \ No newline at end of file diff --git a/src/app/api/skill/actions.ts b/src/app/api/skill/actions.ts index eda7f39..6a0deca 100644 --- a/src/app/api/skill/actions.ts +++ b/src/app/api/skill/actions.ts @@ -5,6 +5,13 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; +export interface CreateSkillInputs { + id?: number; + name: String; + code: String; + description: String; +} + export interface comboProp { id: any; label: string; @@ -18,4 +25,13 @@ export const fetchSkillCombo = cache(async () => { return serverFetchJson(`${BASE_API_URL}/skill/combo`, { next: { tags: ["skill"] }, }); - }); \ No newline at end of file + }); + + +export const saveSkill = async (data: CreateSkillInputs) => { + return serverFetchJson(`${BASE_API_URL}/skill/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/skill/index.ts b/src/app/api/skill/index.ts new file mode 100644 index 0000000..cf6ebec --- /dev/null +++ b/src/app/api/skill/index.ts @@ -0,0 +1,22 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + +export interface SkillResult { + action: any; + id: number; + name: string; + description: string; + code: string; + } + + export const preloadSkill = () => { + fetchSkill(); + }; + + export const fetchSkill = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/skill`, { + next: { tags: ["sill"] }, + }); + }); \ No newline at end of file diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 9416d2d..a2235d6 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { StaffResult, data } from "."; import { cache } from "react"; @@ -59,8 +59,8 @@ export const testing = async (data: CreateStaffInputs) => { }); }; -export const deleteStaff = async (data: StaffResult) => { - return serverFetchJson(`${BASE_API_URL}/staffs/delete/${data.id}`, { +export const deleteStaff = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/staffs/delete/${id}`, { method: "DELETE", // body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/team/actions.ts b/src/app/api/team/actions.ts index 28496d0..47e1a82 100644 --- a/src/app/api/team/actions.ts +++ b/src/app/api/team/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import { TeamResult } from "."; @@ -53,10 +53,9 @@ export const saveTeam = async (data: CreateTeamInputs) => { }; -export const deleteTeam = async (data: TeamResult) => { - return serverFetchJson(`${BASE_API_URL}/team/delete/${data.id}`, { +export const deleteTeam = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/team/delete/${id}`, { method: "DELETE", - // body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts new file mode 100644 index 0000000..5df734a --- /dev/null +++ b/src/app/api/user/actions.ts @@ -0,0 +1,27 @@ +"use server"; + +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; +import { UserDetail, UserResult } from "."; +import { cache } from "react"; + +export interface UserInputs { + username: string; + firstname: string; + lastname: string; +} + + +export const fetchUserDetails = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/user/${id}`, { + next: { tags: ["user"] }, + }); + }); + +export const deleteUser = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts new file mode 100644 index 0000000..9a6065b --- /dev/null +++ b/src/app/api/user/index.ts @@ -0,0 +1,43 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + + +export interface UserResult { + action: any; + id: number; + name: string; + locale: string; + username: string; + fullName: string; + firstname: string; + lastname: string; + title: string; + department: string; + email: string; + phone1: string; + phone2: string; + remarks: string; + } + +// export interface DetailedUser extends UserResult { +// username: string; +// password: string +// } + +export interface UserDetail { + authIds: number[]; + data: UserResult; + groupIds: number[]; + } + + export const preloadUser = () => { + fetchUser(); + }; + + export const fetchUser = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/user`, { + next: { tags: ["user"] }, + }); + }); \ No newline at end of file diff --git a/src/components/CreateSkill/CreateSkill.tsx b/src/components/CreateSkill/CreateSkill.tsx new file mode 100644 index 0000000..d264b34 --- /dev/null +++ b/src/components/CreateSkill/CreateSkill.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; +import { Check, Close, RestartAlt } from "@mui/icons-material"; +import { useCallback, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions"; +import { Error } from "@mui/icons-material"; +import SkillInfo from "./SkillInfo"; + +interface Props {} + +const CreateSkill: React.FC = () => { + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const { t } = useTranslation(); + const [tabIndex, setTabIndex] = useState(0); + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + await saveSkill(data) + router.replace(`/settings/skill`) + } catch (e) { + console.log(e); + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router] + ); + + const handleCancel = () => { + router.back(); + }; + +// const handleReset = useCallback(() => { +// console.log(defaultValues) +// }, [defaultValues]) + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors + ) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } + }; + return ( + <> + + + + + ) : undefined + } + iconPosition="end" + /> + {/* */} + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + + + + + + + + ); +}; + +export default CreateSkill; diff --git a/src/components/CreateSkill/CreateSkillLoading.tsx b/src/components/CreateSkill/CreateSkillLoading.tsx new file mode 100644 index 0000000..f7d17bf --- /dev/null +++ b/src/components/CreateSkill/CreateSkillLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const CreateSkillLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + CreateSkill + + + + + + + + + + + ); +}; + +export default CreateSkillLoading; diff --git a/src/components/CreateSkill/CreateSkillWrapper.tsx b/src/components/CreateSkill/CreateSkillWrapper.tsx new file mode 100644 index 0000000..f2f667a --- /dev/null +++ b/src/components/CreateSkill/CreateSkillWrapper.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import CreateSkill from "./CreateSkill"; +import CreateSkillLoading from "./CreateSkillLoading"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import { useSearchParams } from "next/navigation"; + +interface SubComponents { + Loading: typeof CreateSkillLoading; +} + +const CreateSkillWrapper: React.FC & SubComponents = async () => { + + + return ; +}; + +CreateSkillWrapper.Loading = CreateSkillLoading; + +export default CreateSkillWrapper; diff --git a/src/components/CreateSkill/SkillInfo.tsx b/src/components/CreateSkill/SkillInfo.tsx new file mode 100644 index 0000000..be9724d --- /dev/null +++ b/src/components/CreateSkill/SkillInfo.tsx @@ -0,0 +1,90 @@ +"use client"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { useTranslation } from "react-i18next"; +import CardActions from "@mui/material/CardActions"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Button from "@mui/material/Button"; +import { Controller, useFormContext } from "react-hook-form"; +import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import { useCallback } from "react"; +import { CreateSkillInputs } from "@/app/api/skill/actions"; + +const SkillInfo: React.FC = ( +) => { + const { t } = useTranslation(); + const { + register, + formState: { errors, defaultValues }, + control, + reset, + resetField, + setValue, + } = useFormContext(); + + const resetSkill = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("name"); + } + }, [defaultValues]); + + return ( + <> + + + + + {t("Skill Info")} + + + + + + + + + + + + + + + + + ); +}; +export default SkillInfo; diff --git a/src/components/CreateSkill/index.ts b/src/components/CreateSkill/index.ts new file mode 100644 index 0000000..044c4cf --- /dev/null +++ b/src/components/CreateSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateSkillWrapper"; diff --git a/src/components/CreateTeam/CreateTeam.tsx b/src/components/CreateTeam/CreateTeam.tsx index 93b585e..64159c0 100644 --- a/src/components/CreateTeam/CreateTeam.tsx +++ b/src/components/CreateTeam/CreateTeam.tsx @@ -89,7 +89,7 @@ const hasErrorsInTab = ( } iconPosition="end" /> - + {serverError && ( diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index c51b839..bbd768c 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -18,9 +18,21 @@ import { StaffResult } from "@/app/api/staff"; import SearchResults, { Column } from "../SearchResults"; import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; import { Card } from "reactstrap"; -import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material"; +import { + Box, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; import { differenceBy } from "lodash"; -import StarsIcon from '@mui/icons-material/Stars'; +import StarsIcon from "@mui/icons-material/Stars"; export interface Props { allStaffs: StaffResult[]; @@ -35,16 +47,15 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { reset, resetField, } = useFormContext(); - + const initialStaffs = staff.map((s) => ({ ...s })); -// console.log(initialStaffs) + // console.log(initialStaffs) const [filteredStaff, setFilteredStaff] = useState(initialStaffs); const [selectedStaff, setSelectedStaff] = useState( initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) ); - const [seletedTeamLead, setSeletedTeamLead] = useState() - // Adding / Removing staff + // Adding / Removing staff const addStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => [...s, staff]); }, []); @@ -53,27 +64,31 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); }, []); - const setTeamLead = useCallback((staff: StaffResult) => { - - setSeletedTeamLead(staff.id) - const rearrangedList = getValues("addStaffIds").reduce((acc, num, index) => { - if (num === staff.id && index !== 0) { + const setTeamLead = useCallback( + (staff: StaffResult) => { + const rearrangedList = getValues("addStaffIds").reduce( + (acc, num, index) => { + if (num === staff.id && index !== 0) { acc.splice(index, 1); - acc.unshift(num) - } - return acc; - }, getValues("addStaffIds")); - console.log(rearrangedList) - console.log(selectedStaff) - - const rearrangedStaff = rearrangedList.map((id) => { + acc.unshift(num); + } + return acc; + }, + getValues("addStaffIds") + ); + console.log(rearrangedList); + console.log(selectedStaff); + + const rearrangedStaff = rearrangedList.map((id) => { return selectedStaff.find((staff) => staff.id === id); }); - console.log(rearrangedStaff) - setSelectedStaff(rearrangedStaff as StaffResult[]); + console.log(rearrangedStaff); + setSelectedStaff(rearrangedStaff as StaffResult[]); - setValue("addStaffIds", rearrangedList) - }, [addStaff, selectedStaff]); + setValue("addStaffIds", rearrangedList); + }, + [addStaff, selectedStaff] + ); const clearSubsidiary = useCallback(() => { if (defaultValues !== undefined) { @@ -86,7 +101,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { // Sync with form useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); setValue( "addStaffIds", selectedStaff.map((s) => s.id) @@ -94,7 +109,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, [selectedStaff, setValue]); useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); }, [selectedStaff]); const StaffPoolColumns = useMemo[]>( @@ -107,7 +122,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, { label: t("Staff Id"), name: "staffId" }, { label: t("Staff Name"), name: "name" }, - { label: t("Current Position"), name: "currentPosition" }, + { label: t("Position"), name: "currentPosition" }, ], [addStaff, t] ); @@ -122,7 +137,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, { label: t("Staff Id"), name: "staffId" }, { label: t("Staff Name"), name: "name" }, - { label: t("Current Position"), name: "currentPosition" }, + { label: t("Position"), name: "currentPosition" }, { label: t("Team Lead"), name: "action", @@ -144,16 +159,16 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + setFilteredStaff( + initialStaffs.filter((i) => { + const q = query.toLowerCase(); + return ( + i.staffId.toLowerCase().includes(q) || + i.name.toLowerCase().includes(q) || + i.currentPosition.toLowerCase().includes(q) + ); + }) + ); }, [staff, query]); const resetStaff = React.useCallback(() => { @@ -161,8 +176,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { clearSubsidiary(); }, [clearQueryInput, clearSubsidiary]); - const formProps = useForm({ - }); + const formProps = useForm({}); // Tab related const [tabIndex, setTabIndex] = React.useState(0); @@ -170,7 +184,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { (_e, newValue) => { setTabIndex(newValue); }, - [], + [] ); return ( @@ -185,48 +199,48 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { {t("staff")} - - - - - - - - ), - }} - /> + + + + + + + + ), + }} + /> + - - - - - - - {tabIndex === 0 && ( - - )} - {tabIndex === 1 && ( - + + - )} - + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index ad68823..9016052 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -31,6 +31,8 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; import GroupIcon from '@mui/icons-material/Group'; import BusinessIcon from '@mui/icons-material/Business'; +import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; interface NavigationItem { icon: React.ReactNode; @@ -117,10 +119,12 @@ const navigationItems: NavigationItem[] = [ { icon: , label: "Subsidiary", path: "/settings/subsidiary" }, { icon: , label: "Staff", path: "/settings/staff" }, { icon: , label: "Company", path: "/settings/company" }, + { icon: , label: "Skill", path: "/settings/skill" }, { icon: , label: "Department", path: "/settings/department" }, { icon: , label: "Position", path: "/settings/position" }, { icon: , label: "Salary", path: "/settings/salary" }, { icon: , label: "Team", path: "/settings/team" }, + { icon: , label: "User", path: "/settings/user" }, ], }, ]; diff --git a/src/components/SkillSearch/SkillSearch.tsx b/src/components/SkillSearch/SkillSearch.tsx new file mode 100644 index 0000000..01db336 --- /dev/null +++ b/src/components/SkillSearch/SkillSearch.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox/index"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { SkillResult } from "@/app/api/skill"; + +interface Props { + skill: SkillResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const SkillSearch: React.FC = ({ skill }) => { + const { t } = useTranslation(); + const [filteredStaff, setFilteredStaff] = useState(skill); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff Name"), + paramName: "name", + type: "text", + }, + ], + [t] + ); + + const onSkillClick = useCallback( + (skill: SkillResult) => { + console.log(skill); + const id = skill.id; + // router.push(`/settings/skill/edit?id=${id}`); + }, + [router, t] + ); + + const deleteClick = useCallback((skill: SkillResult) => { + // deleteDialog(async () => { + // await deleteStaff(skill.id); + // successDialog("Delete Success", t); + // setFilteredStaff((prev) => prev.filter((obj) => obj.id !== skill.id)); + // }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Actions"), + onClick: onSkillClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("Description") }, + { + name: "action", + label: t("Actions"), + onClick: deleteClick, + buttonIcon: , + color: "error", + }, + ], + [t, onSkillClick, deleteClick] + ); + + return ( + <> + { + // setFilteredStaff( + // skill.filter( + // (s) => + // s.skillId.toLowerCase().includes(query.skillId.toLowerCase()) && + // s.name.toLowerCase().includes(query.name.toLowerCase()) + // // (query.team === "All" || s.team === query.team) && + // // (query.category === "All" || s.category === query.category) && + // // (query.team === "All" || s.team === query.team), + // ) + // ); + }} + /> + items={filteredStaff} columns={columns} /> + + ); +}; + +export default SkillSearch; diff --git a/src/components/SkillSearch/SkillSearchLoading.tsx b/src/components/SkillSearch/SkillSearchLoading.tsx new file mode 100644 index 0000000..a5959e9 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const SkillSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SkillSearchLoading; diff --git a/src/components/SkillSearch/SkillSearchWrapper.tsx b/src/components/SkillSearch/SkillSearchWrapper.tsx new file mode 100644 index 0000000..33d0547 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchWrapper.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import SkillSearch from "./SkillSearch"; +import SkillSearchLoading from "./SkillSearchLoading"; +import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; +import { fetchTeamCombo } from "@/app/api/team/actions"; +import { fetchDepartmentCombo } from "@/app/api/departments/actions"; +import { fetchPositionCombo } from "@/app/api/positions/actions"; +import { fetchGradeCombo } from "@/app/api/grades/actions"; +import { fetchSkillCombo } from "@/app/api/skill/actions"; +import { fetchSalaryCombo } from "@/app/api/salarys/actions"; +import { SkillResult, fetchSkill } from "@/app/api/skill"; +// import { preloadStaff } from "@/app/api/staff"; + +interface SubComponents { + Loading: typeof SkillSearchLoading; +} + +const SkillSearchWrapper: React.FC & SubComponents = async () => { + const skill = await fetchSkill() + console.log(skill); + + return ; +}; + +SkillSearchWrapper.Loading = SkillSearchLoading; + +export default SkillSearchWrapper; diff --git a/src/components/SkillSearch/index.ts b/src/components/SkillSearch/index.ts new file mode 100644 index 0000000..5833a58 --- /dev/null +++ b/src/components/SkillSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./SkillSearchWrapper"; diff --git a/src/components/StaffSearch/ConfirmDeleteModal.tsx b/src/components/StaffSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index abeb962..0000000 --- a/src/components/StaffSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; - // staff: StaffResult[]; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index e65cfe7..fc6204d 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -5,15 +5,11 @@ import SearchBox, { Criterion } from "../SearchBox/index"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import ConfirmModal from "./ConfirmDeleteModal"; +import DeleteIcon from "@mui/icons-material/Delete"; import { deleteStaff } from "@/app/api/staff/actions"; import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; -interface combo { - id: any; - label: string; -} interface Props { staff: StaffResult[]; } @@ -24,8 +20,6 @@ type SearchParamNames = keyof SearchQuery; const StaffSearch: React.FC = ({ staff }) => { const { t } = useTranslation(); const [filteredStaff, setFilteredStaff] = useState(staff); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); const router = useRouter(); const searchCriteria: Criterion[] = useMemo( @@ -41,10 +35,10 @@ const StaffSearch: React.FC = ({ staff }) => { paramName: "name", type: "text", }, - { - label: t("Staff ID"), - paramName: "staffId", - type: "text" + { + label: t("Staff ID"), + paramName: "staffId", + type: "text", }, { label: t("Grade"), @@ -59,39 +53,26 @@ const StaffSearch: React.FC = ({ staff }) => { options: ["pos1", "CEO"], }, ], - [t], + [t] ); - const onStaffClick = useCallback((staff: StaffResult) => { - console.log(staff); - const id = staff.id - router.push(`/settings/staff/edit?id=${id}`); - }, [router, t]); - - const deleteClick = (staff: StaffResult) => { - console.log(staff); - setData(staff) - setIsOpen(!isOpen) - }; - - const onConfirm = useCallback(async (staff: StaffResult) => { - console.log(staff); - if (data) - await deleteStaff(data) - setIsOpen(false) - window.location.reload; - }, [deleteStaff, data]); + const onStaffClick = useCallback( + (staff: StaffResult) => { + console.log(staff); + const id = staff.id; + router.push(`/settings/staff/edit?id=${id}`); + }, + [router, t] + ); - const onCancel = useCallback((staff: StaffResult) => { - console.log(staff); - setIsOpen(false) + const deleteClick = useCallback((staff: StaffResult) => { + deleteDialog(async () => { + await deleteStaff(staff.id); + successDialog("Delete Success", t); + setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); + }, t); }, []); - // useEffect(() => { - // console.log("id"); - // console.log(id); - // }, [id]); - const columns = useMemo[]>( () => [ { @@ -110,34 +91,30 @@ const StaffSearch: React.FC = ({ staff }) => { label: t("Actions"), onClick: deleteClick, buttonIcon: , + color: "error", }, ], - [t, onStaffClick, deleteClick], + [t, onStaffClick, deleteClick] ); return ( <> { + onSearch={(query) => { setFilteredStaff( staff.filter( - (s) => - s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - s.name.toLowerCase().includes(query.name.toLowerCase()) + (s) => + s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && + s.name.toLowerCase().includes(query.name.toLowerCase()) // (query.team === "All" || s.team === query.team) && // (query.category === "All" || s.category === query.category) && // (query.team === "All" || s.team === query.team), ) - ) + ); }} /> items={filteredStaff} columns={columns} /> - ); }; diff --git a/src/components/TeamSearch/ConfirmDeleteModal.tsx b/src/components/TeamSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index a5e7ed0..0000000 --- a/src/components/TeamSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index b2cc9e8..a1db872 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -6,12 +6,10 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import { deleteStaff } from "@/app/api/staff/actions"; +import DeleteIcon from "@mui/icons-material/Delete"; import { useRouter } from "next/navigation"; -import ConfirmModal from "./ConfirmDeleteModal"; import { deleteTeam } from "@/app/api/team/actions"; - +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { team: TeamResult[]; @@ -20,109 +18,90 @@ type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const TeamSearch: React.FC = ({ team }) => { - const { t } = useTranslation(); - const [filteredTeam, setFilteredTeam] = useState(team); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); - const router = useRouter(); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Team Name"), - paramName: "name", - type: "text", - }, - { - label: t("Team Code"), - paramName: "code", - type: "text", - }, - { - label: t("Team Description"), - paramName: "description", - type: "text", - }, - ], - [t], - ); + const { t } = useTranslation(); + const [filteredTeam, setFilteredTeam] = useState(team); + const router = useRouter(); - const onTeamClick = useCallback((team: TeamResult) => { - console.log(team); - const id = team.id - router.push(`/settings/team/edit?id=${id}`); - }, [router, t]); - - // const onDeleteClick = useCallback((team: TeamResult) => { - // console.log(team); - // deleteTeam + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team Name"), + paramName: "name", + type: "text", + }, + { + label: t("Team Code"), + paramName: "code", + type: "text", + }, + { + label: t("Team Description"), + paramName: "description", + type: "text", + }, + ], + [t] + ); - // }, [router, t]); + const onTeamClick = useCallback( + (team: TeamResult) => { + console.log(team); + const id = team.id; + router.push(`/settings/team/edit?id=${id}`); + }, + [router, t] + ); - const onDeleteClick = (team: TeamResult) => { - console.log(team); - setData(team) - setIsOpen(!isOpen) - }; + const onDeleteClick = useCallback((team: TeamResult) => { + deleteDialog(async () => { + await deleteTeam(team.id); - const onConfirm = useCallback(async (team: TeamResult) => { - console.log(team); - if (data) - await deleteTeam(data) - setIsOpen(false) - window.location.reload; - }, [deleteTeam, data]); + successDialog("Delete Success", t); - const onCancel = useCallback(() => { - setIsOpen(false) - }, []); + setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); + }, t); + }, []); - const columns = useMemo[]>( - () => [ - { - name: "action", - label: t("Edit"), - onClick: onTeamClick, - buttonIcon: , - }, - { name: "name", label: t("Name") }, - { name: "code", label: t("Code") }, - { name: "description", label: t("description") }, - { - name: "action", - label: t("Delete"), - onClick: onDeleteClick, - buttonIcon: , - }, - ], - [t], - ); + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onTeamClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("description") }, + { name: "staffName", label: t("TeamLead") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); return ( - <> - + { - // setFilteredStaff( - // staff.filter( - // (s) => - // s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - // s.name.toLowerCase().includes(query.name.toLowerCase()) - // // (query.team === "All" || s.team === query.team) && - // // (query.category === "All" || s.category === query.category) && - // // (query.team === "All" || s.team === query.team), - // ) - // ) + onSearch={(query) => { + setFilteredTeam( + team.filter( + (t) => + t.name.toLowerCase().includes(query.name.toLowerCase()) && + t.code.toLowerCase().includes(query.code.toLowerCase()) && + t.description.toLowerCase().includes(query.description.toLowerCase()) + ) + ) }} /> items={filteredTeam} columns={columns} /> - - - + ); }; export default TeamSearch; diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx new file mode 100644 index 0000000..095c544 --- /dev/null +++ b/src/components/UserSearch/UserSearch.tsx @@ -0,0 +1,98 @@ +"use client"; + +import SearchBox, { Criterion } from "../SearchBox"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { UserResult } from "@/app/api/user"; +import { deleteUser } from "@/app/api/user/actions"; + +interface Props { + users: UserResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const UserSearch: React.FC = ({ users }) => { + const { t } = useTranslation(); + const [filteredUser, setFilteredUser] = useState(users); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("User Name"), + paramName: "title", + type: "text", + }, + ], + [t] + ); + + const onUserClick = useCallback( + (users: UserResult) => { + console.log(users); + router.push(`/settings/user/edit?id=${users.id}`) + }, + [router, t] + ); + + const onDeleteClick = useCallback((users: UserResult) => { + deleteDialog(async () => { + await deleteUser(users.id); + + successDialog("Delete Success", t); + + setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); + }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onUserClick, + buttonIcon: , + }, + { name: "name", label: t("UserName") }, + { name: "fullName", label: t("FullName") }, + { name: "title", label: t("Title") }, + { name: "department", label: t("Department") }, + { name: "email", label: t("Email") }, + { name: "phone1", label: t("Phone") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); + + return ( + <> + { + // setFilteredUser( + // users.filter( + // (t) => + // t.name.toLowerCase().includes(query.name.toLowerCase()) && + // t.code.toLowerCase().includes(query.code.toLowerCase()) && + // t.description.toLowerCase().includes(query.description.toLowerCase()) + // ) + // ) + }} + /> + items={filteredUser} columns={columns} /> + + ); +}; +export default UserSearch; diff --git a/src/components/UserSearch/UserSearchLoading.tsx b/src/components/UserSearch/UserSearchLoading.tsx new file mode 100644 index 0000000..535a751 --- /dev/null +++ b/src/components/UserSearch/UserSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const UserSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default UserSearchLoading; diff --git a/src/components/UserSearch/UserSearchWrapper.tsx b/src/components/UserSearch/UserSearchWrapper.tsx new file mode 100644 index 0000000..beaef92 --- /dev/null +++ b/src/components/UserSearch/UserSearchWrapper.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import UserSearch from "./UserSearch"; +import UserSearchLoading from "./UserSearchLoading"; +import { UserResult, fetchUser } from "@/app/api/user"; + +interface SubComponents { + Loading: typeof UserSearchLoading; +} + +const UserSearchWrapper: React.FC & SubComponents = async () => { +const users = await fetchUser() + console.log(users); + + return ; +}; + +UserSearchWrapper.Loading = UserSearchLoading; + +export default UserSearchWrapper; diff --git a/src/components/UserSearch/index.ts b/src/components/UserSearch/index.ts new file mode 100644 index 0000000..c2e98d7 --- /dev/null +++ b/src/components/UserSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./UserSearchWrapper"; From dedeff4b413fd2c2e2aa6f3f52b1e48fd76ad647 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 16:41:03 +0800 Subject: [PATCH 07/22] update claim, add EX02 report --- .../EX02ProjectCashFlowReport/page.tsx | 25 +++++++++ src/app/api/claims/actions.ts | 2 +- src/app/api/reports/actions.ts | 18 +++++++ src/app/api/reports/index.ts | 8 +++ src/app/utils/commonUtil.ts | 16 ++++++ src/app/utils/fetchUtil.ts | 26 ++++++++-- src/components/Breadcrumb/Breadcrumb.tsx | 1 + src/components/ClaimDetail/ClaimDetail.tsx | 3 +- .../ClaimDetail/ClaimFormInputGrid.tsx | 9 ++-- src/components/ClaimSearch/ClaimSearch.tsx | 12 ++--- .../GenerateEX02ProjectCashFlowReport.tsx | 52 +++++++++++++++++++ ...nerateEX02ProjectCashFlowReportLoading.tsx | 38 ++++++++++++++ ...nerateEX02ProjectCashFlowReportWrapper.tsx | 18 +++++++ .../index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 1 + src/i18n/en/claim.json | 1 + src/i18n/en/report.json | 3 ++ src/i18n/zh/claim.json | 1 + src/i18n/zh/report.json | 3 ++ 19 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx create mode 100644 src/app/api/reports/actions.ts create mode 100644 src/app/api/reports/index.ts create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/index.ts create mode 100644 src/i18n/en/report.json create mode 100644 src/i18n/zh/report.json diff --git a/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx new file mode 100644 index 0000000..d1d54d8 --- /dev/null +++ b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx @@ -0,0 +1,25 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "@/components/GenerateEX02ProjectCashFlowReport"; + +export const metadata: Metadata = { + title: "EX02 - Project Cash Flow Report", +}; + +const ProjectCashFlowReport: React.FC = async () => { + fetchProjects(); + + return ( + <> + + }> + + + + + ); +}; + +export default ProjectCashFlowReport; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts index d607c48..542effe 100644 --- a/src/app/api/claims/actions.ts +++ b/src/app/api/claims/actions.ts @@ -21,7 +21,7 @@ export interface ClaimDetailTable { id: number; invoiceDate: Date; description: string; - project: ProjectCombo; + project: number; amount: number; supportingDocumentName: string; oldSupportingDocument: SupportingDocument; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts new file mode 100644 index 0000000..e0db4c4 --- /dev/null +++ b/src/app/api/reports/actions.ts @@ -0,0 +1,18 @@ +"use server"; + +import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; +import { EX02ProjectCashFlowReportRequest } from "."; +import { BASE_API_URL } from "@/config/api"; + +export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts new file mode 100644 index 0000000..6baa7aa --- /dev/null +++ b/src/app/api/reports/index.ts @@ -0,0 +1,8 @@ +// EX02 - Project Cash Flow Report +export interface EX02ProjectCashFlowReportFilter { + project: string[]; +} + +export interface EX02ProjectCashFlowReportRequest { + projectId: number; +} \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index d4c71b6..f0cc208 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -20,4 +20,20 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str return true } } +} + +function s2ab(s: string) { + var buf = new ArrayBuffer(s.length); + var view = new Uint8Array(buf); + for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; + return buf; +} + +export const downloadFile = (blob: Blob | string, type: string, filename: string) => { + + const url = URL.createObjectURL(typeof blob === "string" ? new Blob([blob], { type: type }) : blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", filename); + link.click(); } \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 0aaa798..fa11529 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -14,9 +14,9 @@ export const serverFetch: typeof fetch = async (input, init) => { ...init?.headers, ...(accessToken ? { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } : {}), }, }); @@ -56,6 +56,26 @@ export async function serverFetchWithNoContent(...args: FetchParams) { } } +export async function serverFetchBlob(...args: FetchParams) { + const response = await serverFetch(...args); + + if (response.ok) { + console.log(response) + const blob = await response.blob() + const blobText = await blob.text(); + const blobType = await blob.type; + return {filename: response.headers.get("filename"), blobText: blobText, blobType: blobType}; + } else { + switch (response.status) { + case 401: + signOutUser(); + default: + console.error(await response.text()); + throw Error("Something went wrong fetching data in server."); + } + } +} + export const signOutUser = () => { const headersList = headers(); const referer = headersList.get("referer"); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 314ea63..3d6123a 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -28,6 +28,7 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/position": "Position", "/settings/position/new": "Create Position", "/settings/salarys": "Salary", + "/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report", }; const Breadcrumb = () => { diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx index db74447..54a82a5 100644 --- a/src/components/ClaimDetail/ClaimDetail.tsx +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -75,9 +75,10 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { const formData = new FormData() formData.append("expenseType", data.expenseType) data.addClaimDetails.forEach((claimDetail) => { + console.log(claimDetail) formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) - formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id)) + formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project)) formData.append("addClaimDetailDescriptions", claimDetail.description) formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) diff --git a/src/components/ClaimDetail/ClaimFormInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx index 24807bd..6aac620 100644 --- a/src/components/ClaimDetail/ClaimFormInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC = ({ flex: 1, editable: true, type: "singleSelect", - getOptionLabel: (value: any) => { + getOptionLabel: (value: ProjectCombo) => { return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; }, - getOptionValue: (value: any) => value, + getOptionValue: (value: ProjectCombo) => value.id, valueOptions: () => { const options = projectCombo ?? [] if (options.length === 0) { options.push({ id: -1, code: "", name: "No Projects" }) } - return options; + + return options as ProjectCombo[]; }, valueGetter: (params) => { - return params.value ?? projectCombo[0].id ?? -1 + return params.value ?? projectCombo[0] ?? { id: -1, code: "", name: "No Projects" } as ProjectCombo }, }, { diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index c0ab01f..304993a 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -50,12 +50,12 @@ const ClaimSearch: React.FC = ({ claims }) => { const columns = useMemo[]>( () => [ - // { - // name: "action", - // label: t("Actions"), - // onClick: onClaimClick, - // buttonIcon: , - // }, + { + name: "id", + label: t("Details"), + onClick: onClaimClick, + buttonIcon: , + }, { name: "created", label: t("Creation Date"), type: "date" }, { name: "code", label: t("Claim Code") }, // { name: "project", label: t("Related Project Name") }, diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx new file mode 100644 index 0000000..d0bc75f --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports"; +import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; + +interface Props { + projects: ProjectResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { + const { t } = useTranslation(); + const projectCombo = projects.map(project => `${project.code} - ${project.name}`) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project"), paramName: "project", type: "select", options: projectCombo}, + ], + [t], + ); + + return ( + <> + { + const projectIndex = projectCombo.findIndex(project => project === query.project) + const response = await fetchEX02ProjectCashFlowReport({projectId: projects[projectIndex].id}) + console.log(response) + if (response) { + downloadFile(response.blobText, response.blobType, response.filename!!) + } + + // const url = URL.createObjectURL(response.blob); + // const link = document.createElement("a"); + // link.href = url; + // link.setAttribute("download", "abc.xlsx"); + // link.click(); + }} + /> + + ); +}; + +export default GenerateEX02ProjectCashFlowReport; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx new file mode 100644 index 0000000..1792221 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx @@ -0,0 +1,38 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const GenerateEX02ProjectCashFlowReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateEX02ProjectCashFlowReportLoading; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx new file mode 100644 index 0000000..5bf1089 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import GenerateEX02ProjectCashFlowReportLoading from "./GenerateEX02ProjectCashFlowReportLoading"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "./GenerateEX02ProjectCashFlowReport"; + +interface SubComponents { + Loading: typeof GenerateEX02ProjectCashFlowReportLoading; +} + +const GenerateEX02ProjectCashFlowReportWrapper: React.FC & SubComponents = async () => { + const projects = await fetchProjects(); + + return ; +}; + +GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading; + +export default GenerateEX02ProjectCashFlowReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/index.ts b/src/components/GenerateEX02ProjectCashFlowReport/index.ts new file mode 100644 index 0000000..b547e33 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateEX02ProjectCashFlowReportWrapper"; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 9016052..aef7c0d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -110,6 +110,7 @@ const navigationItems: NavigationItem[] = [ {icon: , label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, + {icon: , label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"}, ], }, { diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json index 51f657c..b5b5e42 100644 --- a/src/i18n/en/claim.json +++ b/src/i18n/en/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "Please ensure the projects are selected", "Please ensure the amount are correct": "Please ensure the amount are correct", + "Details": "Details", "Description": "Description", "Actions": "Actions" } \ No newline at end of file diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json new file mode 100644 index 0000000..e7e61fb --- /dev/null +++ b/src/i18n/en/report.json @@ -0,0 +1,3 @@ +{ + "Project": "Project" +} \ No newline at end of file diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json index 92e5b7e..9e2de7e 100644 --- a/src/i18n/zh/claim.json +++ b/src/i18n/zh/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "請確保所有項目欄位已選擇", "Please ensure the amount are correct": "請確保所有金額輸入正確", + "Details": "詳請", "Description": "描述", "Actions": "行動" } \ No newline at end of file diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json new file mode 100644 index 0000000..a6257cf --- /dev/null +++ b/src/i18n/zh/report.json @@ -0,0 +1,3 @@ +{ + "Project": "項目" +} \ No newline at end of file From 1f8a4e18c933004d12dd6a62f0e7e2de3d472187 Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Mon, 29 Apr 2024 16:48:28 +0800 Subject: [PATCH 08/22] add project resource summary page --- .../dashboard/ProjectResourceSummary/page.tsx | 29 + .../dashboard/StaffUtilization/page.tsx | 2 +- src/app/api/clientprojects/index.ts | 6 +- src/app/api/resourcesummary/index.ts | 53 ++ .../CustomDatagrid/CustomDatagrid.tsx | 18 +- .../NavigationContent/NavigationContent.tsx | 6 + .../ProgressByClient/ProgressByClient.tsx | 3 +- .../ProgressByClientSearch.tsx | 21 +- .../ProgressByTeam/ProgressByTeam.tsx | 3 +- .../ProjectResourceSummary.tsx | 548 ++++++++++++++++++ .../ProjectResourceSummary/index.ts | 1 + .../ProjectResourceSummarySearch.tsx | 75 +++ .../ProjectResourceSummarySearchLoading.tsx | 40 ++ .../ProjectResourceSummarySearchWrapper.tsx | 20 + .../ProjectResourceSummarySearch/index.ts | 1 + 15 files changed, 814 insertions(+), 12 deletions(-) create mode 100644 src/app/(main)/dashboard/ProjectResourceSummary/page.tsx create mode 100644 src/app/api/resourcesummary/index.ts create mode 100644 src/components/ProjectResourceSummary/ProjectResourceSummary.tsx create mode 100644 src/components/ProjectResourceSummary/index.ts create mode 100644 src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx create mode 100644 src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx create mode 100644 src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx create mode 100644 src/components/ProjectResourceSummarySearch/index.ts diff --git a/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx new file mode 100644 index 0000000..5dc2f77 --- /dev/null +++ b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import DashboardPage from "@/components/DashboardPage/DashboardPage"; +import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton"; +import { Suspense } from "react"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import Typography from "@mui/material/Typography"; +import StaffUtilizationComponent from "@/components/StaffUtilization"; +import ProjectResourceSummarySearch from "@/components/ProjectResourceSummarySearch"; +import { ResourceSummaryResult } from "@/app/api/resourcesummary"; + +export const metadata: Metadata = { + title: "Project Resource Summary", +}; + +const ProjectResourceSummary: React.FC = () => { + return ( + + + Project Resource Summary + + }> + + + + ); +}; +export default ProjectResourceSummary; diff --git a/src/app/(main)/dashboard/StaffUtilization/page.tsx b/src/app/(main)/dashboard/StaffUtilization/page.tsx index 2ddea02..87bb6c0 100644 --- a/src/app/(main)/dashboard/StaffUtilization/page.tsx +++ b/src/app/(main)/dashboard/StaffUtilization/page.tsx @@ -10,7 +10,7 @@ import Typography from "@mui/material/Typography"; import StaffUtilizationComponent from "@/components/StaffUtilization"; export const metadata: Metadata = { - title: "Project Status by Client", + title: "Staff Utilization", }; const StaffUtilization: React.FC = () => { diff --git a/src/app/api/clientprojects/index.ts b/src/app/api/clientprojects/index.ts index 5c65810..3eed422 100644 --- a/src/app/api/clientprojects/index.ts +++ b/src/app/api/clientprojects/index.ts @@ -27,7 +27,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 5, }, { - id: 1, + id: 2, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-001", @@ -35,7 +35,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 5, }, { - id: 1, + id: 3, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-002", @@ -43,7 +43,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 3, }, { - id: 1, + id: 4, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-003", diff --git a/src/app/api/resourcesummary/index.ts b/src/app/api/resourcesummary/index.ts new file mode 100644 index 0000000..ffaba69 --- /dev/null +++ b/src/app/api/resourcesummary/index.ts @@ -0,0 +1,53 @@ +import { cache } from "react"; + +export interface ResourceSummaryResult { + id: number; + projectCode: string; + projectName: string; + clientCode: string; + clientName: string; + clientCodeAndName: string; +} + +export const preloadProjects = () => { + fetchResourceSummary(); +}; + +export const fetchResourceSummary = cache(async () => { + return mockProjects; +}); + +const mockProjects: ResourceSummaryResult[] = [ + { + id: 1, + projectCode: 'C-1001-001', + projectName: 'Consultancy Project A', + clientCode: 'Client-001', + clientName: 'AAA Construction', + clientCodeAndName: 'Client-001 - AAA Construction', + }, + { + id: 2, + projectCode: 'C-1002-001', + projectName: 'Consultancy Project B', + clientCode: 'Client-001', + clientName: 'AAA Construction', + clientCodeAndName: 'Client-001 - AAA Construction', + }, + { + id: 3, + projectCode: 'C-1003-001', + projectName: 'Consultancy Project C', + clientCode: 'Client-002', + clientName: 'BBB Construction', + clientCodeAndName: 'Client-002 - BBB Construction', + }, + { + id: 4, + projectCode: 'C-1004-001', + projectName: 'Consultancy Project D', + clientCode: 'Client-002', + clientName: 'BBB Construction', + clientCodeAndName: 'Client-002 - BBB Construction', + }, +]; diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index 314ba6c..4867623 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/src/components/CustomDatagrid/CustomDatagrid.tsx @@ -1,7 +1,7 @@ "use client"; import * as React from "react"; import { Card, CardHeader, CardContent, SxProps, Theme } from "@mui/material"; -import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; +import { DataGrid, GridColDef, GridRowSelectionModel, GridColumnGroupingModel} from "@mui/x-data-grid"; import { darken, lighten, styled } from "@mui/material/styles"; import { useState } from "react"; @@ -19,6 +19,8 @@ interface CustomDatagridProps { newSelectionModel: GridRowSelectionModel, ) => void; selectionModel?: any; + columnGroupingModel?: any; + pageSize?:any; } const CustomDatagrid: React.FC = ({ @@ -32,6 +34,8 @@ const CustomDatagrid: React.FC = ({ checkboxSelection, // Destructure the new prop onRowSelectionModelChange, // Destructure the new prop selectionModel, + columnGroupingModel, + pageSize, ...props }) => { const modifiedColumns = columns.map((column) => { @@ -193,6 +197,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -222,6 +228,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -251,6 +259,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} style={{ marginRight: 20 }} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, @@ -282,8 +292,10 @@ const CustomDatagrid: React.FC = ({ style={{ marginRight: 0 }} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ - pagination: { paginationModel: { pageSize: 10 } }, + pagination: { paginationModel: { pageSize: pageSize ?? 10 } }, }} className="customDataGrid" sx={{ @@ -293,7 +305,7 @@ const CustomDatagrid: React.FC = ({ "& .MuiDataGrid-cell:hover": { color: "primary.main", }, - height: 300, + height: dataGridHeight ?? 300, "& .MuiDataGrid-root": { overflow: "auto", }, diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index aef7c0d..1c1f12d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -31,6 +31,7 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; import GroupIcon from '@mui/icons-material/Group'; import BusinessIcon from '@mui/icons-material/Business'; +import ViewWeekIcon from '@mui/icons-material/ViewWeek'; import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; @@ -78,6 +79,11 @@ const navigationItems: NavigationItem[] = [ label: "Staff Utilization", path: "/dashboard/StaffUtilization", }, + { + icon: , + label: "Project Resource Summary", + path: "/dashboard/ProjectResourceSummary", + } ], }, { diff --git a/src/components/ProgressByClient/ProgressByClient.tsx b/src/components/ProgressByClient/ProgressByClient.tsx index 6815f73..6750392 100644 --- a/src/components/ProgressByClient/ProgressByClient.tsx +++ b/src/components/ProgressByClient/ProgressByClient.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Card, CardHeader } from "@mui/material"; import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; -import ReactApexChart from "react-apexcharts"; +// import ReactApexChart from "react-apexcharts"; import { ApexOptions } from "apexcharts"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import ReportProblemIcon from "@mui/icons-material/ReportProblem"; @@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByClient: React.FC = () => { const [activeTab, setActiveTab] = useState("financialSummary"); diff --git a/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx b/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx index 553a306..2062919 100644 --- a/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx +++ b/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx @@ -1,11 +1,13 @@ "use client"; import { ProjectResult } from "@/app/api/projects"; -import React, { useMemo, useState } from "react"; +import React, { useMemo, useState, useCallback } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import { ClientProjectResult } from "@/app/api/clientprojects"; +import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; interface Props { projects: ClientProjectResult[]; @@ -15,7 +17,7 @@ type SearchParamNames = keyof SearchQuery; const ProgressByClientSearch: React.FC = ({ projects }) => { const { t } = useTranslation("projects"); - + const searchParams = useSearchParams() // If project searching is done on the server-side, then no need for this. const [filteredProjects, setFilteredProjects] = useState(projects); @@ -27,15 +29,28 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { [t], ); + const onTaskClick = useCallback((clientProjectResult: ClientProjectResult) => { + const params = new URLSearchParams(searchParams.toString()) + params.set("id", clientProjectResult.id.toString()) + console.log(clientProjectResult) +}, []); + const columns = useMemo[]>( () => [ + { + name: "id", + label: t("Details"), + onClick: onTaskClick, + buttonIcon: , + }, { name: "clientCode", label: t("Client Code") }, { name: "clientName", label: t("Client Name") }, { name: "SubsidiaryClientCode", label: t("Subsidiary Code") }, { name: "SubsidiaryClientName", label: t("Subisdiary") }, { name: "NoOfProjects", label: t("No. of Projects") }, ], - [t], + [onTaskClick, t], + // [t], ); return ( diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx index bee6f73..164a00d 100644 --- a/src/components/ProgressByTeam/ProgressByTeam.tsx +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Card, CardHeader } from "@mui/material"; import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; -import ReactApexChart from "react-apexcharts"; +// import ReactApexChart from "react-apexcharts"; import { ApexOptions } from "apexcharts"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import ReportProblemIcon from "@mui/icons-material/ReportProblem"; @@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; import { Suspense } from "react"; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByTeam: React.FC = () => { const [activeTab, setActiveTab] = useState("financialSummary"); diff --git a/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx b/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx new file mode 100644 index 0000000..57d25d2 --- /dev/null +++ b/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx @@ -0,0 +1,548 @@ +"use client"; +import * as React from "react"; +import Grid from "@mui/material/Grid"; +import { useState, useEffect, useMemo } from "react"; +import Paper from "@mui/material/Paper"; +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; +import { Card, CardHeader } from "@mui/material"; +import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; +import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; +import ReactApexChart from "react-apexcharts"; +import { ApexOptions } from "apexcharts"; +import { DataGrid, GridColDef, GridRowSelectionModel} from "@mui/x-data-grid"; +import ReportProblemIcon from "@mui/icons-material/ReportProblem"; +import dynamic from "next/dynamic"; +import "../../app/global.css"; +import { AnyARecord, AnyCnameRecord } from "dns"; +import SearchBox, { Criterion } from "../SearchBox"; +import ProgressByClientSearch from "@/components/ProgressByClientSearch"; +import { Suspense } from "react"; +import { getPossibleInstrumentationHookFilenames } from "next/dist/build/utils"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Collapse from '@mui/material/Collapse'; +import IconButton from '@mui/material/IconButton'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + + +const ProjectResourceSummary: React.FC = () => { + const [SearchCriteria, setSearchCriteria] = React.useState({}); + const { t } = useTranslation("dashboard"); + const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [projectName, setProjectName]:any = React.useState("NA"); + const [projectFee, setProjectFee]:any = React.useState(0); + const [status, setStatus]:any = React.useState("NA"); + const [plannedResources, setPlannedResources]:any = React.useState(0); + const [actualResourcesSpent, setActualResourcesSpent]:any = React.useState(0); + const [remainingResources, setRemainingResources]:any = React.useState(0); + + function createData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any, task:any) { + return { + stage, + taskCount, + g1Planned, + g1Actual, + g2Planned, + g2Actual, + g3Planned, + g3Actual, + g4Planned, + g4Actual, + g5Planned, + g5Actual, + totalPlanned, + totalActual, + task:task + } + } + + function createTaskData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any) { + return { + stage, + taskCount, + g1Planned, + g1Actual, + g2Planned, + g2Actual, + g3Planned, + g3Actual, + g4Planned, + g4Actual, + g5Planned, + g5Actual, + totalPlanned, + totalActual, + } + } + + const task1Rows:any = [ + {stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"}, + {stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"}, + {stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"} + ] + + const task2Rows:any = [ + ] + + const task3Rows:any = [ + ] + + const task4Rows:any = [ + ] + + const task5Rows:any = [ + ] + + const task6Rows:any = [ + ] + + const rows = [ + createData("Stage 1 - Design & Cost Planning / Estimating","5","576.00","576.00","192.00", "180.00", "144.00", "140.00", "38.40", "38.00", "9.60", "9.75", "960.00", "943.75",task1Rows), + createData("Stage 2 - Tender Documentation","11", "384.00", "382.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "620.00",task2Rows), + createData("Stage 3 - Tender Analysis & Report & Contract Documentation","7", "384.00", "300.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "538.00",task3Rows), + createData("Stage 4 - Construction", "13", "480.00", "400.00", "160.00", "160.00", "120.00", "128.00", "32.00", "25.00", "8.00", "3.00", "800.00", "716.00",task4Rows), + createData("Stage 5 - Miscellaneous", "4", "96.00", "-", "32.00", "-", "24.00", "-0", "6.40", "-", "1.600", "-", "160.00", "-",task5Rows), + createData("","Total", "1920.00", "1658.00", "640.00", "600.00", "480.00", "426.00", "128.00", "113.00", "32.00", "20.75", "3,200.00", "2817.75",task6Rows), + ]; + + // const taskRows = [ + // createTaskData("1.1 Preparation of preliminary...","-","-","172.00","-","54.00","-","42.00","-","12.00","-","3.00","-","283.00"), + // ]; + + function Row(props:any) { + const { row } = props; + const [open, setOpen] = React.useState(false); + + return ( + + *': { borderBottom: 'unset' } }}> + + {row.task.length > 0 && ( + setOpen(!open)} + > + {open ? : } + + )} + + {row.stage} + {row.taskCount} + {row.g1Planned} + {row.g1Actual} + {row.g2Planned} + {row.g2Actual} + {row.g3Planned} + {row.g3Actual} + {row.g4Planned} + {row.g4Actual} + {row.g5Planned} + {row.g5Actual} + {row.totalPlanned} + {row.totalActual} + + {row.task.map((taskRow:any) => ( + <> + + + + + + + + + {taskRow.stage} + + + + + {taskRow.taskCount} + + + + + {taskRow.g1Planned} + + + + + {taskRow.g1Actual} + + + + + {taskRow.g2Planned} + + + + + {taskRow.g2Actual} + + + + + {taskRow.g3Planned} + + + + + {taskRow.g3Actual} + + + + + {taskRow.g4Planned} + + + + + {taskRow.g4Actual} + + + + + {taskRow.g5Planned} + + + + + {taskRow.g5Actual} + + + + + {taskRow.totalPlanned} + + + + + {taskRow.totalActual} + + + + + ))} + {/* + + + + {row.task.map((taskRow:any) => ( + + + {taskRow.stage} + {taskRow.taskCount} + {taskRow.g1Planned} + {taskRow.g1Actual} + {taskRow.g2Planned} + {taskRow.g2Actual} + {taskRow.g3Planned} + {taskRow.g3Actual} + {taskRow.g4Planned} + {taskRow.g4Actual} + {taskRow.g5Planned} + {taskRow.g5Actual} + {taskRow.totalPlanned} + {taskRow.totalActual} + + ))} + + + + */} + + ); + } + + useEffect(() => { + setProjectName("C-1001-001 - Consultancy Project A") + const fee = 2000000 + setProjectFee(fee.toLocaleString()) + setStatus("Within Budget / Overconsumption") + const plannedResourcesInt = 3200 + setPlannedResources(plannedResourcesInt.toLocaleString()) + const actualResourcesSpentInt = 2817.75 + setActualResourcesSpent(actualResourcesSpentInt.toLocaleString()) + const remainingResourcesInt = 382.25 + setRemainingResources(remainingResourcesInt.toLocaleString()) + }, []) + + const projectResourcesRows = [ + {id: 1,stage:"Stage 1 - Design & Cost Planning / Estimating",taskCount:"5",g1Planned:"576.00",g1Actual:"576.00",g2Planned:"192.00", g2Actual:"180.00", g3Planned:"144.00", g3Actual:"140.00", g4Planned: "38.40", g4Actual:"38S.00", g5Planned:"9.60", g5Actual:"9.75", totalPlanned:"960.00", totalActual:"943.75"}, + {id: 2,stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {id: 3,stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {id: 4,stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"}, + {id: 5,stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"}, + {id: 6,stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"}, + {id: 7,stage:"Stage 2 - Tender Documentation",taskCount:"11",g1Planned:"384.00",g1Actual:"382.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"620.00"}, + {id: 8,stage:"Stage 3 - Tender Analysis & Report & Contract Documentation",taskCount:"7",g1Planned:"384.00",g1Actual:"300.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"538.00"}, + {id: 9,stage:"Stage 4 - Construction",taskCount:"13",g1Planned:"480.00",g1Actual:"400.00",g2Planned:"160.00", g2Actual:"160.00", g3Planned:"120.00", g3Actual:"128.00", g4Planned: "32.00", g4Actual:"25.00", g5Planned:"8.00", g5Actual:"3.00", totalPlanned:"800.00", totalActual:"716.00"}, + {id: 10,stage:"Stage 5 - Miscellaneous",taskCount:"4",g1Planned:"96.00",g1Actual:"-",g2Planned:"32.00", g2Actual:"-", g3Planned:"24.00", g3Actual:"-0", g4Planned: "6.40", g4Actual:"-", g5Planned:"1.600", g5Actual:"-", totalPlanned:"160.00", totalActual:"-"}, + {id: 11,stage:"",taskCount:"Total",g1Planned:"1920.00",g1Actual:"1658.00",g2Planned:"640.00", g2Actual:"600.00", g3Planned:"480.00", g3Actual:"426.00", g4Planned: "128.00", g4Actual:"113.00", g5Planned:"32.00", g5Actual:"20.75", totalPlanned:"3,200.00", totalActual:"2817.75"}, + ] + +const columns2 = [ + { + id: 'stage', + field: 'stage', + headerName: "Stage", + flex: 2, + }, + { + id: 'taskCount', + field: 'taskCount', + headerName: "Task Count", + flex: 0.5, + }, + { + id: 'g1Planned', + field: 'g1Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g1Actual', + field: 'g1Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g2Planned', + field: 'g2Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g2Actual', + field: 'g2Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g3Planned', + field: 'g3Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g3Actual', + field: 'g3Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g4Planned', + field: 'g4Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g4Actual', + field: 'g4Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g5Planned', + field: 'g5Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g5Actual', + field: 'g5Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'totalPlanned', + field: 'totalPlanned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'totalActual', + field: 'totalActual', + headerName: "Actual", + flex: 0.7, + }, +]; + + const columnGroupingModel = [ + { + groupId: 'G1', + children: [{ field: 'g1Planned' },{ field: 'g1Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G2', + children: [{ field: 'g2Planned' },{ field: 'g2Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G3', + children: [{ field: 'g3Planned' },{ field: 'g3Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G4', + children: [{ field: 'g4Planned' },{ field: 'g4Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G5', + children: [{ field: 'g5Planned' },{ field: 'g5Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'Total', + children: [{ field: 'totalPlanned' },{ field: 'totalActual' }], + headerClassName: 'totalGroupColor', + }, + ]; + + return ( + + + +

+
+
+ + Project + +
+
+ {projectName} +
+
+
+
+ + Project Fee + +
+
+ HKD {projectFee} +
+
+
+
+ + Status + +
+
+ {status} +
+
+
+
+ + Planned Resources + +
+
+ {plannedResources} Manhours +
+
+
+
+ + Actual Resources Spent + +
+
+ {actualResourcesSpent} Manhours +
+
+
+
+ + Remaining Resources + +
+
+ {remainingResources} Manhours +
+
+
+ {/*
+ +
*/} +
+ + + + + + + + + G1 + + + G2 + + + G3 + + + G4 + + + G5 + + + Total + + + + + Stage + Task Count + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + + + + {rows.map((row) => ( + + ))} + +
+
+
+ + + ); +}; + +export default ProjectResourceSummary; diff --git a/src/components/ProjectResourceSummary/index.ts b/src/components/ProjectResourceSummary/index.ts new file mode 100644 index 0000000..056f6f9 --- /dev/null +++ b/src/components/ProjectResourceSummary/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectResourceSummary"; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx new file mode 100644 index 0000000..d8efb61 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { ProjectResult } from "@/app/api/projects"; +import React, { useMemo, useState, useCallback } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import { ResourceSummaryResult } from "@/app/api/resourcesummary"; +import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import ProjectResourceSummary from "@/components/ProjectResourceSummary"; +import ArticleIcon from '@mui/icons-material/Article'; + +interface Props { + projects: ResourceSummaryResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + + +const ProjectResourceSummarySearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + const searchParams = useSearchParams() + // If project searching is done on the server-side, then no need for this. + const [filteredProjects, setFilteredProjects] = useState(projects); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "Project Code", paramName: "projectCode", type: "text" }, + { label: "Project Name", paramName: "projectName", type: "text" }, + { label: "Client Code", paramName: "clientCode", type: "text" }, + { label: "Client Name", paramName: "clientName", type: "text" }, + ], + [t], + ); + + const onTaskClick = useCallback((resourceSummaryResult: ResourceSummaryResult) => { + console.log(resourceSummaryResult) + }, []); + + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("View"), + onClick: onTaskClick, + buttonIcon: , + }, + { name: "projectCode", label: t("Project Code") }, + { name: "projectName", label: t("Project Name") }, + { name: "clientCodeAndName", label: t("Client Code And Name") }, + ], + [onTaskClick, t], + // [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + + items={filteredProjects} + columns={columns} + /> + + + ); +}; + +export default ProjectResourceSummarySearch; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx new file mode 100644 index 0000000..b6d4bc1 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const ProjectResourceSummarySearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectResourceSummarySearchLoading; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx new file mode 100644 index 0000000..068debc --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx @@ -0,0 +1,20 @@ +import { fetchResourceSummary } from "@/app/api/resourcesummary"; +import React from "react"; +import ProjectResourceSummarySearch from "./ProjectResourceSummarySearch"; +import ProjectResourceSummarySearchLoading from "./ProjectResourceSummarySearchLoading"; + +interface SubComponents { + Loading: typeof ProjectResourceSummarySearchLoading; +} + +const ProjectResourceSummarySearchWrapper: React.FC & SubComponents = async () => { + const clentprojects = await fetchResourceSummary(); + + return ; +}; + +ProjectResourceSummarySearchWrapper.Loading = ProjectResourceSummarySearchLoading; + +export default ProjectResourceSummarySearchWrapper; + + diff --git a/src/components/ProjectResourceSummarySearch/index.ts b/src/components/ProjectResourceSummarySearch/index.ts new file mode 100644 index 0000000..98ec034 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectResourceSummarySearchWrapper"; From 529c26d114239b6e9ab810a72df99c7c551046f6 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 18:19:35 +0800 Subject: [PATCH 09/22] update gen EX02 report --- src/app/api/reports/actions.ts | 9 ++++++++- src/app/utils/commonUtil.ts | 12 ++---------- src/app/utils/fetchUtil.ts | 12 +++++++----- .../GenerateEX02ProjectCashFlowReport.tsx | 14 ++++---------- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index e0db4c4..6a7e70c 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -4,8 +4,13 @@ import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; import { EX02ProjectCashFlowReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; +export interface FileResponse { + filename: string; + blobValue: Uint8Array; +} + export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { - const reportBlob = await serverFetchBlob( + const reportBlob = await serverFetchBlob( `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, { method: "POST", @@ -14,5 +19,7 @@ export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowRe }, ); + console.log(reportBlob) + return reportBlob }; \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index f0cc208..72d4a56 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -22,16 +22,8 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str } } -function s2ab(s: string) { - var buf = new ArrayBuffer(s.length); - var view = new Uint8Array(buf); - for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; - return buf; -} - -export const downloadFile = (blob: Blob | string, type: string, filename: string) => { - - const url = URL.createObjectURL(typeof blob === "string" ? new Blob([blob], { type: type }) : blob); +export const downloadFile = (blobData: Uint8Array, filename: string) => { + const url = URL.createObjectURL(new Blob([blobData])); const link = document.createElement("a"); link.href = url; link.setAttribute("download", filename); diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index fa11529..7e9ab5f 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -8,6 +8,7 @@ export const serverFetch: typeof fetch = async (input, init) => { const session = await getServerSession(authOptions); const accessToken = session?.accessToken; + console.log(accessToken) return fetch(input, { ...init, headers: { @@ -56,15 +57,16 @@ export async function serverFetchWithNoContent(...args: FetchParams) { } } -export async function serverFetchBlob(...args: FetchParams) { +export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { console.log(response) - const blob = await response.blob() - const blobText = await blob.text(); - const blobType = await blob.type; - return {filename: response.headers.get("filename"), blobText: blobText, blobType: blobType}; + // const blob = await response.blob() + // const blobText = await blob.text(); + // const blobType = await blob.type; + const blobValue = (await response.body?.getReader().read())!!.value!! + return {filename: response.headers.get("filename"), blobValue: blobValue} as T; } else { switch (response.status) { case 401: diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx index d0bc75f..43b5e5d 100644 --- a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -7,6 +7,7 @@ import { ProjectResult } from "@/app/api/projects"; import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports"; import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions"; import { downloadFile } from "@/app/utils/commonUtil"; +import { BASE_API_URL } from "@/config/api"; interface Props { projects: ProjectResult[]; @@ -21,7 +22,7 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { const searchCriteria: Criterion[] = useMemo( () => [ - { label: t("Project"), paramName: "project", type: "select", options: projectCombo}, + { label: t("Project"), paramName: "project", type: "select", options: projectCombo }, ], [t], ); @@ -32,17 +33,10 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { criteria={searchCriteria} onSearch={async (query) => { const projectIndex = projectCombo.findIndex(project => project === query.project) - const response = await fetchEX02ProjectCashFlowReport({projectId: projects[projectIndex].id}) - console.log(response) + const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) if (response) { - downloadFile(response.blobText, response.blobType, response.filename!!) + downloadFile(new Uint8Array(response.blobValue), response.filename!!) } - - // const url = URL.createObjectURL(response.blob); - // const link = document.createElement("a"); - // link.href = url; - // link.setAttribute("download", "abc.xlsx"); - // link.click(); }} /> From 8771ce179a13deaa46ae83444f38edadade85682 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 18:35:54 +0800 Subject: [PATCH 10/22] update report (but sometimes cannot open) --- src/app/api/reports/actions.ts | 2 -- src/app/utils/fetchUtil.ts | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 6a7e70c..be74c6f 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -19,7 +19,5 @@ export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowRe }, ); - console.log(reportBlob) - return reportBlob }; \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 7e9ab5f..8f563e2 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -16,7 +16,7 @@ export const serverFetch: typeof fetch = async (input, init) => { ...(accessToken ? { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" + Accept: "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } : {}), }, @@ -61,12 +61,14 @@ export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { - console.log(response) // const blob = await response.blob() // const blobText = await blob.text(); // const blobType = await blob.type; - const blobValue = (await response.body?.getReader().read())!!.value!! - return {filename: response.headers.get("filename"), blobValue: blobValue} as T; + const readBody = await response.body?.getReader().read() + const bodyValue = readBody!!.value!! + + console.log(bodyValue) + return {filename: response.headers.get("filename"), blobValue: bodyValue} as T; } else { switch (response.status) { case 401: From 1f69f582be430bf36acfaf40f2e569c8b29afe3f Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 30 Apr 2024 12:08:33 +0800 Subject: [PATCH 11/22] fix sometimes cannot open the file --- src/app/utils/fetchUtil.ts | 39 +++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 8f563e2..9fa878e 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -61,14 +61,43 @@ export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { + const body = response.body + // console.log(body) + // console.log(body?.tee()[0].getReader()) + + + const reader = body?.getReader() + let finalUInt8Array = new Uint8Array() + let done = false + + // Read some more, and call this function again + while (!done) { + // Result objects contain two properties: + // done - true if the stream has already given you all its data. + // value - some data. Always undefined when done is true. + const read = await reader?.read() + + if (read?.done) { + done = true + } else { + // value for fetch streams is a Uint8Array + finalUInt8Array = new Uint8Array(read?.value.length!!) + finalUInt8Array.set(read?.value!!) + } + } + + // const bodyRead = await reader?.read() + // const bodyValue = bodyRead?.value + // const blob = await response.blob() // const blobText = await blob.text(); // const blobType = await blob.type; - const readBody = await response.body?.getReader().read() - const bodyValue = readBody!!.value!! - - console.log(bodyValue) - return {filename: response.headers.get("filename"), blobValue: bodyValue} as T; + + // console.log(bodyReader) + // console.log(finalUInt8Array) + // console.log(bodyValue) + + return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; } else { switch (response.status) { case 401: From 1c3cfafe5bb732fcf0cfc72c92dc102b114d92e8 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 30 Apr 2024 14:46:26 +0800 Subject: [PATCH 12/22] update --- src/app/utils/fetchUtil.ts | 36 ++++++++++++++----- .../GenerateEX02ProjectCashFlowReport.tsx | 13 ++++--- src/components/SearchBox/SearchBox.tsx | 3 +- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 9fa878e..4c96d1e 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -70,23 +70,41 @@ export async function serverFetchBlob(...args: FetchParams) { let finalUInt8Array = new Uint8Array() let done = false - // Read some more, and call this function again while (!done) { - // Result objects contain two properties: - // done - true if the stream has already given you all its data. - // value - some data. Always undefined when done is true. const read = await reader?.read() + // version 1 if (read?.done) { done = true } else { - // value for fetch streams is a Uint8Array - finalUInt8Array = new Uint8Array(read?.value.length!!) - finalUInt8Array.set(read?.value!!) + const tempUInt8Array = new Uint8Array(finalUInt8Array.length + read?.value.length!!) + tempUInt8Array.set(finalUInt8Array) + tempUInt8Array.set(read?.value!!, finalUInt8Array.length) + finalUInt8Array = new Uint8Array(tempUInt8Array.length!!) + finalUInt8Array.set(tempUInt8Array) + + // console.log("1", finalUInt8Array) } } - // const bodyRead = await reader?.read() + // version 2 & return bodyRead + // const bodyRead = reader?.read().then(function processText({ done, value }): any { + // // Result objects contain two properties: + // // done - true if the stream has already given you all its data. + // // value - some data. Always undefined when done is true. + // if (done) { + // console.log("Stream complete"); + // return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T;; + // } + + // // value for fetch streams is a Uint8Array + // finalUInt8Array = new Uint8Array(value.length) + // finalUInt8Array.set(value) + + // console.log(finalUInt8Array) + // // Read some more, and call this function again + // return reader.read().then(processText); + // }) // const bodyValue = bodyRead?.value // const blob = await response.blob() @@ -94,7 +112,7 @@ export async function serverFetchBlob(...args: FetchParams) { // const blobType = await blob.type; // console.log(bodyReader) - // console.log(finalUInt8Array) + // console.log("2", finalUInt8Array) // console.log(bodyValue) return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx index 43b5e5d..7aec1c2 100644 --- a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -22,7 +22,7 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { const searchCriteria: Criterion[] = useMemo( () => [ - { label: t("Project"), paramName: "project", type: "select", options: projectCombo }, + { label: t("Project"), paramName: "project", type: "select", options: projectCombo, needAll: false}, ], [t], ); @@ -32,10 +32,13 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { { - const projectIndex = projectCombo.findIndex(project => project === query.project) - const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) - if (response) { - downloadFile(new Uint8Array(response.blobValue), response.filename!!) + + if (query.project.length > 0 && query.project.toLocaleLowerCase() !== "all") { + const projectIndex = projectCombo.findIndex(project => project === query.project) + const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } } }} /> diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 26914fe..5ea1690 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -36,6 +36,7 @@ interface TextCriterion extends BaseCriterion { interface SelectCriterion extends BaseCriterion { type: "select"; options: string[]; + needAll?: boolean; } interface DateRangeCriterion extends BaseCriterion { @@ -134,7 +135,7 @@ function SearchBox({ onChange={makeSelectChangeHandler(c.paramName)} value={inputs[c.paramName]} > - {t("All")} + {!(c.needAll === false) && {t("All")}} {c.options.map((option, index) => ( {t(option)} From 2fd5beba0a4fa169fe552e7e5f27ceaf7ecaa981 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 30 Apr 2024 17:03:52 +0800 Subject: [PATCH 13/22] staff multiple skills --- src/components/CreateStaff/CreateStaff.tsx | 2 +- .../CustomInputForm/CustomInputForm.tsx | 68 ++++++++++++++++++- src/components/EditStaff/EditStaff.tsx | 29 ++++++-- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/components/CreateStaff/CreateStaff.tsx b/src/components/CreateStaff/CreateStaff.tsx index 312f18e..94e7f71 100644 --- a/src/components/CreateStaff/CreateStaff.tsx +++ b/src/components/CreateStaff/CreateStaff.tsx @@ -190,7 +190,7 @@ const CreateStaff: React.FC = ({ Title }) => { { id: "skillSetId", label: t("Skillset"), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo || [], required: false, }, diff --git a/src/components/CustomInputForm/CustomInputForm.tsx b/src/components/CustomInputForm/CustomInputForm.tsx index 9497208..417c6f1 100644 --- a/src/components/CustomInputForm/CustomInputForm.tsx +++ b/src/components/CustomInputForm/CustomInputForm.tsx @@ -15,6 +15,7 @@ import { Checkbox, FormControlLabel, Button, + Chip, } from "@mui/material"; import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { darken, lighten, styled } from "@mui/material/styles"; @@ -31,6 +32,7 @@ import { useCallback, useEffect, useState } from "react"; import { Check, Close, RestartAlt } from "@mui/icons-material"; import { NumericFormat, NumericFormatProps } from "react-number-format"; import * as React from "react"; +import CancelIcon from "@mui/icons-material/Cancel"; interface Options { id: any; @@ -286,7 +288,7 @@ const CustomInputForm: React.FC = ({ ); } else if (field.type === "multiDate") { - console.log(dayjs(field.value)) + // console.log(dayjs(field.value)) return ( @@ -343,8 +345,6 @@ const CustomInputForm: React.FC = ({ id={field.id} value={value} onChange={(event) => { - console.log(event); - console.log(event.target); onChange(event.target.value); const newValue = event.target.value; const selectedOption = field.options?.find( @@ -379,6 +379,68 @@ const CustomInputForm: React.FC = ({ ); + } else if (field.type === "multiSelect-Obj") { + return ( + + + {field.label} + ( + + )} + /> + + + ); } else if (field.type === "numeric") { return ( diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index bcc7346..0d62e28 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -15,8 +15,15 @@ import { fetchSkillCombo } from "@/app/api/skill/actions"; import { fetchSalaryCombo } from "@/app/api/salarys/actions"; // import { Field } from "react-hook-form"; -interface dataType { - [key: string]: any; + +interface skill { + id: number; + name: string; + code: string; +} +interface skillObj { + id: number; + skill: skill; } interface Options { @@ -113,7 +120,9 @@ const EditStaff: React.FC = async () => { if (data) setGradeCombo(data.records); }); fetchSkillCombo().then((data) => { - if (data) setSkillCombo(data.records); + if (data) { + }setSkillCombo(data.records); + console.log(data.records) }); fetchSalaryCombo().then((data) => { if (data) setSalaryCombo(data.records); @@ -127,6 +136,10 @@ const EditStaff: React.FC = async () => { console.log(id) fetchStaffEdit(id).then((staff) => { console.log(staff.data); + const skillset = staff.data.skillset + console.log(skillset); + const skillIds = skillset.map((item: skillObj) => item.skill.id); + console.log(skillIds) const data = staff.data; ///////////////////// list 1 ///////////////////// const list1 = keyOrder1 @@ -181,15 +194,17 @@ const EditStaff: React.FC = async () => { label: t(`Grade`), type: "combo-Obj", options: gradeCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: data[key]?.id ?? "", }; case "skill": + console.log(skillIds) return { id: `${key}SetId`, label: t(`Skillset`), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: skillIds ?? [], + //array problem }; case "currentPosition": return { @@ -206,7 +221,7 @@ const EditStaff: React.FC = async () => { label: t(`Salary Point`), type: "combo-Obj", options: salaryCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: data[key]?.id ?? "", required: true, }; // case "hourlyRate": From 301bacdec669b2c7f70d3475007ae3ca7d2d9449 Mon Sep 17 00:00:00 2001 From: Wayne Date: Tue, 30 Apr 2024 18:41:20 +0900 Subject: [PATCH 14/22] Fetch assigned projects --- package-lock.json | 172 +++++++++++++++++- src/app/api/projects/index.ts | 14 ++ src/app/utils/fetchUtil.ts | 39 ++-- .../TimesheetModal/TimesheetModal.tsx | 5 +- .../TimesheetTable/EntryInputTable.tsx | 128 ++----------- .../TimesheetTable/TimesheetTable.tsx | 25 ++- .../UserWorkspacePage/AssignedProjects.tsx | 53 +----- .../UserWorkspacePage/ProjectGrid.tsx | 23 +-- .../UserWorkspacePage/UserWorkspacePage.tsx | 10 +- .../UserWorkspaceWrapper.tsx | 64 +------ 10 files changed, 265 insertions(+), 268 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84bf9b7..398e982 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@faker-js/faker": "^8.4.1", "@fontsource/inter": "^5.0.16", "@fontsource/plus-jakarta-sans": "^5.0.18", "@mui/icons-material": "^5.15.0", @@ -37,7 +38,8 @@ "react-select": "^5.8.0", "reactstrap": "^9.2.2", "styled-components": "^6.1.8", - "sweetalert2": "^11.10.3" + "sweetalert2": "^11.10.3", + "xlsx-js-style": "^1.2.0" }, "devDependencies": { "@types/lodash": "^4.14.202", @@ -1933,6 +1935,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", @@ -3530,6 +3547,21 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4150,6 +4182,26 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4236,6 +4288,26 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==", + "dependencies": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage/node_modules/commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4316,6 +4388,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5431,6 +5514,14 @@ "node": ">=0.8.x" } }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5476,6 +5567,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", + "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5599,6 +5695,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -7880,6 +7984,17 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8631,6 +8746,17 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -9853,6 +9979,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/workbox-background-sync": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", @@ -10238,6 +10380,34 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xlsx-js-style": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz", + "integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.3.8", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 0c7209c..7bfb067 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -59,6 +59,11 @@ export interface AssignedProject { endDate: string; }; }; + // Manhour info + hoursSpent: number; + hoursSpentOther: number; + hoursAllocated: number; + hoursAllocatedOther: number; } export const preloadProjects = () => { @@ -131,3 +136,12 @@ export const fetchProjectWorkNatures = cache(async () => { next: { tags: ["projectWorkNatures"] }, }); }); + +export const fetchAssignedProjects = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/assignedProjects`, + { + next: { tags: ["assignedProjects"] }, + }, + ); +}); diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 4c96d1e..5060991 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -8,16 +8,17 @@ export const serverFetch: typeof fetch = async (input, init) => { const session = await getServerSession(authOptions); const accessToken = session?.accessToken; - console.log(accessToken) + console.log(accessToken); return fetch(input, { ...init, headers: { ...init?.headers, ...(accessToken ? { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - } + Authorization: `Bearer ${accessToken}`, + Accept: + "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + } : {}), }, }); @@ -61,27 +62,28 @@ export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { - const body = response.body + const body = response.body; // console.log(body) // console.log(body?.tee()[0].getReader()) - - const reader = body?.getReader() - let finalUInt8Array = new Uint8Array() - let done = false + const reader = body?.getReader(); + let finalUInt8Array = new Uint8Array(); + let done = false; while (!done) { - const read = await reader?.read() + const read = await reader?.read(); // version 1 if (read?.done) { - done = true + done = true; } else { - const tempUInt8Array = new Uint8Array(finalUInt8Array.length + read?.value.length!!) - tempUInt8Array.set(finalUInt8Array) - tempUInt8Array.set(read?.value!!, finalUInt8Array.length) - finalUInt8Array = new Uint8Array(tempUInt8Array.length!!) - finalUInt8Array.set(tempUInt8Array) + const tempUInt8Array = new Uint8Array( + finalUInt8Array.length + read?.value.length!, + ); + tempUInt8Array.set(finalUInt8Array); + tempUInt8Array.set(read?.value!, finalUInt8Array.length); + finalUInt8Array = new Uint8Array(tempUInt8Array.length!); + finalUInt8Array.set(tempUInt8Array); // console.log("1", finalUInt8Array) } @@ -115,7 +117,10 @@ export async function serverFetchBlob(...args: FetchParams) { // console.log("2", finalUInt8Array) // console.log(bodyValue) - return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; + return { + filename: response.headers.get("filename"), + blobValue: finalUInt8Array, + } as T; } else { switch (response.status) { case 401: diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index 055e0d9..d6146b0 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -16,11 +16,13 @@ import { FormProvider, useForm } from "react-hook-form"; import { RecordTimesheetInput } from "@/app/api/timesheets/actions"; import dayjs from "dayjs"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { AssignedProject } from "@/app/api/projects"; interface Props { isOpen: boolean; onClose: () => void; timesheetType: "time" | "leave"; + assignedProjects: AssignedProject[]; } const modalSx: SxProps = { @@ -37,6 +39,7 @@ const TimesheetModal: React.FC = ({ isOpen, onClose, timesheetType, + assignedProjects, }) => { const { t } = useTranslation("home"); @@ -73,7 +76,7 @@ const TimesheetModal: React.FC = ({ marginBlock: 4, }} > - + - - + {Boolean(assignedProjects.length) && ( + + + + + )} - - - + {assignedProjects.length > 0 ? ( + <> + + + + + ) : ( + <> + + {t("You have no assigned projects!")} + + + )} ); }; From 6fdbe946101652d9058bccf43867d9b8578159cc Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 30 Apr 2024 18:13:43 +0800 Subject: [PATCH 16/22] update tasks (can edit now), staff, subsidiary, customer, user, rename customer, --- .../(main)/settings/customer/create/page.tsx | 4 +- .../(main)/settings/customer/edit/page.tsx | 4 +- src/app/(main)/tasks/create/page.tsx | 5 +- src/app/(main)/tasks/edit/page.tsx | 26 +++ src/app/(main)/tasks/page.tsx | 13 +- src/app/api/tasks/actions.ts | 30 ++- .../CreateTaskTemplate/CreateTaskTemplate.tsx | 191 +++++++++++------- src/components/CustomerDetail/index.ts | 1 - .../ContactInfo.tsx | 0 .../CustomerInfo.tsx | 0 .../CustomerSave.tsx} | 4 +- .../CustomerSaveWrapper.tsx} | 8 +- .../SubsidiaryAllocation.tsx | 0 src/components/CustomerSave/index.ts | 1 + src/components/StaffSearch/StaffSearch.tsx | 2 +- .../SubsidiaryDetailWrapper.tsx | 4 +- .../SubsidiarySearch/SubsidiarySearch.tsx | 2 +- .../TaskTemplateSearch/TaskTemplateSearch.tsx | 28 ++- src/components/TeamSearch/TeamSearch.tsx | 2 +- src/components/TransferList/TransferList.tsx | 2 +- src/components/UserSearch/UserSearch.tsx | 2 +- src/i18n/en/common.json | 2 + src/i18n/en/tasks.json | 27 +++ src/i18n/zh/common.json | 2 + src/i18n/zh/tasks.json | 27 +++ 25 files changed, 291 insertions(+), 96 deletions(-) create mode 100644 src/app/(main)/tasks/edit/page.tsx delete mode 100644 src/components/CustomerDetail/index.ts rename src/components/{CustomerDetail => CustomerSave}/ContactInfo.tsx (100%) rename src/components/{CustomerDetail => CustomerSave}/CustomerInfo.tsx (100%) rename src/components/{CustomerDetail/CustomerDetail.tsx => CustomerSave/CustomerSave.tsx} (99%) rename src/components/{CustomerDetail/CustomerDetailWrapper.tsx => CustomerSave/CustomerSaveWrapper.tsx} (73%) rename src/components/{CustomerDetail => CustomerSave}/SubsidiaryAllocation.tsx (100%) create mode 100644 src/components/CustomerSave/index.ts create mode 100644 src/i18n/en/tasks.json create mode 100644 src/i18n/zh/tasks.json diff --git a/src/app/(main)/settings/customer/create/page.tsx b/src/app/(main)/settings/customer/create/page.tsx index e0dc0e0..13b38eb 100644 --- a/src/app/(main)/settings/customer/create/page.tsx +++ b/src/app/(main)/settings/customer/create/page.tsx @@ -1,4 +1,4 @@ -import CustomerDetail from "@/components/CustomerDetail"; +import CustomerSave from "@/components/CustomerSave"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -16,7 +16,7 @@ const CreateCustomer: React.FC = async () => { <> {t("Create Customer")} - + ); diff --git a/src/app/(main)/settings/customer/edit/page.tsx b/src/app/(main)/settings/customer/edit/page.tsx index 004781b..7755403 100644 --- a/src/app/(main)/settings/customer/edit/page.tsx +++ b/src/app/(main)/settings/customer/edit/page.tsx @@ -1,5 +1,5 @@ import { fetchAllSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; -import CustomerDetail from "@/components/CustomerDetail"; +import CustomerSave from "@/components/CustomerSave"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -18,7 +18,7 @@ const EditCustomer: React.FC = async () => { <> {t("Edit Customer")} - + ); diff --git a/src/app/(main)/tasks/create/page.tsx b/src/app/(main)/tasks/create/page.tsx index 656139f..262f624 100644 --- a/src/app/(main)/tasks/create/page.tsx +++ b/src/app/(main)/tasks/create/page.tsx @@ -3,6 +3,7 @@ import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; export const metadata: Metadata = { title: "Create Task Template", @@ -15,7 +16,9 @@ const Projects: React.FC = async () => { return ( <> {t("Create Task Template")} - + + + ); }; diff --git a/src/app/(main)/tasks/edit/page.tsx b/src/app/(main)/tasks/edit/page.tsx new file mode 100644 index 0000000..2b2c02c --- /dev/null +++ b/src/app/(main)/tasks/edit/page.tsx @@ -0,0 +1,26 @@ +import { preloadAllTasks } from "@/app/api/tasks"; +import CreateTaskTemplate from "@/components/CreateTaskTemplate"; +import { getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; + +export const metadata: Metadata = { + title: "Edit Task Template", +}; + +const TaskTemplates: React.FC = async () => { + const { t } = await getServerI18n("tasks"); + preloadAllTasks(); + + return ( + <> + {t("Edit Task Template")} + + + + + ); +}; + +export default TaskTemplates; diff --git a/src/app/(main)/tasks/page.tsx b/src/app/(main)/tasks/page.tsx index b9e9bf8..bf06dc2 100644 --- a/src/app/(main)/tasks/page.tsx +++ b/src/app/(main)/tasks/page.tsx @@ -8,13 +8,14 @@ import Typography from "@mui/material/Typography"; import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; export const metadata: Metadata = { title: "Tasks", }; const TaskTemplates: React.FC = async () => { - const { t } = await getServerI18n("projects"); + const { t } = await getServerI18n("tasks"); preloadTaskTemplates(); return ( @@ -34,12 +35,14 @@ const TaskTemplates: React.FC = async () => { LinkComponent={Link} href="/tasks/create" > - {t("Create Template")} + {t("Create Task Template")} - }> - - + + }> + + + ); }; diff --git a/src/app/api/tasks/actions.ts b/src/app/api/tasks/actions.ts index 862cc62..2c043be 100644 --- a/src/app/api/tasks/actions.ts +++ b/src/app/api/tasks/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { TaskTemplate } from "."; import { revalidateTag } from "next/cache"; @@ -9,11 +9,13 @@ export interface NewTaskTemplateFormInputs { code: string; name: string; taskIds: number[]; + + id: number | null; } export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { const newTaskTemplate = await serverFetchJson( - `${BASE_API_URL}/tasks/templates/new`, + `${BASE_API_URL}/tasks/templates/save`, { method: "POST", body: JSON.stringify(data), @@ -25,3 +27,27 @@ export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { return newTaskTemplate; }; + +export const fetchTaskTemplate = async (id: number) => { + const taskTemplate = await serverFetchJson( + `${BASE_API_URL}/tasks/templates/${id}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate; +}; + +export const deleteTaskTemplate = async (id: number) => { + const taskTemplate = await serverFetchWithNoContent( + `${BASE_API_URL}/tasks/templates/${id}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate +}; diff --git a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx index 066c994..f7d5912 100644 --- a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx +++ b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx @@ -10,15 +10,17 @@ import TransferList from "../TransferList"; import Button from "@mui/material/Button"; import Check from "@mui/icons-material/Check"; import Close from "@mui/icons-material/Close"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import React from "react"; import Stack from "@mui/material/Stack"; import { Task } from "@/app/api/tasks"; import { NewTaskTemplateFormInputs, + fetchTaskTemplate, saveTaskTemplate, } from "@/app/api/tasks/actions"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { SubmitHandler, useFieldArray, useForm } from "react-hook-form"; +import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { tasks: Task[]; @@ -27,6 +29,7 @@ interface Props { const CreateTaskTemplate: React.FC = ({ tasks }) => { const { t } = useTranslation(); + const searchParams = useSearchParams() const router = useRouter(); const handleCancel = () => { router.back(); @@ -49,6 +52,7 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { handleSubmit, setValue, watch, + resetField, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { taskIds: [] } }); @@ -57,12 +61,56 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { return items.filter((item) => currentTaskIds.includes(item.id)); }, [currentTaskIds, items]); + const [refTaskTemplate, setRefTaskTemplate] = React.useState() + const id = searchParams.get('id') + + const fetchCurrentTaskTemplate = async () => { + try { + const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) + + const defaultValues = { + id: parseInt(id!!), + code: taskTemplate.code ?? null, + name: taskTemplate.name ?? null, + taskIds: taskTemplate.tasks.map(task => task.id) ?? [], + } + + setRefTaskTemplate(defaultValues) + } catch (e) { + console.log(e) + } + } + + React.useLayoutEffect(() => { + if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() + }, [id]) + + React.useEffect(() => { + if (refTaskTemplate) { + setValue("taskIds", refTaskTemplate.taskIds) + resetField("code", { defaultValue: refTaskTemplate.code }) + resetField("name", { defaultValue: refTaskTemplate.name }) + setValue("id", refTaskTemplate.id) + } + }, [refTaskTemplate]) + const onSubmit: SubmitHandler = React.useCallback( async (data) => { try { setServerError(""); - await saveTaskTemplate(data); - router.replace("/tasks"); + submitDialog(async () => { + const response = await saveTaskTemplate(data); + + if (response?.id !== null && response?.id !== undefined && response?.id > 0) { + successDialog(t("Submit Success"), t).then(() => { + router.replace("/tasks"); + }) + } else { + errorDialog(t("Submit Fail"), t).then(() => { + return false + }) + } + }, t) } catch (e) { setServerError(t("An error has occurred. Please try again later.")); } @@ -71,72 +119,77 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { ); return ( - - - - {t("Task List Setup")} - - - - - - + { + (id === null || refTaskTemplate !== undefined) && + + + {t("Task List Setup")} + + + + + + + + + { + setValue( + "taskIds", + selectedTasks.map((item) => item.id), + ); + }} + allItemsLabel={t("Task Pool")} + selectedItemsLabel={t("Task List Template")} /> - - - { - setValue( - "taskIds", - selectedTasks.map((item) => item.id), - ); - }} - allItemsLabel={t("Task Pool")} - selectedItemsLabel={t("Task List Template")} - /> - - - {serverError && ( - - {serverError} - - )} - - - - - + + + { + serverError && ( + + {serverError} + + ) + } + + + + + } + ); }; diff --git a/src/components/CustomerDetail/index.ts b/src/components/CustomerDetail/index.ts deleted file mode 100644 index a8811e6..0000000 --- a/src/components/CustomerDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CustomerDetailWrapper"; \ No newline at end of file diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerSave/ContactInfo.tsx similarity index 100% rename from src/components/CustomerDetail/ContactInfo.tsx rename to src/components/CustomerSave/ContactInfo.tsx diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerSave/CustomerInfo.tsx similarity index 100% rename from src/components/CustomerDetail/CustomerInfo.tsx rename to src/components/CustomerSave/CustomerInfo.tsx diff --git a/src/components/CustomerDetail/CustomerDetail.tsx b/src/components/CustomerSave/CustomerSave.tsx similarity index 99% rename from src/components/CustomerDetail/CustomerDetail.tsx rename to src/components/CustomerSave/CustomerSave.tsx index 88a99ad..fc2469e 100644 --- a/src/components/CustomerDetail/CustomerDetail.tsx +++ b/src/components/CustomerSave/CustomerSave.tsx @@ -42,7 +42,7 @@ const hasErrorsInTab = ( } }; -const CustomerDetail: React.FC = ({ +const CustomerSave: React.FC = ({ subsidiaries, customerTypes, }) => { @@ -277,4 +277,4 @@ const CustomerDetail: React.FC = ({ ); }; -export default CustomerDetail; \ No newline at end of file +export default CustomerSave; \ No newline at end of file diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerSave/CustomerSaveWrapper.tsx similarity index 73% rename from src/components/CustomerDetail/CustomerDetailWrapper.tsx rename to src/components/CustomerSave/CustomerSaveWrapper.tsx index 0206940..078f50a 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerSave/CustomerSaveWrapper.tsx @@ -3,7 +3,7 @@ // import { fetchProjectCategories } from "@/app/api/projects"; // import { fetchTeamLeads } from "@/app/api/staff"; import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; -import CustomerDetail from "./CustomerDetail"; +import CustomerSave from "./CustomerSave"; // type Props = { // params: { @@ -11,7 +11,7 @@ import CustomerDetail from "./CustomerDetail"; // }; // }; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { // const { params } = props // console.log(params) const [subsidiaries, customerTypes] = @@ -21,8 +21,8 @@ const CustomerDetailWrapper: React.FC = async () => { ]); return ( - + ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/CustomerDetail/SubsidiaryAllocation.tsx b/src/components/CustomerSave/SubsidiaryAllocation.tsx similarity index 100% rename from src/components/CustomerDetail/SubsidiaryAllocation.tsx rename to src/components/CustomerSave/SubsidiaryAllocation.tsx diff --git a/src/components/CustomerSave/index.ts b/src/components/CustomerSave/index.ts new file mode 100644 index 0000000..ea74d25 --- /dev/null +++ b/src/components/CustomerSave/index.ts @@ -0,0 +1 @@ +export { default } from "./CustomerSaveWrapper"; \ No newline at end of file diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index fc6204d..4111d14 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -68,7 +68,7 @@ const StaffSearch: React.FC = ({ staff }) => { const deleteClick = useCallback((staff: StaffResult) => { deleteDialog(async () => { await deleteStaff(staff.id); - successDialog("Delete Success", t); + successDialog(t("Delete Success"), t); setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); }, t); }, []); diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx index 1a9ced0..c335042 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx @@ -1,7 +1,7 @@ import { fetchAllCustomers, fetchSubsidiaryTypes } from "@/app/api/subsidiary"; import SubsidiaryDetail from "./SubsidiaryDetail"; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { const [customers, subsidiaryTypes] = await Promise.all([ fetchAllCustomers(), @@ -13,4 +13,4 @@ const CustomerDetailWrapper: React.FC = async () => { ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/SubsidiarySearch/SubsidiarySearch.tsx b/src/components/SubsidiarySearch/SubsidiarySearch.tsx index 95c901f..c4e1db5 100644 --- a/src/components/SubsidiarySearch/SubsidiarySearch.tsx +++ b/src/components/SubsidiarySearch/SubsidiarySearch.tsx @@ -46,7 +46,7 @@ const SubsidiarySearch: React.FC = ({ subsidiaries }) => { deleteDialog(async() => { await deleteSubsidiary(subsidiary.id) - successDialog("Delete Success", t) + successDialog(t("Delete Success"), t) setFilteredSubsidiaries((prev) => prev.filter((obj) => obj.id !== subsidiary.id)) }, t) diff --git a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx index 72be3c1..7563f53 100644 --- a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx +++ b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx @@ -6,6 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import DeleteIcon from '@mui/icons-material/Delete'; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { deleteTaskTemplate } from "@/app/api/tasks/actions"; interface Props { taskTemplates: TaskTemplate[]; @@ -16,6 +20,8 @@ type SearchParamNames = keyof SearchQuery; const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { const { t } = useTranslation("tasks"); + const searchParams = useSearchParams() + const router = useRouter() const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); const searchCriteria: Criterion[] = useMemo( @@ -30,7 +36,20 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, [taskTemplates]); const onTaskClick = useCallback((taskTemplate: TaskTemplate) => { - console.log(taskTemplate); + const params = new URLSearchParams(searchParams.toString()) + params.set("id", taskTemplate.id.toString()) + router.replace(`/tasks/edit?${params.toString()}`); + }, []); + + const onDeleteClick = useCallback((taskTemplate: TaskTemplate) => { + + deleteDialog(async () => { + await deleteTaskTemplate(taskTemplate.id) + + successDialog(t("Delete Success"), t) + + setFilteredTemplates((prev) => prev.filter((obj) => obj.id !== taskTemplate.id)) + }, t) }, []); const columns = useMemo[]>( @@ -43,6 +62,13 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, { name: "code", label: t("Task Template Code") }, { name: "name", label: t("Task Template Name") }, + { + name: "id", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, ], [onTaskClick, t], ); diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index a1db872..71ecb79 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -56,7 +56,7 @@ const TeamSearch: React.FC = ({ team }) => { deleteDialog(async () => { await deleteTeam(team.id); - successDialog("Delete Success", t); + successDialog(t("Delete Success"), t); setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); }, t); diff --git a/src/components/TransferList/TransferList.tsx b/src/components/TransferList/TransferList.tsx index 84cd468..232c486 100644 --- a/src/components/TransferList/TransferList.tsx +++ b/src/components/TransferList/TransferList.tsx @@ -109,7 +109,7 @@ const ItemList: React.FC = ({ {label} - {`${checkedItems.length}/${items.length} selected`} + {`${checkedItems.length}/${items.length} ${t("selected")}`} diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx index 095c544..658d25c 100644 --- a/src/components/UserSearch/UserSearch.tsx +++ b/src/components/UserSearch/UserSearch.tsx @@ -45,7 +45,7 @@ const UserSearch: React.FC = ({ users }) => { deleteDialog(async () => { await deleteUser(users.id); - successDialog("Delete Success", t); + successDialog(t("Delete Success"), t); setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); }, t); diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 5f1d289..a7d019a 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -17,6 +17,8 @@ "Do you want to delete?": "Do you want to delete", "Delete Success": "Delete Success", + "Details": "Details", + "Delete": "Delete", "Search": "Search", "Search Criteria": "Search Criteria", "Cancel": "Cancel", diff --git a/src/i18n/en/tasks.json b/src/i18n/en/tasks.json new file mode 100644 index 0000000..d70d00a --- /dev/null +++ b/src/i18n/en/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "Task Template", + "Create Task Template": "Create Task Template", + "Edit Task Template": "Edit Task Template", + + "Task Template Code": "Task Template Code", + "Task Template Name": "Task Template Name", + "Task List Setup": "Task List Setup", + "Task Pool": "Task Pool", + "Task List Template": "Task List Template", + + "Task template code is required": "Task template code is required", + "Task template name is required": "Task template name is required", + + "Do you want to submit?": "Do you want to submit?", + "Submit Success": "Submit Success", + "Submit Fail": "Submit Fail", + "Do you want to delete?": "Do you want to delete?", + "Delete Success": "Delete Success", + + "selected": "selected", + "Details": "Details", + "Delete": "Delete", + "Cancel": "Cancel", + "Submit": "Submit", + "Confirm": "Confirm" +} \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index e4642ea..4ff2fcf 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -15,6 +15,8 @@ "Do you want to delete?": "你是否確認要刪除?", "Delete Success": "刪除成功", + "Details": "詳情", + "Delete": "刪除", "Search": "搜尋", "Search Criteria": "搜尋條件", "Cancel": "取消", diff --git a/src/i18n/zh/tasks.json b/src/i18n/zh/tasks.json new file mode 100644 index 0000000..16ba727 --- /dev/null +++ b/src/i18n/zh/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "工作範本", + "Create Task Template": "建立工作範本", + "Edit Task Template": "編輯工作範本", + + "Task Template Code": "工作範本編號", + "Task Template Name": "工作範本名稱", + "Task List Setup": "工作名單設置", + "Task Pool": "所有工作", + "Task List Template": "工作名單範本", + + "Task template code is required": "需要工作範本編號", + "Task template name is required": "需要工作範本名稱", + + "Do you want to submit?": "你是否確認要提交?", + "Submit Success": "提交成功", + "Submit Fail": "提交失敗", + "Do you want to delete?": "你是否確認要刪除?", + "Delete Success": "刪除成功", + + "selected": "已選擇", + "Details": "詳情", + "Delete": "刪除", + "Cancel": "取消", + "Submit": "提交", + "Confirm": "確認" +} \ No newline at end of file From 6c16db51f5322352e68460a6f5eff1ea496a335f Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Tue, 30 Apr 2024 18:14:49 +0800 Subject: [PATCH 17/22] add report ex01 --- public/temp/EX01_Financial Status Report.xlsx | Bin 0 -> 13143 bytes .../analytics/FinancialStatusReport/page.tsx | 24 + src/app/api/reporte1/index.ts | 42 ++ .../NavigationContent/NavigationContent.tsx | 1 + .../FinancialStatusReport.tsx | 17 + .../Report/FinancialStatusReport/index.ts | 2 + .../FinancialStatusReportGen.tsx | 43 ++ .../FinancialStatusReportGenLoading.tsx | 41 ++ .../FinancialStatusReportGenWrapper.tsx | 19 + .../Report/FinancialStatusReportGen/index.ts | 2 + .../Report/ReportSearchBoxe1/SearchBoxe1.tsx | 482 ++++++++++++++++++ .../Report/ReportSearchBoxe1/index.ts | 3 + 12 files changed, 676 insertions(+) create mode 100644 public/temp/EX01_Financial Status Report.xlsx create mode 100644 src/app/(main)/analytics/FinancialStatusReport/page.tsx create mode 100644 src/app/api/reporte1/index.ts create mode 100644 src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx create mode 100644 src/components/Report/FinancialStatusReport/index.ts create mode 100644 src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx create mode 100644 src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx create mode 100644 src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx create mode 100644 src/components/Report/FinancialStatusReportGen/index.ts create mode 100644 src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx create mode 100644 src/components/Report/ReportSearchBoxe1/index.ts diff --git a/public/temp/EX01_Financial Status Report.xlsx b/public/temp/EX01_Financial Status Report.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bd1b55dbd8348ebf9a7381c5a951080b0776f6ec GIT binary patch literal 13143 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 V9Mr20f`K8v?!R0=0n4wu{|9^8l?VU; literal 0 HcmV?d00001 diff --git a/src/app/(main)/analytics/FinancialStatusReport/page.tsx b/src/app/(main)/analytics/FinancialStatusReport/page.tsx new file mode 100644 index 0000000..0a3865d --- /dev/null +++ b/src/app/(main)/analytics/FinancialStatusReport/page.tsx @@ -0,0 +1,24 @@ +//src\app\(main)\analytics\DelayReport\page.tsx +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import FinancialStatusReportComponent from "@/components/Report/FinancialStatusReport"; + +export const metadata: Metadata = { + title: "Financial Status Report", +}; + +const ProjectFinancialStatusReport: React.FC = () => { + return ( + + + Financial Status Report + + {/* }> + + */} + + + ); +}; +export default ProjectFinancialStatusReport; diff --git a/src/app/api/reporte1/index.ts b/src/app/api/reporte1/index.ts new file mode 100644 index 0000000..5e27648 --- /dev/null +++ b/src/app/api/reporte1/index.ts @@ -0,0 +1,42 @@ +//src\app\api\report\index.ts +import { cache } from "react"; + +export interface FinancialStatus { + id: number; + projectCode: string; + projectName: string; + team: string; + teamLeader: string; + startDate: string; + startDateFrom: string; + startDateTo: string; + targetEndDate: string; + client: string; + subsidiary: string; + status: string; +} + +export const preloadProjects = () => { + fetchProjectsFinancialStatus(); +}; + +export const fetchProjectsFinancialStatus = cache(async () => { + return mockProjects; +}); + +const mockProjects: FinancialStatus[] = [ + { + id: 1, + projectCode: "CUST-001", + projectName: "Client A", + team: "N/A", + teamLeader: "N/A", + startDate: "5", + startDateFrom: "5", + startDateTo: "5", + targetEndDate: "s", + client: "ss", + subsidiary: "ss", + status: "1", + }, +]; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index ad68823..45bfda9 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -108,6 +108,7 @@ const navigationItems: NavigationItem[] = [ {icon: , label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, + {icon: , label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, ], }, { diff --git a/src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx b/src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx new file mode 100644 index 0000000..893b0ba --- /dev/null +++ b/src/components/Report/FinancialStatusReport/FinancialStatusReport.tsx @@ -0,0 +1,17 @@ +//src\components\DelayReport\DelayReport.tsx +"use client"; +import * as React from "react"; +import "../../../app/global.css"; +import { Suspense } from "react"; +import FinancialStatusReportGen from "@/components/Report/FinancialStatusReportGen"; + +const FinancialStatusReport: React.FC = () => { + + return ( + }> + + + ); +}; + +export default FinancialStatusReport; \ No newline at end of file diff --git a/src/components/Report/FinancialStatusReport/index.ts b/src/components/Report/FinancialStatusReport/index.ts new file mode 100644 index 0000000..4500704 --- /dev/null +++ b/src/components/Report/FinancialStatusReport/index.ts @@ -0,0 +1,2 @@ +//src\components\LateStartReport\index.ts +export { default } from "./FinancialStatusReport"; diff --git a/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx new file mode 100644 index 0000000..bf4a7cd --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGen.tsx @@ -0,0 +1,43 @@ +//src\components\LateStartReportGen\LateStartReportGen.tsx +"use client"; +import React, { useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../ReportSearchBoxe1"; +import { useTranslation } from "react-i18next"; +import { FinancialStatus } from "@/app/api/reporte1"; +//import { DownloadReportButton } from './DownloadReportButton'; +interface Props { + projects: FinancialStatus[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const ProgressByClientSearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "{Project Code}", paramName: "projectCode", type: "select", options: ["M1234", "M1268", "M1352", "M1393"] }, + // { + // label: "Status", + // label2: "Remained Date To", + // paramName: "targetEndDate", + // type: "dateRange", + // }, + ], + [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + {/* */} + + ); +}; + +export default ProgressByClientSearch; diff --git a/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx new file mode 100644 index 0000000..6d992f9 --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenLoading.tsx @@ -0,0 +1,41 @@ +//src\components\LateStartReportGen\LateStartReportGenLoading.tsx +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const FinancialStatusReportGenLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default FinancialStatusReportGenLoading; diff --git a/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx new file mode 100644 index 0000000..e82ea4b --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/FinancialStatusReportGenWrapper.tsx @@ -0,0 +1,19 @@ +//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx +import { fetchProjectsFinancialStatus } from "@/app/api/reporte1"; +import React from "react"; +import FinancialStatusReportGen from "./FinancialStatusReportGen"; +import FinancialStatusReportGenLoading from "./FinancialStatusReportGenLoading"; + +interface SubComponents { + Loading: typeof FinancialStatusReportGenLoading; +} + +const FinancialStatusReportGenWrapper: React.FC & SubComponents = async () => { + const clentprojects = await fetchProjectsFinancialStatus(); + + return ; +}; + +FinancialStatusReportGenWrapper.Loading = FinancialStatusReportGenLoading; + +export default FinancialStatusReportGenWrapper; \ No newline at end of file diff --git a/src/components/Report/FinancialStatusReportGen/index.ts b/src/components/Report/FinancialStatusReportGen/index.ts new file mode 100644 index 0000000..d53d85b --- /dev/null +++ b/src/components/Report/FinancialStatusReportGen/index.ts @@ -0,0 +1,2 @@ +//src\components\DelayReportGen\index.ts +export { default } from "./FinancialStatusReportGenWrapper"; diff --git a/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx b/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx new file mode 100644 index 0000000..ea7afb6 --- /dev/null +++ b/src/components/Report/ReportSearchBoxe1/SearchBoxe1.tsx @@ -0,0 +1,482 @@ +//src\components\ReportSearchBox\SearchBox2.tsx +"use client"; + +import Grid from "@mui/material/Grid"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import React, { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TextField from "@mui/material/TextField"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Search from "@mui/icons-material/Search"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-hk"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Box } from "@mui/material"; +import * as XLSX from 'xlsx-js-style'; +//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; + +interface BaseCriterion { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + interface CellValue { + v: number | string; // Value of the cell + t: 'n' | 's'; // Type of the cell value: 'n' for number, 's' for string + s?: XLSX.CellStyle; // Optional style for the cell + } + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + // Function to merge cells from A2:B2 to A14:B14 +function mergeCells(worksheet: XLSX.WorkSheet) { + // Ensure the 'merges' array exists in the worksheet + if (!worksheet['!merges']) worksheet['!merges'] = []; + + // Loop through rows 2 to 14 (0-indexed + 1) + for (let row = 1; row <= 13; row++) { + // Define the range for current row to merge A and B columns + const mergeRange = { + s: { c: 0, r: row }, // Start cell (Column A) + e: { c: 1, r: row } // End cell (Column B) + }; + // Add the range to the 'merges' array in the worksheet + worksheet['!merges'].push(mergeRange); + // Apply center alignment to the merged cell + const mergedCellRef = XLSX.utils.encode_cell({ c: 0, r: row }); + if (!worksheet[mergedCellRef]) { + worksheet[mergedCellRef] = {}; // Create the cell if it doesn't exist + } + worksheet[mergedCellRef].s = { + alignment: { horizontal: "left", wrapText: true } + }; + } +} + +// Processing and inserting table data with calculations +function processDataAndInsert(worksheet: XLSX.WorkSheet, startRow:number, data:(string|number)[][]) { + data.forEach((row, rowIndex) => { + const r = startRow + rowIndex; + + // Direct assignments for columns A-F as strings + const stringCols = ['A', 'B', 'C', 'D', 'E', 'F']; + stringCols.forEach((col, index) => { + const cellRef = col + r; + worksheet[cellRef] = { v: row[index], t: 's' }; // Force type as string + }); + + // Assignments for columns G-O as numbers + const numberCols = ['G', 'H', 'I', 'K', 'N']; + const colIndices = [6, 7, 8, 9, 10]; // Indices in the data array corresponding to G, H, I, K, N + numberCols.forEach((col, index) => { + const cellRef = col + r; + worksheet[cellRef] = { v: row[colIndices[index]], t: 'n' }; // Force type as number + }); + + // Calculations for columns J, L, M, O + const h = row[6] as number; + const i = row[7] as number; + const k = row[9] as number; + const n = row[10] as number; + + // Column J: H - I + worksheet['J' + r] = { v: h - i, t: 'n' }; + + // Column L: IF(H { + const cellRefs = data.map((_, index) => col + (startRow + index)); + const formula = `=SUM(${cellRefs.join(',')})`; + worksheet[col + sumRow] = { f: formula, t: 'n', s: { + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}} + } + }}; + }); + XLSX.utils.sheet_add_aoa(worksheet, [['Sub-total']], { origin: { c: 0, r: (sumRow-1) } }); + +// const mergedCellRefA1 = XLSX.utils.encode_cell({ c: 0, r: sumRow-1}); +// if (!worksheet[mergedCellRefA1]) { +// worksheet[mergedCellRefA1] = {}; // Create the cell if it doesn't exist +// } +// // Apply right alignment, center vertical alignment, wrap text, and border styles to the 'Sub-total' cell +// worksheet[mergedCellRefA1].s = { +// alignment: {horizontal: "right", vertical: "center", wrapText: true}, +// border: { +// top: {style: 'thin', color: {auto: 1}}, +// bottom: {style: 'double', color: {auto: 1}} +// } +// }; +// Define the range of cells to merge for 'Sub-total' +const mergeRangeSubTotal = { + s: { c: 0, r: sumRow-1}, // Start at column A + e: { c: 5, r: sumRow-1} // End at column F +}; +// // Add the range to the 'merges' array in the worksheet if it doesn't exist +// if (!worksheet['!merges']) worksheet['!merges'] = []; +// worksheet['!merges'].push(mergeRangeSubTotal); + +// Update styles for the merged cell range where 'Sub-total' is located +const mergedCellRefSubTotal = XLSX.utils.encode_cell({ c: 0, r: sumRow-1 }); +if (!worksheet[mergedCellRefSubTotal]) { + worksheet[mergedCellRefSubTotal] = {}; // Create the cell if it doesn't exist +} +worksheet[mergedCellRefSubTotal].s = { + alignment: {horizontal: "right", vertical: "center", wrapText: true}, + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}}} +}; +// Add the range to the 'merges' array in the worksheet if it doesn't exist +if (!worksheet['!merges']) worksheet['!merges'] = []; +worksheet['!merges'].push(mergeRangeSubTotal) + + +const mergedCellRefM1 = XLSX.utils.encode_cell({ c: 12, r: sumRow}); +if (!worksheet[mergedCellRefM1]) { + worksheet[mergedCellRefM1] = {}; // Create the cell if it doesn't exist +} +worksheet[mergedCellRefM1].s = { + alignment: {horizontal: "right", vertical: "center", wrapText: true}, + border: { + top: {style: 'thin', color: {auto: 1}}, + bottom: {style: 'double', color: {auto: 1}} + } +}; + +} +const firstTableData = [ + ['Code1', 'PJName1', 'Client1','Team1','2011/01/01','2011/02/01','625','500','350','350','171'], // Row 1 + ['Code2', 'PJName2', 'Client2','Team2','2011/03/01','2011/04/01','1000','800','565','565','565'],// Row 2 + ['Code2', 'PJName2', 'Client2','Team2','2011/03/01','2011/04/01','1000','800','565','565','565'],// Row 3 + // ... more rows as needed +]; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/EX01_Financial Status Report.xlsx', { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + }); + if (!response.ok) throw new Error('Network response was not ok.'); + + const data = await response.blob(); + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target && e.target.result) { + const ab = e.target.result as ArrayBuffer; + const workbook = XLSX.read(ab, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // Add the current date to cell C2 + const cellAddress = 'C2'; + const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD + const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD + XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); + + mergeCells(worksheet); + + // Style for cell A1: Font size 16 and bold + if (worksheet['A1']) { + worksheet['A1'].s = { + font: {bold: true,sz: 16,},alignment: { horizontal: 'center' } // Font size 16 //name: 'Times New Roman' // Specify font + }; + } + + // Apply styles from A2 A3 A5 (bold) + ['A2', 'A3', 'A5','A14'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: true },alignment: { horizontal: 'left' } }; + } + }); + + // Apply styles from A2 A3 A5 (bold) + ['A6', 'A7', 'A8','A9','A10','A11','A12'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: false },alignment: { horizontal: 'left' } }; + } + }); + + // Formatting from A15 to O15 + // Apply styles from A6 to K6 (bold, bottom border, center alignment) + for (let col = 0; col < 15; col++) { // Columns A to O + const cellRef = XLSX.utils.encode_col(col) + '15'; + if (worksheet[cellRef]) { + worksheet[cellRef].s = { + font: { bold: true }, + alignment: { horizontal: 'center' }, + border: { + bottom: { style: 'thin', color: { auto: 1 } } + } + }; + } + } + + // Find the last row of the first table + let lastRowOfFirstTable = 16; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + // Insert the first data form into the worksheet at the desired location + //XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Assuming worksheet is already defined, and we start inserting from row 16 +processDataAndInsert(worksheet, 16, firstTableData); + // Update lastRowOfFirstTable to account for the new data + lastRowOfFirstTable += firstTableData.length; + // Now insert the text that goes between the two tables + + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + + // Start with a base width for each column (optional, but can help with columns that have no data) + // Check if worksheet['!ref'] is defined to prevent errors + const maxCol = worksheet['!ref'] ? worksheet['!ref'].split(':')[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1 : 0; + for (let col = 0; col < maxCol; col++) { + colWidths[col] = 10; // Default base width + } + + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + + // Skip the first row in the jsonData + for (let row = 1; row < jsonData.length; row++) { + jsonData[row].forEach((cell, index) => { + // Only process if the cell is not null/undefined + if (cell) { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + } + }); + } + + // Check if worksheet exists before setting '!cols' + if (worksheet) { + worksheet['!cols'] = colWidths.map((width) => ({ wch: width + 2 })); // +2 for a little extra padding + } + + // Format filename with date + const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD + const filename = `EX01_Financial_Status_Report_${today}.xlsx`; // Append formatted date to the filename + + // Convert workbook back to XLSX file + XLSX.writeFile(workbook, filename); + } else { + throw new Error('Failed to load file'); + } + }; + reader.readAsArrayBuffer(data); + } catch (error) { + console.error('Error downloading the file: ', error); + } + + //setIsLoading(false); + }; + return ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/Report/ReportSearchBoxe1/index.ts b/src/components/Report/ReportSearchBoxe1/index.ts new file mode 100644 index 0000000..8f4d421 --- /dev/null +++ b/src/components/Report/ReportSearchBoxe1/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBoxe1"; +export type { Criterion } from "./SearchBoxe1"; From 06f1d96d0d69bfe13596cecd0811f184201e84fc Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Tue, 30 Apr 2024 18:17:35 +0800 Subject: [PATCH 18/22] update --- package-lock.json | 246 +++++++++++- public/temp/AR08_Project P&L Report.xlsx | Bin 0 -> 12932 bytes .../Report/ReportSearchBox8/SearchBox8.tsx | 358 ++++++++++++++++++ .../Report/ReportSearchBox8/index.ts | 3 + 4 files changed, 604 insertions(+), 3 deletions(-) create mode 100644 public/temp/AR08_Project P&L Report.xlsx create mode 100644 src/components/Report/ReportSearchBox8/SearchBox8.tsx create mode 100644 src/components/Report/ReportSearchBox8/index.ts diff --git a/package-lock.json b/package-lock.json index 84bf9b7..4e65451 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@faker-js/faker": "^8.4.1", "@fontsource/inter": "^5.0.16", "@fontsource/plus-jakarta-sans": "^5.0.18", "@mui/icons-material": "^5.15.0", @@ -20,6 +21,7 @@ "@mui/x-date-pickers": "^6.18.7", "@unly/universal-language-detector": "^2.0.3", "apexcharts": "^3.45.2", + "axios": "^1.6.8", "dayjs": "^1.11.10", "i18next": "^23.7.11", "i18next-resources-to-backend": "^1.2.0", @@ -37,7 +39,8 @@ "react-select": "^5.8.0", "reactstrap": "^9.2.2", "styled-components": "^6.1.8", - "sweetalert2": "^11.10.3" + "sweetalert2": "^11.10.3", + "xlsx-js-style": "^1.2.0" }, "devDependencies": { "@types/lodash": "^4.14.202", @@ -1933,6 +1936,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", @@ -3530,6 +3548,21 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3836,6 +3869,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -3904,6 +3942,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -4150,6 +4198,26 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4236,6 +4304,26 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==", + "dependencies": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage/node_modules/commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4249,6 +4337,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4316,6 +4415,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4516,6 +4626,14 @@ "rimraf": "bin.js" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5431,6 +5549,14 @@ "node": ">=0.8.x" } }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5476,6 +5602,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", + "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5575,6 +5706,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5599,6 +5749,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -6977,7 +7148,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "peer": true, "engines": { "node": ">= 0.6" } @@ -6986,7 +7156,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7880,6 +8049,17 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7890,6 +8070,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8631,6 +8816,17 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -9853,6 +10049,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/workbox-background-sync": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", @@ -10238,6 +10450,34 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xlsx-js-style": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz", + "integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.3.8", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/public/temp/AR08_Project P&L Report.xlsx b/public/temp/AR08_Project P&L Report.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..de45dbed85c0bb39c5e88a59f093ebd1a1ebfaaa GIT binary patch literal 12932 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 Mn;J{}(GCIle++{s6951J literal 0 HcmV?d00001 diff --git a/src/components/Report/ReportSearchBox8/SearchBox8.tsx b/src/components/Report/ReportSearchBox8/SearchBox8.tsx new file mode 100644 index 0000000..d4e1033 --- /dev/null +++ b/src/components/Report/ReportSearchBox8/SearchBox8.tsx @@ -0,0 +1,358 @@ +//src\components\ReportSearchBox\SearchBox.tsx +"use client"; + +import Grid from "@mui/material/Grid"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import React, { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TextField from "@mui/material/TextField"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import CardActions from "@mui/material/CardActions"; +import Button from "@mui/material/Button"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Search from "@mui/icons-material/Search"; +import dayjs from "dayjs"; +import "dayjs/locale/zh-hk"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Box } from "@mui/material"; +import * as XLSX from 'xlsx-js-style'; +//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; + +interface BaseCriterion { + label: string; + label2?: string; + paramName: T; + paramName2?: T; +} + +interface TextCriterion extends BaseCriterion { + type: "text"; +} + +interface SelectCriterion extends BaseCriterion { + type: "select"; + options: string[]; +} + +interface DateRangeCriterion extends BaseCriterion { + type: "dateRange"; +} + +export type Criterion = + | TextCriterion + | SelectCriterion + | DateRangeCriterion; + +interface Props { + criteria: Criterion[]; + onSearch: (inputs: Record) => void; + onReset?: () => void; +} + +function SearchBox({ + criteria, + onSearch, + onReset, +}: Props) { + const { t } = useTranslation("common"); + const defaultInputs = useMemo( + () => + criteria.reduce>( + (acc, c) => { + return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; + }, + {} as Record, + ), + [criteria], + ); + const [inputs, setInputs] = useState(defaultInputs); + + const makeInputChangeHandler = useCallback( + (paramName: T): React.ChangeEventHandler => { + return (e) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, + [], + ); + + const makeSelectChangeHandler = useCallback((paramName: T) => { + return (e: SelectChangeEvent) => { + setInputs((i) => ({ ...i, [paramName]: e.target.value })); + }; + }, []); + + const makeDateChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); + }; + }, []); + + const makeDateToChangeHandler = useCallback((paramName: T) => { + return (e: any) => { + setInputs((i) => ({ + ...i, + [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), + })); + }; + }, []); + + const handleReset = () => { + setInputs(defaultInputs); + onReset?.(); + }; + + const handleSearch = () => { + onSearch(inputs); + + }; + + const handleDownload = async () => { + //setIsLoading(true); + + try { + const response = await fetch('/temp/AR08_Project P&L Report.xlsx', { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + }); + if (!response.ok) throw new Error('Network response was not ok.'); + + const data = await response.blob(); + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target && e.target.result) { + const ab = e.target.result as ArrayBuffer; + const workbook = XLSX.read(ab, { type: 'array' }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + + // Add the current date to cell B2 + const cellAddress = 'B2'; + const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD + const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD + XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress }); + + // Style for cell A1: Font size 16 and bold + if (worksheet['A1']) { + worksheet['A1'].s = { + font: {bold: true,sz: 16, // Font size 16 + alignment: { horizontal: 'left' }, + //name: 'Times New Roman' // Specify font + } + }; + } + + // Apply styles from A2 to A7 (bold) + ['A2', 'A3', 'A4','A5', 'A6', 'A7'].forEach(cell => { + if (worksheet[cell]) { + worksheet[cell].s = { font: { bold: true,sz: 12, + alignment: { horizontal: 'left' }, } + }; + } + }); + + const firstTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 + ['Data1', 'Data2', 'Data3'], // Row 2 + // ... more rows as needed + ]; + const secondTableData = [ + ['Column1', 'Column2', 'Column3'], // Row 1 of second table + ['Data1', 'Data2', 'Data3'], // Row 2 of second table + // ... more rows as needed + ]; + + // Find the last row of the first table + let lastRowOfFirstTable = 10; // Starting row for data in the first table + while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) { + lastRowOfFirstTable++; + } + + // Insert the first data form into the worksheet at the desired location + XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } }); + // Update lastRowOfFirstTable to account for the new data + lastRowOfFirstTable += firstTableData.length; + // Now insert the text that goes between the two tables + + // // Insert the additional text with one row of spacing after the first table + const textRow = lastRowOfFirstTable + 1; // Adjust the 1 based on how many lines of spacing you want + XLSX.utils.sheet_add_aoa(worksheet, [['Staff No. and Name']], { origin: { c: 0, r: textRow+1 } }); + + + let secondTableStartRow = textRow + 3; + // Insert the second data form into the worksheet at the new starting row + XLSX.utils.sheet_add_aoa(worksheet, secondTableData, { origin: { c: 0, r: secondTableStartRow } }); + + + // Source cell coordinates + const sourceCellCoord = { c: 1, r: 2 }; // C3 (columns and rows are 0-indexed in this library) + // Target cell coordinates + const targetCellCoord = { c: 3, r: 9 }; + // Create references for source and target cells + const sourceCellRef = XLSX.utils.encode_cell(sourceCellCoord); + const targetCellRef = XLSX.utils.encode_cell(targetCellCoord); + // Copy the cell content from C3 to the target cell + if (worksheet[sourceCellRef]) { + worksheet[targetCellRef] = { ...worksheet[sourceCellRef] }; + // If the source cell has a style, deep clone it for the target cell + if (worksheet[sourceCellRef].s) { + worksheet[targetCellRef].s = JSON.parse(JSON.stringify(worksheet[sourceCellRef].s)); + } + } + + // Formatting from A10 to F10 + // Apply styles from A10 to F10 (bottom border, center alignment) + for (let col = 0; col < 7; col++) { // Columns A to G + const cellRef = XLSX.utils.encode_col(col) + '10';//row + if (worksheet[cellRef]) { + worksheet[cellRef].s = { + font: { bold: false }, + alignment: { horizontal: 'center' }, + border: { + bottom: { style: 'thin', color: { auto: 1 } } + } + }; + } + } + + // Calculate the maximum length of content in each column and set column width + const colWidths: number[] = []; + // Start with a base width for each column (optional, but can help with columns that have no data) + const maxCol = worksheet['!ref'] ? worksheet['!ref'].split(':')[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1 : 0; + for (let col = 0; col < maxCol; col++) { + colWidths[col] = 10; // Default base width + } + const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][]; + // Skip the first row in the jsonData + for (let row = 1; row < jsonData.length; row++) { + jsonData[row].forEach((cell, index) => { + // Only process if the cell is not null/undefined + if (cell) { + const valueLength = cell.toString().length; + colWidths[index] = Math.max(colWidths[index] || 0, valueLength); + } + }); + } + + // Apply calculated widths to each column, skipping the first row + worksheet['!cols'] = colWidths.map((width, index) => { + return { wch: width + 2 }; // +2 for a little extra padding + }); + + + // Format filename with date + const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD + const filename = `AR08_Project_P&L_Report_${today}.xlsx`; // Append formatted date to the filename + + // Convert workbook back to XLSX file + XLSX.writeFile(workbook, filename); + } else { + throw new Error('Failed to load file'); + } + }; + reader.readAsArrayBuffer(data); + } catch (error) { + console.error('Error downloading the file: ', error); + } + + //setIsLoading(false); + }; + return ( + + + {t("Search Criteria")} + + {criteria.map((c) => { + return ( + + {c.type === "text" && ( + + )} + {c.type === "select" && ( + + {c.label} + + + )} + {c.type === "dateRange" && ( + + + + + + + {"-"} + + + + + + + )} + + ); + })} + + + + + + + + ); +} + +export default SearchBox; diff --git a/src/components/Report/ReportSearchBox8/index.ts b/src/components/Report/ReportSearchBox8/index.ts new file mode 100644 index 0000000..003fbde --- /dev/null +++ b/src/components/Report/ReportSearchBox8/index.ts @@ -0,0 +1,3 @@ +//src\components\SearchBox\index.ts +export { default } from "./SearchBox8"; +export type { Criterion } from "./SearchBox8"; From 293dfb7bb464b03ac34401db37a3418605eac974 Mon Sep 17 00:00:00 2001 From: Wayne Date: Tue, 30 Apr 2024 23:55:11 +0900 Subject: [PATCH 19/22] Add edit project page --- .../(main)/projects/edit/[projectId]/page.tsx | 62 +++++++ src/app/api/projects/actions.ts | 6 +- src/app/api/projects/index.ts | 14 +- .../CreateProject/CreateProject.tsx | 163 ++++++++++-------- .../CreateProject/CreateProjectWrapper.tsx | 17 +- .../ProjectSearch/ProjectSearch.tsx | 11 +- .../TableCellEdit/TableCellEdit.tsx | 8 +- .../TimesheetTable/EntryInputTable.tsx | 10 +- 8 files changed, 209 insertions(+), 82 deletions(-) create mode 100644 src/app/(main)/projects/edit/[projectId]/page.tsx diff --git a/src/app/(main)/projects/edit/[projectId]/page.tsx b/src/app/(main)/projects/edit/[projectId]/page.tsx new file mode 100644 index 0000000..dbad026 --- /dev/null +++ b/src/app/(main)/projects/edit/[projectId]/page.tsx @@ -0,0 +1,62 @@ +import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; +import { fetchGrades } from "@/app/api/grades"; +import { + fetchProjectBuildingTypes, + fetchProjectCategories, + fetchProjectContractTypes, + fetchProjectDetails, + fetchProjectFundingTypes, + fetchProjectLocationTypes, + fetchProjectServiceTypes, + fetchProjectWorkNatures, +} from "@/app/api/projects"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import CreateProject from "@/components/CreateProject"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; + +interface Props { + params: { + projectId: string; + }; +} + +export const metadata: Metadata = { + title: "Edit Project", +}; + +const Projects: React.FC = async ({ params }) => { + const { t } = await getServerI18n("projects"); + + // Preload necessary dependencies + fetchAllTasks(); + fetchTaskTemplates(); + fetchProjectCategories(); + fetchProjectContractTypes(); + fetchProjectFundingTypes(); + fetchProjectLocationTypes(); + fetchProjectServiceTypes(); + fetchProjectBuildingTypes(); + fetchProjectWorkNatures(); + fetchAllCustomers(); + fetchAllSubsidiaries(); + fetchGrades(); + preloadTeamLeads(); + preloadStaff(); + + // TODO: Handle not found + const fetchedProject = await fetchProjectDetails(params.projectId); + + return ( + <> + {t("Edit Project")} + + + + + ); +}; + +export default Projects; diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index 232f863..121111a 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -4,6 +4,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { Task, TaskGroup } from "../tasks"; import { Customer } from "../customer"; +import { revalidateTag } from "next/cache"; export interface CreateProjectInputs { // Project details @@ -62,9 +63,12 @@ export interface PaymentInputs { } export const saveProject = async (data: CreateProjectInputs) => { - return serverFetchJson(`${BASE_API_URL}/projects/new`, { + const newProject = await serverFetchJson(`${BASE_API_URL}/projects/new`, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); + + revalidateTag("projects"); + return newProject; }; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 7bfb067..90b0e10 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -3,6 +3,7 @@ import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import "server-only"; import { Task, TaskGroup } from "../tasks"; +import { CreateProjectInputs } from "./actions"; export interface ProjectResult { id: number; @@ -55,8 +56,8 @@ export interface AssignedProject { tasks: Task[]; milestones: { [taskGroupId: TaskGroup["id"]]: { - startDate: string; - endDate: string; + startDate?: string; + endDate?: string; }; }; // Manhour info @@ -145,3 +146,12 @@ export const fetchAssignedProjects = cache(async () => { }, ); }); + +export const fetchProjectDetails = cache(async (projectId: string) => { + return serverFetchJson( + `${BASE_API_URL}/projects/projectDetails/${projectId}`, + { + next: { tags: [`projectDetails_${projectId}`] }, + }, + ); +}); diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 3e1a3f0..175d712 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -22,7 +22,7 @@ import { useForm, } from "react-hook-form"; import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; -import { Error } from "@mui/icons-material"; +import { Delete, Error, PlayArrow } from "@mui/icons-material"; import { BuildingType, ContractType, @@ -38,6 +38,8 @@ import { Grade } from "@/app/api/grades"; import { Customer, Subsidiary } from "@/app/api/customer"; export interface Props { + isEditMode: boolean; + defaultInputs?: CreateProjectInputs; allTasks: Task[]; projectCategories: ProjectCategory[]; taskTemplates: TaskTemplate[]; @@ -69,6 +71,8 @@ const hasErrorsInTab = ( }; const CreateProject: React.FC = ({ + isEditMode, + defaultInputs, allTasks, projectCategories, taskTemplates, @@ -90,7 +94,7 @@ const CreateProject: React.FC = ({ const router = useRouter(); const handleCancel = () => { - router.back(); + router.replace("/projects"); }; const handleTabChange = useCallback>( @@ -128,7 +132,7 @@ const CreateProject: React.FC = ({ ); const formProps = useForm({ - defaultValues: { + defaultValues: defaultInputs ?? { taskGroups: {}, allocatedStaffIds: [], milestones: {}, @@ -142,76 +146,95 @@ const CreateProject: React.FC = ({ const errors = formProps.formState.errors; return ( - - - - - ) : undefined - } - iconPosition="end" - /> - - - - - { - - } - { - - } - { - - } - {} - {serverError && ( - - {serverError} - - )} - - - - - + )} + + + + + ) : undefined + } + iconPosition="end" + /> + + + + + { + + } + { + + } + { + + } + {} + {serverError && ( + + {serverError} + + )} + + + + + + + ); }; diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 3ca2fae..ab9c830 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -4,6 +4,7 @@ import { fetchProjectBuildingTypes, fetchProjectCategories, fetchProjectContractTypes, + fetchProjectDetails, fetchProjectFundingTypes, fetchProjectLocationTypes, fetchProjectServiceTypes, @@ -13,7 +14,15 @@ import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; import { fetchGrades } from "@/app/api/grades"; -const CreateProjectWrapper: React.FC = async () => { +type CreateProjectProps = { isEditMode: false }; +interface EditProjectProps { + isEditMode: true; + projectId: string; +} + +type Props = CreateProjectProps | EditProjectProps; + +const CreateProjectWrapper: React.FC = async (props) => { const [ tasks, taskTemplates, @@ -46,8 +55,14 @@ const CreateProjectWrapper: React.FC = async () => { fetchGrades(), ]); + const projectInfo = props.isEditMode + ? await fetchProjectDetails(props.projectId) + : undefined; + return ( >; type SearchParamNames = keyof SearchQuery; const ProjectSearch: React.FC = ({ projects, projectCategories }) => { + const router = useRouter(); const { t } = useTranslation("projects"); const [filteredProjects, setFilteredProjects] = useState(projects); @@ -51,9 +53,12 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { setFilteredProjects(projects); }, [projects]); - const onProjectClick = useCallback((project: ProjectResult) => { - console.log(project); - }, []); + const onProjectClick = useCallback( + (project: ProjectResult) => { + router.push(`/projects/edit/${project.id}`); + }, + [router], + ); const columns = useMemo[]>( () => [ diff --git a/src/components/TableCellEdit/TableCellEdit.tsx b/src/components/TableCellEdit/TableCellEdit.tsx index a96f0b7..0a366fc 100644 --- a/src/components/TableCellEdit/TableCellEdit.tsx +++ b/src/components/TableCellEdit/TableCellEdit.tsx @@ -9,9 +9,9 @@ import { Box, Input, SxProps, TableCell } from "@mui/material"; interface Props { value: T; - onChange: (newValue?: T) => void; + onChange: (newValue: T) => void; renderValue?: (value: T) => string; - convertValue: (inputValue?: string) => T; + convertValue: (inputValue: string) => T; cellSx?: SxProps; inputSx?: SxProps; } @@ -25,7 +25,7 @@ const TableCellEdit = ({ inputSx, }: Props) => { const [editMode, setEditMode] = useState(false); - const [input, setInput] = useState(); + const [input, setInput] = useState(""); const inputRef = useRef(null); const onClick = useCallback(() => { @@ -41,7 +41,7 @@ const TableCellEdit = ({ const onBlur = useCallback(() => { setEditMode(false); onChange(convertValue(input)); - setInput(undefined); + setInput(""); }, [convertValue, input, onChange]); useEffect(() => { diff --git a/src/components/TimesheetTable/EntryInputTable.tsx b/src/components/TimesheetTable/EntryInputTable.tsx index 15dce8d..87eac77 100644 --- a/src/components/TimesheetTable/EntryInputTable.tsx +++ b/src/components/TimesheetTable/EntryInputTable.tsx @@ -37,7 +37,6 @@ type TimeEntryRow = Partial< _error: string; isPlanned: boolean; id: string; - taskGroupId: number; } >; @@ -221,6 +220,9 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => { valueOptions() { return assignedProjects.map((p) => ({ value: p.id, label: p.name })); }, + valueGetter({ value }) { + return value ?? ""; + }, }, { field: "taskGroupId", @@ -228,6 +230,9 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => { width: 200, editable: true, type: "singleSelect", + valueGetter({ value }) { + return value ?? ""; + }, valueOptions(params) { const updatedRow = params.id ? apiRef.current.getRowWithUpdatedValues(params.id, "") @@ -253,6 +258,9 @@ const EntryInputTable: React.FC = ({ day, assignedProjects }) => { width: 200, editable: true, type: "singleSelect", + valueGetter({ value }) { + return value ?? ""; + }, valueOptions(params) { const updatedRow = params.id ? apiRef.current.getRowWithUpdatedValues(params.id, "") From 966d7d0c65f37a801cd70b1aeb4d5342bb647356 Mon Sep 17 00:00:00 2001 From: Wayne Date: Wed, 1 May 2024 23:30:37 +0900 Subject: [PATCH 20/22] Add not-found page for edit project --- src/app/(main)/projects/edit/not-found.tsx | 17 ++++++++++++ .../projects/edit/{[projectId] => }/page.tsx | 26 ++++++++++++++----- src/app/utils/fetchUtil.ts | 15 ++++++++++- src/components/Breadcrumb/Breadcrumb.tsx | 1 + .../CreateProject/CreateProject.tsx | 22 +++++++++++----- .../ProjectSearch/ProjectSearch.tsx | 2 +- 6 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 src/app/(main)/projects/edit/not-found.tsx rename src/app/(main)/projects/edit/{[projectId] => }/page.tsx (69%) diff --git a/src/app/(main)/projects/edit/not-found.tsx b/src/app/(main)/projects/edit/not-found.tsx new file mode 100644 index 0000000..14e0e6d --- /dev/null +++ b/src/app/(main)/projects/edit/not-found.tsx @@ -0,0 +1,17 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("projects", "common"); + + return ( + + {t("Not Found")} + {t("The project was not found!")} + + {t("Return to all projects")} + + + ); +} diff --git a/src/app/(main)/projects/edit/[projectId]/page.tsx b/src/app/(main)/projects/edit/page.tsx similarity index 69% rename from src/app/(main)/projects/edit/[projectId]/page.tsx rename to src/app/(main)/projects/edit/page.tsx index dbad026..78e0ed1 100644 --- a/src/app/(main)/projects/edit/[projectId]/page.tsx +++ b/src/app/(main)/projects/edit/page.tsx @@ -12,23 +12,30 @@ import { } from "@/app/api/projects"; import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; +import { ServerFetchError } from "@/app/utils/fetchUtil"; import CreateProject from "@/components/CreateProject"; import { I18nProvider, getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; +import { isArray } from "lodash"; import { Metadata } from "next"; +import { notFound } from "next/navigation"; interface Props { - params: { - projectId: string; - }; + searchParams: { [key: string]: string | string[] | undefined }; } export const metadata: Metadata = { title: "Edit Project", }; -const Projects: React.FC = async ({ params }) => { +const Projects: React.FC = async ({ searchParams }) => { const { t } = await getServerI18n("projects"); + // Assume projectId is string here + const projectId = searchParams["id"]; + + if (!projectId || isArray(projectId)) { + notFound(); + } // Preload necessary dependencies fetchAllTasks(); @@ -46,14 +53,19 @@ const Projects: React.FC = async ({ params }) => { preloadTeamLeads(); preloadStaff(); - // TODO: Handle not found - const fetchedProject = await fetchProjectDetails(params.projectId); + try { + await fetchProjectDetails(projectId); + } catch (e) { + if (e instanceof ServerFetchError && e.response?.status === 404) { + notFound(); + } + } return ( <> {t("Edit Project")} - + ); diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 5060991..a519164 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -3,6 +3,16 @@ import { getServerSession } from "next-auth"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; +export class ServerFetchError extends Error { + public readonly response: Response | undefined; + constructor(message?: string, response?: Response) { + super(message); + this.response = response; + + Object.setPrototypeOf(this, ServerFetchError.prototype); + } +} + export const serverFetch: typeof fetch = async (input, init) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const session = await getServerSession(authOptions); @@ -37,7 +47,10 @@ export async function serverFetchJson(...args: FetchParams) { signOutUser(); default: console.error(await response.text()); - throw Error("Something went wrong fetching data in server."); + throw new ServerFetchError( + "Something went wrong fetching data in server.", + response, + ); } } } diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 3d6123a..a94670a 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -12,6 +12,7 @@ const pathToLabelMap: { [path: string]: string } = { "/home": "User Workspace", "/projects": "Projects", "/projects/create": "Create Project", + "/projects/edit": "Edit Project", "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/staffReimbursement": "Staff Reimbursement", diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 175d712..3a91a8f 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -36,6 +36,7 @@ import { StaffResult } from "@/app/api/staff"; import { Typography } from "@mui/material"; import { Grade } from "@/app/api/grades"; import { Customer, Subsidiary } from "@/app/api/customer"; +import { isEmpty } from "lodash"; export interface Props { isEditMode: boolean; @@ -108,13 +109,17 @@ const CreateProject: React.FC = ({ async (data) => { try { setServerError(""); - await saveProject(data); + if (isEditMode) { + console.log("edit project", data); + } else { + await saveProject(data); + } router.replace("/projects"); } catch (e) { setServerError(t("An error has occurred. Please try again later.")); } }, - [router, t], + [router, t, isEditMode], ); const onSubmitError = useCallback>( @@ -132,14 +137,19 @@ const CreateProject: React.FC = ({ ); const formProps = useForm({ - defaultValues: defaultInputs ?? { + defaultValues: { taskGroups: {}, allocatedStaffIds: [], milestones: {}, totalManhour: 0, - manhourPercentageByGrade: grades.reduce((acc, grade) => { - return { ...acc, [grade.id]: 1 / grades.length }; - }, {}), + ...defaultInputs, + + // manhourPercentageByGrade should have a sensible default + manhourPercentageByGrade: isEmpty(defaultInputs?.manhourPercentageByGrade) + ? grades.reduce((acc, grade) => { + return { ...acc, [grade.id]: 1 / grades.length }; + }, {}) + : defaultInputs.manhourPercentageByGrade, }, }); diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index d33ce14..1937fe2 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -55,7 +55,7 @@ const ProjectSearch: React.FC = ({ projects, projectCategories }) => { const onProjectClick = useCallback( (project: ProjectResult) => { - router.push(`/projects/edit/${project.id}`); + router.push(`/projects/edit?id=${project.id}`); }, [router], ); From ea4617a2dd316ef60b2bab4aac0eae1b79e47da7 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Thu, 2 May 2024 14:22:26 +0800 Subject: [PATCH 21/22] update --- src/app/api/team/index.ts | 1 + src/components/EditTeam/Allocation.tsx | 18 +++++++--- src/components/EditTeam/EditTeam.tsx | 39 +++++++++++++++++---- src/components/EditTeam/EditTeamWrapper.tsx | 30 +++++++++++----- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/app/api/team/index.ts b/src/app/api/team/index.ts index 3aa872d..d9d3b27 100644 --- a/src/app/api/team/index.ts +++ b/src/app/api/team/index.ts @@ -15,6 +15,7 @@ export interface TeamResult { staffName: string; posLabel: string; posCode: string; + teamLead: number; } diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 44c89b1..2599867 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -35,9 +35,10 @@ import StarsIcon from "@mui/icons-material/Stars"; export interface Props { allStaffs: StaffResult[]; + teamLead: number; } -const Allocation: React.FC = ({ allStaffs: staff }) => { +const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { const { t } = useTranslation(); const searchParams = useSearchParams(); const idString = searchParams.get("id"); @@ -53,9 +54,16 @@ const Allocation: React.FC = ({ allStaffs: staff }) => { const initialStaffs = staff.map((s) => ({ ...s })); const [filteredStaff, setFilteredStaff] = useState(initialStaffs); - const [selectedStaff, setSelectedStaff] = useState( - filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) + const [selectedStaff, setSelectedStaff] = useState(() => { + const rearrangedStaff = filteredStaff.sort((a, b) => { + if (a.id === teamLead) return -1; + if (b.id === teamLead) return 1; + return 0; + }); + return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) + } ); + console.log(filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id))) const [seletedTeamLead, setSeletedTeamLead] = useState(); const [deletedStaffIds, setDeletedStaffIds] = useState([]); @@ -84,8 +92,8 @@ const Allocation: React.FC = ({ allStaffs: staff }) => { }, getValues("addStaffIds") ); - console.log(rearrangedList); - console.log(selectedStaff); + // console.log(rearrangedList); + // console.log(selectedStaff); const rearrangedStaff = rearrangedList.map((id) => { return selectedStaff.find((staff) => staff.id === id); diff --git a/src/components/EditTeam/EditTeam.tsx b/src/components/EditTeam/EditTeam.tsx index 095559b..432bc0e 100644 --- a/src/components/EditTeam/EditTeam.tsx +++ b/src/components/EditTeam/EditTeam.tsx @@ -20,12 +20,15 @@ import { StaffResult } from "@/app/api/staff"; interface desc { id: number; + name: string; description: string; + teamLead: number; } interface Props { staff: StaffResult[]; desc: desc[]; + // teamLead: StaffResult[] } const EditTeam: React.FC = async ({ staff, desc }) => { @@ -37,6 +40,8 @@ const EditTeam: React.FC = async ({ staff, desc }) => { const [filteredItems, setFilteredItems] = useState(); const [allStaffs, setAllStaffs] = useState(); const [filteredDesc, setFilteredDesc] = useState(); + const [filteredName, setFilteredName] = useState(); + const [teamLead, setTeamLead] = useState(); const [tabIndex, setTabIndex] = useState(0); const router = useRouter(); // const [selectedStaff, setSelectedStaff] = useState( @@ -63,25 +68,47 @@ const EditTeam: React.FC = async ({ staff, desc }) => { ); useEffect(() => { let idList: number[] = [] + console.log(desc) if (idString) { const filteredTeam = staff.filter( - (item) => item.teamId === parseInt(idString) + (item) => { + console.log(item) + console.log(parseInt(idString)) + return (item.teamId === parseInt(idString))} ); + console.log(filteredTeam) const tempDesc = desc.filter( (item) => item.id === parseInt(idString) ) - + // const leader = teamLead.filter( + // (staff) => staff.teamId === parseInt(idString) + // ) + // console.log(leader) + console.log(tempDesc[0].teamLead) + setTeamLead(tempDesc[0].teamLead) if (filteredTeam.length > 0) { const filteredIds: number[] = filteredTeam.map((i) => ( i.id - )) + )) + + // const teamLead = tempDesc[0].teamLead + // const index = filteredIds.indexOf(teamLead); + + // if (index !== -1) { + // filteredIds.splice(index, 1); + // filteredIds.unshift(teamLead); + // } + idList = filteredIds + console.log(filteredIds) } - // console.log(filteredIds) + console.log(idList) setFilteredItems(filteredTeam); formProps.reset({description: tempDesc[0].description, addStaffIds: idList}) setFilteredDesc(tempDesc[0].description) + setFilteredName(tempDesc[0].name) } + console.log(staff) setAllStaffs(staff) @@ -139,7 +166,7 @@ const EditTeam: React.FC = async ({ staff, desc }) => { > - {t("Edit Team")} + {t("Edit Team")} - {filteredName} = async ({ staff, desc }) => { {tabIndex === 0 && } - {tabIndex === 1 && } + {tabIndex === 1 && } - - - )} + {isEditMode && !(formProps.getValues("projectDeleted") === true) && ( + + {!formProps.getValues("projectActualStart") && } + {formProps.getValues("projectActualStart") && !formProps.getValues("projectActualEnd") && } + {!(formProps.getValues("projectActualStart") && formProps.getValues("projectActualEnd")) && } + + )} = ({ > {t("Cancel")} - diff --git a/src/components/CustomerSave/CustomerSave.tsx b/src/components/CustomerSave/CustomerSave.tsx index fc2469e..acb4ecf 100644 --- a/src/components/CustomerSave/CustomerSave.tsx +++ b/src/components/CustomerSave/CustomerSave.tsx @@ -199,20 +199,20 @@ const CustomerSave: React.FC = ({ setServerError(""); submitDialog(async () => { - const response = await saveCustomer(data); - - if (response.message === "Success") { - successDialog(t("Submit Success"), t).then(() => { - router.replace("/settings/customer"); - }) - } else { - errorDialog(t("Submit Fail"), t).then(() => { - formProps.setError("code", { message: response.message, type: "custom" }) - setTabIndex(0) - return false - }) - } - }, t) + const response = await saveCustomer(data); + + if (response.message === "Success") { + successDialog(t("Submit Success"), t).then(() => { + router.replace("/settings/customer"); + }) + } else { + errorDialog(t("Submit Fail"), t).then(() => { + formProps.setError("code", { message: response.message, type: "custom" }) + setTabIndex(0) + return false + }) + } + }, t) } catch (e) { console.log(e) setServerError(t("An error has occurred. Please try again later.")); diff --git a/src/components/Swal/CustomAlerts.js b/src/components/Swal/CustomAlerts.js index 6eddfb2..668502c 100644 --- a/src/components/Swal/CustomAlerts.js +++ b/src/components/Swal/CustomAlerts.js @@ -50,13 +50,13 @@ export const warningDialog = (text, t) => { }) } -export const submitDialog = async (confirmAction, t) => { +export const submitDialog = async (confirmAction, t, {...props}) => { // const { t } = useTranslation("common") const result = await Swal.fire({ icon: "question", - title: t("Do you want to submit?"), + title: props.title ?? t("Do you want to submit?"), cancelButtonText: t("Cancel"), - confirmButtonText: t("Submit"), + confirmButtonText: props.confirmButtonText ?? t("Submit"), showCancelButton: true, showConfirmButton: true, });