Bổ sung nút report excel trang timekeeping , leave management

This commit is contained in:
Truong Vo 2024-12-05 17:25:07 +07:00
parent fcaa2926fd
commit 93030b73c7
10 changed files with 3291 additions and 5715 deletions

View File

@ -2,6 +2,7 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Exports\LeaveManagementExport;
use App\Http\Controllers\Controller;
use App\Jobs\InitializeLeaveDays;
use App\Models\LeaveDays;
@ -9,6 +10,7 @@ use App\Models\Notes;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
class LeaveManagementController extends Controller
{
@ -139,4 +141,20 @@ class LeaveManagementController extends Controller
return response()->json(['status' => true, 'message' => 'Updated successfully']);
}
public function export(Request $request)
{
$year = $request->query('year', now()->year);
$leaveDays = $this->getDataByYear($year);
if ($leaveDays->isEmpty()) {
return response()->json(['status' => false, 'message' => 'No data found']);
}
$currentDate = date('d_His');
return Excel::download(
new LeaveManagementExport($leaveDays),
"LeaveManagement_{$year}_{$currentDate}.xlsx"
);
}
}

View File

@ -15,6 +15,8 @@ use Illuminate\Support\Facades\DB;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\TimekeepingExport;
class TimekeepingController extends Controller
{
@ -149,7 +151,7 @@ class TimekeepingController extends Controller
return response()->json(['status' => true, 'message' => 'Add successfully']);
}
public function updateCacheMonth(Request $request)
@ -185,4 +187,38 @@ class TimekeepingController extends Controller
return response()->json(['message' => 'Delete fail', 'status' => false]);
}
public function export(Request $request)
{
// Validate request
$request->validate([
'month' => 'required|numeric|between:1,12',
'year' => 'required|numeric|min:2000',
'working_days' => 'required|numeric|between:1,31'
]);
// Reuse get() function to fetch data
$response = $this->get($request);
$responseData = json_decode($response->getContent(), true);
if (!$responseData['status']) {
return response()->json(['status' => false, 'message' => 'No data found']);
}
// Lọc chỉ lấy user có permission staff
$staffData = array_filter($responseData['data'], function($user) {
return isset($user['user']['permission']) && $user['user']['permission'] === 'staff';
});
$currentDate = date('d_His');
return Excel::download(
new TimekeepingExport(
array_values($staffData), // Convert to indexed array after filtering
$request->month,
$request->year,
$request->working_days
),
"Timekeeping_{$request->month}_{$request->year}_{$currentDate}.xlsx"
);
}
}

View File

@ -126,6 +126,7 @@ Route::middleware('api')
Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
Route::get('/export', [TimekeepingController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
@ -146,6 +147,7 @@ Route::middleware('api')
'prefix' => 'leave-management',
], function () {
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
});

View File

@ -0,0 +1,134 @@
<?php
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class LeaveManagementExport implements FromArray, WithHeadings, WithStyles
{
protected $data;
protected $year;
public function __construct($data)
{
$this->data = $data;
$this->year = Carbon::now()->year;
}
public function headings(): array
{
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return array_merge(
['No.', 'User'],
$months,
['Total', 'Off', 'Remaining', 'Notes']
);
}
public function array(): array
{
$headers = $this->headings(); // Lấy tiêu đề
$rows = [];
foreach ($this->data as $index => $user) {
$totalDayOff = 0;
$totalDayLeave = $user['leaveDay']['ld_day'] + $user['leaveDay']['ld_date_additional'];
// Tính tổng ngày nghỉ theo tháng
$monthlyLeaves = array_fill(1, 12, 0);
foreach ($user['monthlyLeaveDays'] as $leaveDay) {
$monthlyLeaves[$leaveDay['month']] += $leaveDay['leave_days'];
$totalDayOff += $leaveDay['leave_days'];
}
// Tạo dòng dữ liệu
$row = [
$index + 1,
$user['user']['name']
];
// Thêm dữ liệu các tháng
for ($month = 1; $month <= 12; $month++) {
$row[] = $monthlyLeaves[$month] ?: '';
}
// Thêm tổng số ngày
$row[] = $totalDayLeave;
$row[] = $totalDayOff;
$row[] = $totalDayLeave - $totalDayOff;
$row[] = $user['leaveDay']['ld_note'] ?? '';
$rows[] = $row;
}
return array_merge([$headers], $rows); // Thêm tiêu đề vào đầu mảng
}
public function styles(Worksheet $sheet)
{
$lastRow = count($this->data) + 2;
$lastColumn = 'R';
// Thêm và style title
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', "DANH SÁCH NGÀY NGHỈ NĂM {$this->year}");
$sheet->getStyle("A1:{$lastColumn}1")->applyFromArray([
'font' => [
'bold' => true,
'size' => 14
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Style cho header (dời xuống row 2)
$sheet->getStyle("A2:{$lastColumn}2")->applyFromArray([
'font' => ['bold' => true],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Style cho toàn bộ bảng (bắt đầu từ row 1)
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN
]
],
'alignment' => [
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Căn giữa cho các cột số liệu (điều chỉnh range bắt đầu từ row 3)
for ($col = 'C'; $col <= 'P'; $col++) {
$sheet->getStyle("{$col}3:{$col}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER);
}
// Set độ rộng cột
$sheet->getColumnDimension('A')->setWidth(5); // No.
$sheet->getColumnDimension('B')->setWidth(30); // User
// Các tháng
for ($i = 'C'; $i <= 'N'; $i++) {
$sheet->getColumnDimension($i)->setWidth(8);
}
$sheet->getColumnDimension('O')->setWidth(8); // Total
$sheet->getColumnDimension('P')->setWidth(8); // Off
$sheet->getColumnDimension('Q')->setWidth(12); // Remaining
$sheet->getColumnDimension('R')->setWidth(30); // Notes
return $sheet;
}
}

View File

@ -0,0 +1,184 @@
<?php
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class TimekeepingExport implements FromArray, WithHeadings, WithStyles
{
protected $data;
protected $month;
protected $year;
protected $workingDays;
protected $daysInMonth;
public function __construct($data, $month, $year, $workingDays)
{
$this->data = $data;
$this->month = $month;
$this->year = $year;
$this->workingDays = $workingDays;
$this->daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
}
public function array(): array
{
// Lấy headers
$headers = $this->headings();
// Lấy dữ liệu người dùng
$userRows = [];
foreach ($this->data as $user) {
// Kiểm tra permission staff
if (!isset($user['user']['permission']) || $user['user']['permission'] !== 'staff') {
continue;
}
$row = [
$user['user']['name'] ?? 'Unknown',
0, // Total days
$this->workingDays, // Off days (initialize with working days)
];
$totalDays = 0;
// Add data for each day in month
for ($day = 1; $day <= $this->daysInMonth; $day++) {
$dayData = '';
if (isset($user['history'])) {
foreach ($user['history'] as $history) {
if ($history['day'] === $day) {
$total = $history['total'] ?? 0;
if ($total >= 7 * 3600) {
$dayData = '1';
$totalDays += 1;
} else if ($total >= 3.5 * 3600) {
$dayData = '0.5';
$totalDays += 0.5;
}
break;
}
}
}
$row[] = $dayData;
}
// Update total and off days
$row[1] = $totalDays;
$row[2] = $this->workingDays - $totalDays;
// Add Notes column with formatted content
$notes = [];
if (isset($user['history'])) {
foreach ($user['history'] as $history) {
if (!empty($history['notes'])) {
$dayNotes = [];
foreach ($history['notes'] as $note) {
$dayNotes[] = "- {$note['reasonName']} ({$note['timeTypeName']}): {$note['note']}";
}
if (!empty($dayNotes)) {
$notes[] = "Day {$history['day']}:\n" . implode("\n", $dayNotes);
}
}
}
}
$row[] = !empty($notes) ? implode("\n\n", $notes) : '';
$userRows[] = $row;
}
return array_merge($headers, $userRows);
}
public function headings(): array
{
$firstRow = ['Day', '', ''];
// Second row: Day of week
$secondRow = ['', '', ''];
$date = Carbon::create($this->year, $this->month, 1);
for ($day = 1; $day <= $this->daysInMonth; $day++) {
$firstRow[] = $day;
$secondRow[] = $date->format('D');
$date->addDay();
}
// Add Notes column
$firstRow[] = 'Notes';
$secondRow[] = '';
return [$firstRow, $secondRow];
}
public function styles(Worksheet $sheet)
{
$lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
// Title and working days
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', "DANH SÁCH CHẤM CÔNG THÁNG {$this->month} NĂM {$this->year}");
$sheet->mergeCells("A2:{$lastColumn}2");
$sheet->setCellValue('A2', "Số ngày làm việc: {$this->workingDays}");
// Merge cells for "Day" title and set value
$sheet->mergeCells("A3:C3");
$sheet->setCellValue('A3', 'Day');
// Set values for A4, B4, C4
$sheet->setCellValue('A4', 'User');
$sheet->setCellValue('B4', 'Total');
$sheet->setCellValue('C4', 'Off');
// Calculate last row (2 title rows + 2 header rows + data rows)
$lastRow = count($this->data) + 4;
// Styling
$sheet->getStyle("A1:{$lastColumn}1")->getFont()->setBold(true)->setSize(14);
$sheet->getStyle("A2:{$lastColumn}2")->getFont()->setBold(true);
$sheet->getStyle("A3:{$lastColumn}4")->getFont()->setBold(true);
// Border style
$borderStyle = [
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
],
],
];
// Apply borders to the data area
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray($borderStyle);
// Center align all cells except Notes column
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Left align Notes column
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_LEFT);
// Set column widths
$sheet->getColumnDimension('A')->setWidth(30);
$sheet->getColumnDimension('B')->setWidth(10);
$sheet->getColumnDimension('C')->setWidth(10);
for ($i = 4; $i <= $this->daysInMonth + 3; $i++) {
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setWidth(5);
}
// Set width for Note column
$sheet->getColumnDimension($noteColumn)->setWidth(40);
return [];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
//Timekeeping
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
export const exportTimekeeping = API_URL + 'v1/admin/timekeeping/export'
export const updateMultipleUserWorkingTime =
API_URL + 'v1/admin/timekeeping/addMutilple'
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
@ -36,6 +37,7 @@ export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
export const updateNoteLeave =
API_URL + 'v1/admin/leave-management/saveNoteLeave'
export const exportLeaveManagement = API_URL + 'v1/admin/leave-management/export'
//Tickets
export const getTickets = API_URL + 'v1/admin/ticket/all'

View File

@ -1,6 +1,6 @@
import { getLeaveManagement, updateNoteLeave } from '@/api/Admin'
import { getLeaveManagement, updateNoteLeave, exportLeaveManagement } from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import { get, exportFile } from '@/rtk/helpers/apiService'
import {
Avatar,
Box,
@ -20,7 +20,7 @@ import { notifications } from '@mantine/notifications'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { IconEdit } from '@tabler/icons-react'
import { IconEdit, IconFileExcel } from '@tabler/icons-react'
import classes from './LeaveManagement.module.css'
@ -245,6 +245,29 @@ const LeaveManagement = () => {
})
}
const handleExport = async () => {
try {
const timestamp = moment().format('DDMMYYYY_HHmmss')
const fileName = `LeaveManagement_${date.year}_${timestamp}.xlsx`
await exportFile(
exportLeaveManagement,
{
year: parseInt(date.year)
},
fileName
)
} catch (error) {
console.error('Export error:', error)
notifications.show({
title: 'Error',
message: 'Export failed',
color: 'red',
})
}
}
return (
<div>
<div className={classes.title}>
@ -391,22 +414,18 @@ const LeaveManagement = () => {
pl={200}
style={{
display: 'flex',
// alignItems: 'end',
justifyContent: 'end',
}}
>
<Box display={'flex'} style={{ alignItems: 'end' }}>
{/* <Tooltip label="Save working days">
<Button
size="xs"
ml={'sm'}
onClick={() => {
//form add user new
}}
>
Add
</Button>
</Tooltip> */}
<Button
size="xs"
ml={'sm'}
onClick={handleExport}
leftSection={<IconFileExcel size={16} />}
>
Export Excel
</Button>
</Box>
</Box>
</Box>

View File

@ -1,5 +1,6 @@
import {
deleteNote,
exportTimekeeping,
getListMaster,
getTheTimesheet,
updateMultipleUserWorkingTime,
@ -7,7 +8,7 @@ import {
updateWorkingDays,
} from '@/api/Admin'
import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import { exportFile, get } from '@/rtk/helpers/apiService'
import {
Avatar,
Box,
@ -31,6 +32,7 @@ import {
IconPointFilled,
IconTrash,
IconX,
IconFileExcel,
} from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react'
@ -435,12 +437,29 @@ const Timekeeping = () => {
})
}
const handleExport = async () => {
try {
const timestamp = moment().format('DDMMYYYY_HHmmss')
const fileName = `Timekeeping_${date.month}_${date.year}_${timestamp}.xlsx`
await exportFile(
exportTimekeeping,
{
month: date.month,
year: date.year,
working_days: workingDays
},
fileName
)
} catch (error) {
console.error('Export error:', error)
}
}
return (
<div>
<div className={classes.title}>
<h3>
Timekeeping
</h3>
<h3>Timekeeping</h3>
</div>
<Drawer
opened={opened1}
@ -511,7 +530,6 @@ const Timekeeping = () => {
<span style={{ paddingLeft: '10px', paddingRight: '10px' }}>|</span>
<span style={{ fontWeight: 'bold' }}>Day</span>: {customAddNotes.day}
</p>
<Select
mb={'md'}
searchable
@ -688,6 +706,14 @@ const Timekeeping = () => {
Save
</Button>
</Tooltip>
<Button
onClick={handleExport}
size="xs"
ml="xl"
leftSection={<IconFileExcel size={16} />}
>
Export Excel
</Button>
</Box>
</Box>
<Box
@ -866,13 +892,22 @@ const Timekeeping = () => {
// offset={{ mainAxis: 5, crossAxis: 0 }}
label={showTooltipAllNote(user)}
>
<div style={{display:'flex', alignItems:'center'}}><Avatar size={'md'} mr={'md'} src={import.meta.env.VITE_BACKEND_URL.includes('local')
? import.meta.env.VITE_BACKEND_URL +
'storage/' +
user.user.avatar
: import.meta.env.VITE_BACKEND_URL +
'image/storage/' +
user.user.avatar}/>{user.user.name}</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar
size={'md'}
mr={'md'}
src={
import.meta.env.VITE_BACKEND_URL.includes('local')
? import.meta.env.VITE_BACKEND_URL +
'storage/' +
user.user.avatar
: import.meta.env.VITE_BACKEND_URL +
'image/storage/' +
user.user.avatar
}
/>
{user.user.name}
</div>
</Tooltip>
</Table.Td>
<Table.Td ta={'center'}>{totalDays}</Table.Td>

View File

@ -3,6 +3,7 @@ import { getFormDataHeader, getHeaderInfo } from '@/rtk/helpers/tokenCreator'
import { removeTokens } from '@/rtk/localStorage'
import { notifications } from '@mantine/notifications'
import axios from 'axios'
import moment from 'moment'
const handleResponse = (response: any) => {
if (response.status === 401) {
removeTokens()
@ -156,3 +157,45 @@ export const postImage = async (url: string, body: any, method: any) => {
throw handleResponse(err.response)
}
}
export const exportFile = async (url: string, params: any = {}, fileName: string) => {
const header = await getHeaderInfo()
try {
const response = await axios.get(url, {
...header,
params,
responseType: 'blob'
})
if (response.status === 200) {
const blob = new Blob([response.data])
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName
document.body.appendChild(link)
link.click()
window.URL.revokeObjectURL(downloadUrl)
document.body.removeChild(link)
// notifications.show({
// title: 'Success',
// message: 'Export successfully',
// color: 'green',
// })
return true
}
throw new Error('Export failed')
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message || 'Export failed',
color: 'red',
})
throw error
}
}