Add electricity bills module (API, model, PDF) #156
|
|
@ -0,0 +1,295 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Admin\app\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Traits\HasFilterRequest;
|
||||||
|
use App\Traits\HasOrderByRequest;
|
||||||
|
use App\Traits\HasSearchRequest;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Modules\Admin\app\Models\ElectricityBill;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class ElectricityBillController extends Controller
|
||||||
|
{
|
||||||
|
use HasOrderByRequest;
|
||||||
|
use HasFilterRequest;
|
||||||
|
use HasSearchRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all electricity bills with pagination
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$bills = new ElectricityBill;
|
||||||
|
|
||||||
|
// Order by
|
||||||
|
$this->orderByRequest($bills, $request);
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
$this->filterRequest(
|
||||||
|
builder: $bills,
|
||||||
|
request: $request,
|
||||||
|
filterKeys: [
|
||||||
|
'billing_date' => self::F_TEXT,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Search
|
||||||
|
$this->searchRequest(
|
||||||
|
builder: $bills,
|
||||||
|
value: $request->get('search'),
|
||||||
|
fields: ['billing_date', 'notes']
|
||||||
|
);
|
||||||
|
|
||||||
|
$responseData = $bills
|
||||||
|
->leftJoin('users as creator', 'electricity_bills.created_by', '=', 'creator.id')
|
||||||
|
->leftJoin('users as updater', 'electricity_bills.updated_by', '=', 'updater.id')
|
||||||
|
->orderBy('electricity_bills.billing_date', 'desc')
|
||||||
|
->select(
|
||||||
|
'electricity_bills.*',
|
||||||
|
'creator.name as creator_name',
|
||||||
|
'updater.name as updater_name'
|
||||||
|
)
|
||||||
|
->paginate($request->get('per_page', 15));
|
||||||
|
|
||||||
|
return $this->ResultSuccess($responseData);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching electricity bills: ' . $e->getMessage());
|
||||||
|
return $this->ResultError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new electricity bill
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'billing_date' => 'required|string',
|
||||||
|
'previous_reading' => 'required|numeric|min:0',
|
||||||
|
'current_reading' => 'required|numeric|min:0',
|
||||||
|
'unit_price' => 'required|numeric|min:0',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if billing_date already exists
|
||||||
|
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])->first();
|
||||||
|
if ($existingBill) {
|
||||||
|
return $this->ResultError('Bill for this month already exists', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total amount
|
||||||
|
$consumption = $validated['current_reading'] - $validated['previous_reading'];
|
||||||
|
$totalAmount = $consumption * $validated['unit_price'];
|
||||||
|
|
||||||
|
$bill = ElectricityBill::create([
|
||||||
|
'billing_date' => $validated['billing_date'],
|
||||||
|
'previous_reading' => $validated['previous_reading'],
|
||||||
|
'current_reading' => $validated['current_reading'],
|
||||||
|
'unit_price' => $validated['unit_price'],
|
||||||
|
'total_amount' => $totalAmount,
|
||||||
|
'notes' => $validated['notes'] ?? null,
|
||||||
|
'created_by' => auth('admins')->user()->id ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->ResultSuccess($bill, 'Electricity bill created successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating electricity bill: ' . $e->getMessage());
|
||||||
|
return $this->ResultError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update electricity bill
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'billing_date' => 'sometimes|string',
|
||||||
|
'previous_reading' => 'sometimes|numeric|min:0',
|
||||||
|
'current_reading' => 'sometimes|numeric|min:0',
|
||||||
|
'unit_price' => 'sometimes|numeric|min:0',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$bill = ElectricityBill::findOrFail($id);
|
||||||
|
|
||||||
|
// Check if billing_date already exists (excluding current record)
|
||||||
|
if (isset($validated['billing_date'])) {
|
||||||
|
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])
|
||||||
|
->where('id', '!=', $id)
|
||||||
|
->first();
|
||||||
|
if ($existingBill) {
|
||||||
|
return $this->ResultError('Bill for this month already exists', 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate total if any reading or price changed
|
||||||
|
$previousReading = $validated['previous_reading'] ?? $bill->previous_reading;
|
||||||
|
$currentReading = $validated['current_reading'] ?? $bill->current_reading;
|
||||||
|
$unitPrice = $validated['unit_price'] ?? $bill->unit_price;
|
||||||
|
$consumption = $currentReading - $previousReading;
|
||||||
|
$totalAmount = $consumption * $unitPrice;
|
||||||
|
|
||||||
|
$bill->update(array_merge($validated, [
|
||||||
|
'total_amount' => $totalAmount,
|
||||||
|
'updated_by' => auth('admins')->user()->id ?? null,
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $this->ResultSuccess($bill, 'Electricity bill updated successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating electricity bill: ' . $e->getMessage());
|
||||||
|
return $this->ResultError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete electricity bill
|
||||||
|
*/
|
||||||
|
public function delete(Request $request, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$bill = ElectricityBill::findOrFail($id);
|
||||||
|
$bill->delete();
|
||||||
|
|
||||||
|
return $this->ResultSuccess(null, 'Electricity bill deleted successfully');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting electricity bill: ' . $e->getMessage());
|
||||||
|
return $this->ResultError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export electricity bill to PDF
|
||||||
|
*/
|
||||||
|
public function exportPdf(Request $request, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$bill = ElectricityBill::findOrFail($id);
|
||||||
|
|
||||||
|
// Get month name from billing_date
|
||||||
|
$consumption = $bill->current_reading - $bill->previous_reading;
|
||||||
|
$totalText = $this->numberToVietnamese($bill->total_amount);
|
||||||
|
$date = Carbon::parse($bill->billing_date);
|
||||||
|
|
||||||
|
$dateNow = 'Ngày ' . $date->day .
|
||||||
|
' tháng ' . $date->month .
|
||||||
|
' năm ' . $date->year;
|
||||||
|
|
||||||
|
// Generate PDF
|
||||||
|
$pdf = Pdf::loadView('admin::admin.electricity_bills.pdf', [
|
||||||
|
'bill' => $bill,
|
||||||
|
'consumption' => $consumption,
|
||||||
|
'dateNow' => $dateNow,
|
||||||
|
'totalText' => $totalText
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fileName = 'electricity_bill_' . $bill->billing_date . '.pdf';
|
||||||
|
$filePath = 'electricity_bills/' . $fileName;
|
||||||
|
|
||||||
|
// đảm bảo folder tồn tại
|
||||||
|
if (!Storage::disk('public')->exists('electricity_bills')) {
|
||||||
|
Storage::disk('public')->makeDirectory('electricity_bills');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 render 1 lần
|
||||||
|
$pdfContent = $pdf->output();
|
||||||
|
|
||||||
|
// 👇 lưu file
|
||||||
|
Storage::disk('public')->put($filePath, $pdfContent);
|
||||||
|
|
||||||
|
// update DB
|
||||||
|
$bill->update(['file_path' => $filePath]);
|
||||||
|
|
||||||
|
// 👇 trả về đúng file đã tạo
|
||||||
|
return response($pdfContent)
|
||||||
|
->header('Content-Type', 'application/pdf')
|
||||||
|
->header('Content-Disposition', 'attachment; filename="' . $fileName . '"');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error exporting electricity bill to PDF: ' . $e->getMessage());
|
||||||
|
return $this->ResultError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get electricity bill by ID
|
||||||
|
*/
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$bill = ElectricityBill::with(['creator', 'updater'])->findOrFail($id);
|
||||||
|
return $this->ResultSuccess($bill);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching electricity bill: ' . $e->getMessage());
|
||||||
|
return $this->ResultError($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberToVietnamese($number)
|
||||||
|
{
|
||||||
|
$units = ["", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín"];
|
||||||
|
$levels = ["", "nghìn", "triệu", "tỷ"];
|
||||||
|
|
||||||
|
if ($number == 0) return "không đồng";
|
||||||
|
|
||||||
|
$number = (int)$number;
|
||||||
|
$result = "";
|
||||||
|
$level = 0;
|
||||||
|
|
||||||
|
while ($number > 0) {
|
||||||
|
$threeDigits = $number % 1000;
|
||||||
|
if ($threeDigits != 0) {
|
||||||
|
$result = $this->readThreeDigits($threeDigits, $units) . " " . $levels[$level] . " " . $result;
|
||||||
|
}
|
||||||
|
$number = floor($number / 1000);
|
||||||
|
$level++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ucfirst(trim(preg_replace('/\s+/', ' ', $result))) . " đồng";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readThreeDigits($number, $units)
|
||||||
|
{
|
||||||
|
$hundreds = floor($number / 100);
|
||||||
|
$tens = floor(($number % 100) / 10);
|
||||||
|
$ones = $number % 10;
|
||||||
|
|
||||||
|
$result = "";
|
||||||
|
|
||||||
|
if ($hundreds > 0) {
|
||||||
|
$result .= $units[$hundreds] . " trăm";
|
||||||
|
if ($tens == 0 && $ones > 0) {
|
||||||
|
$result .= " lẻ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tens > 1) {
|
||||||
|
$result .= " " . $units[$tens] . " mươi";
|
||||||
|
if ($ones == 1) {
|
||||||
|
$result .= " mốt";
|
||||||
|
} elseif ($ones == 5) {
|
||||||
|
$result .= " lăm";
|
||||||
|
} elseif ($ones > 0) {
|
||||||
|
$result .= " " . $units[$ones];
|
||||||
|
}
|
||||||
|
} elseif ($tens == 1) {
|
||||||
|
$result .= " mười";
|
||||||
|
if ($ones == 5) {
|
||||||
|
$result .= " lăm";
|
||||||
|
} elseif ($ones > 0) {
|
||||||
|
$result .= " " . $units[$ones];
|
||||||
|
}
|
||||||
|
} elseif ($ones > 0) {
|
||||||
|
$result .= " " . $units[$ones];
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Admin\app\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Traits\HasCacheModel;
|
||||||
|
|
||||||
|
class ElectricityBill extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
use HasCacheModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->table = 'electricity_bills';
|
||||||
|
$this->guarded = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total amount based on reading difference and unit price
|
||||||
|
*/
|
||||||
|
public function calculateTotal(): float
|
||||||
|
{
|
||||||
|
$consumption = $this->current_reading - $this->previous_reading;
|
||||||
|
return round($consumption * $this->unit_price, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get consumption in kWh
|
||||||
|
*/
|
||||||
|
public function getConsumption(): float
|
||||||
|
{
|
||||||
|
return $this->current_reading - $this->previous_reading;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user who created this record
|
||||||
|
*/
|
||||||
|
public function creator()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user who updated this record
|
||||||
|
*/
|
||||||
|
public function updater()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Bảng kê thanh toán tiền điện</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: DejaVu Sans, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.mt-10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.no-border {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.signature {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
.signature td {
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h3 class="text-center">BẢNG KÊ THANH TOÁN TIỀN ĐIỆN</h3>
|
||||||
|
<p class="text-center">({{ $dateNow ?? '' }})</p>
|
||||||
|
|
||||||
|
<div class="mt-20">
|
||||||
|
<p>- Tên doanh nghiệp: Công ty TNHH Kỹ Thuật Công Nghệ APAC</p>
|
||||||
|
<p>- Mã số thuế: 0110038408</p>
|
||||||
|
<p>- Địa chỉ: Số 219/26/3 đường Lĩnh Nam, Phường Vĩnh Hưng, thành phố Hà Nội, Việt Nam</p>
|
||||||
|
<p>- Tên chủ sở hữu cho thuê địa điểm sản xuất kinh doanh: Lâm Văn Mười</p>
|
||||||
|
<p>- Địa chỉ thuê: 50B31 tại Khu dân cư 91B giai đoạn 3, phường Tân An, thành phố Cần Thơ</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Số điện đầu kỳ</th>
|
||||||
|
<th>Số điện cuối kỳ</th>
|
||||||
|
<th>Số điện tiêu thụ</th>
|
||||||
|
<th>Đơn giá</th>
|
||||||
|
<th>Thành tiền</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ number_format($bill->previous_reading) ?? 0 }}</td>
|
||||||
|
<td>{{ number_format($bill->current_reading) ?? 0 }}</td>
|
||||||
|
<td>{{ $consumption ?? 0 }}</td>
|
||||||
|
<td>{{ number_format($bill->unit_price) ?? '0' }}</td>
|
||||||
|
<td>{{ number_format($bill->total_amount) ?? '0' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="mt-20">
|
||||||
|
- Tổng tiền thanh toán: <strong>{{ number_format($bill->total_amount) ?? '0' }} VND</strong>
|
||||||
|
({{ $totalText ?? '' }})
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="signature no-border">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Người lập bảng kê<br>
|
||||||
|
(Ký, ghi rõ họ tên)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Đại diện doanh nghiệp<br>
|
||||||
|
(Ký, ghi rõ họ tên)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -23,6 +23,7 @@ use Modules\Admin\app\Http\Controllers\ProjectReviewController;
|
||||||
use Modules\Admin\app\Http\Controllers\ProfileController;
|
use Modules\Admin\app\Http\Controllers\ProfileController;
|
||||||
use Modules\Admin\app\Http\Controllers\TechnicalController;
|
use Modules\Admin\app\Http\Controllers\TechnicalController;
|
||||||
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
|
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
|
||||||
|
use Modules\Admin\app\Http\Controllers\ElectricityBillController;
|
||||||
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
|
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -173,6 +174,18 @@ Route::middleware('api')
|
||||||
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
|
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Electricity Bills
|
||||||
|
Route::group([
|
||||||
|
'prefix' => 'electricity-bill',
|
||||||
|
], function () {
|
||||||
|
Route::get('/', [ElectricityBillController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
Route::get('/{id}', [ElectricityBillController::class, 'show'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
Route::post('/create', [ElectricityBillController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
Route::put('/{id}', [ElectricityBillController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
Route::get('/delete/{id}', [ElectricityBillController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
Route::get('/export-pdf/{id}', [ElectricityBillController::class, 'exportPdf'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
});
|
||||||
|
|
||||||
Route::group([
|
Route::group([
|
||||||
'prefix' => 'profile',
|
'prefix' => 'profile',
|
||||||
], function () {
|
], function () {
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,21 @@ use Illuminate\Routing\Controller as BaseController;
|
||||||
class Controller extends BaseController
|
class Controller extends BaseController
|
||||||
{
|
{
|
||||||
use AuthorizesRequests, ValidatesRequests;
|
use AuthorizesRequests, ValidatesRequests;
|
||||||
|
|
||||||
|
protected function ResultSuccess($data = null, $message = 'Success', $code = 200)
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => $data
|
||||||
|
], $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function ResultError($message = 'Error', $code = 500)
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message
|
||||||
|
], $code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,16 @@
|
||||||
"name": "laravel/laravel",
|
"name": "laravel/laravel",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"description": "The skeleton application for the Laravel framework.",
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
"keywords": ["laravel", "framework"],
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"framework"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"barryvdh/laravel-debugbar": "^3.9",
|
"barryvdh/laravel-debugbar": "^3.9",
|
||||||
|
"barryvdh/laravel-dompdf": "^2.0",
|
||||||
|
"doctrine/dbal": "^3.10",
|
||||||
"drnxloc/laravel-simple-html-dom": "^1.9",
|
"drnxloc/laravel-simple-html-dom": "^1.9",
|
||||||
"guzzlehttp/guzzle": "^7.8",
|
"guzzlehttp/guzzle": "^7.8",
|
||||||
"laravel/framework": "^10.10",
|
"laravel/framework": "^10.10",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('electricity_bills', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->date('billing_date')->comment('Ngày lập hóa đơn');
|
||||||
|
$table->decimal('previous_reading', 12, 2)->comment('Số điện kỳ trước');
|
||||||
|
$table->decimal('current_reading', 12, 2)->comment('Số điện kỳ này');
|
||||||
|
$table->decimal('unit_price', 12, 2)->comment('Đơn giá điện');
|
||||||
|
$table->decimal('total_amount', 12, 2)->comment('Tổng tiền điện');
|
||||||
|
$table->string('notes')->nullable()->comment('Ghi chú');
|
||||||
|
$table->string('file_path')->nullable()->comment('Đường dẫn file PDF');
|
||||||
|
$table->unsignedBigInteger('created_by')->nullable();
|
||||||
|
$table->unsignedBigInteger('updated_by')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
|
||||||
|
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('electricity_bills');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -125,6 +125,19 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete'
|
||||||
// Download File
|
// Download File
|
||||||
export const downloadFile = API_URL + 'v1/admin/download-file'
|
export const downloadFile = API_URL + 'v1/admin/download-file'
|
||||||
|
|
||||||
|
// Electricity Bills
|
||||||
|
export const getElectricityBills = API_URL + 'v1/admin/electricity-bill'
|
||||||
|
export const getElectricityBillById = (id: number) =>
|
||||||
|
API_URL + `v1/admin/electricity-bill/${id}`
|
||||||
|
export const createElectricityBill =
|
||||||
|
API_URL + 'v1/admin/electricity-bill/create'
|
||||||
|
export const updateElectricityBill = (id: number) =>
|
||||||
|
API_URL + `v1/admin/electricity-bill/${id}`
|
||||||
|
export const deleteElectricityBill = (id: number) =>
|
||||||
|
API_URL + `v1/admin/electricity-bill/delete/${id}`
|
||||||
|
export const exportElectricityBillPdf = (id: number) =>
|
||||||
|
API_URL + `v1/admin/electricity-bill/export-pdf/${id}`
|
||||||
|
|
||||||
// Files APIs
|
// Files APIs
|
||||||
export const getFiles = API_URL + 'v1/admin/profile/files'
|
export const getFiles = API_URL + 'v1/admin/profile/files'
|
||||||
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
|
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,14 @@ export const DataTableAll = ({
|
||||||
if (query !== '') {
|
if (query !== '') {
|
||||||
setTData(
|
setTData(
|
||||||
data.filter((obj) =>
|
data.filter((obj) =>
|
||||||
Object.values(obj)?.find((c: any) => c.toString().normalize('NFC').toLowerCase().includes(query.normalize('NFC').toLowerCase())))
|
Object.values(obj)?.find((c: any) =>
|
||||||
|
c
|
||||||
|
.toString()
|
||||||
|
.normalize('NFC')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(query.normalize('NFC').toLowerCase()),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (pagination) {
|
if (pagination) {
|
||||||
|
|
@ -456,7 +463,7 @@ export const DataTablePagination = ({
|
||||||
})
|
})
|
||||||
const [selectedRows, setSelectedRows] = useState<any[]>([])
|
const [selectedRows, setSelectedRows] = useState<any[]>([])
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const urlParams = new URLSearchParams(location.search)
|
let urlParams = new URLSearchParams(location.search)
|
||||||
|
|
||||||
// Render headers
|
// Render headers
|
||||||
const headers = columns.map((col) => (
|
const headers = columns.map((col) => (
|
||||||
|
|
@ -596,7 +603,7 @@ export const DataTablePagination = ({
|
||||||
|
|
||||||
// Remove specific parameters
|
// Remove specific parameters
|
||||||
params.delete(name)
|
params.delete(name)
|
||||||
|
urlParams.delete(name)
|
||||||
// Update the URL without reloading the page
|
// Update the URL without reloading the page
|
||||||
window.history.replaceState({}, document.title, url.toString())
|
window.history.replaceState({}, document.title, url.toString())
|
||||||
}
|
}
|
||||||
|
|
@ -628,9 +635,9 @@ export const DataTablePagination = ({
|
||||||
Array.isArray(dataFilter[key])
|
Array.isArray(dataFilter[key])
|
||||||
? dataFilter[key]
|
? dataFilter[key]
|
||||||
: key === 'to_date'
|
: key === 'to_date'
|
||||||
? Math.floor(dataFilter[key].getTime() / 1000) +
|
? Math.floor(dataFilter[key].getTime() / 1000) +
|
||||||
(60 * 60 * 23 + 60 * 59 + 59)
|
(60 * 60 * 23 + 60 * 59 + 59)
|
||||||
: Math.floor(dataFilter[key].getTime() / 1000),
|
: Math.floor(dataFilter[key].getTime() / 1000),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -660,9 +667,8 @@ export const DataTablePagination = ({
|
||||||
date_used_to: date_used,
|
date_used_to: date_used,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all attributes in 'params' to URL params
|
// Add all attributes in 'params' to URL params
|
||||||
Object.entries(params).forEach((param) => urlParams.set(...param))
|
urlParams = new URLSearchParams(Object.entries(params))
|
||||||
Object.entries(dataFilter).forEach(([key, value]) => {
|
Object.entries(dataFilter).forEach(([key, value]) => {
|
||||||
const typeFilter = filterInfo.find((o) => o.key === key).type
|
const typeFilter = filterInfo.find((o) => o.key === key).type
|
||||||
const hasType = {
|
const hasType = {
|
||||||
|
|
@ -679,17 +685,16 @@ export const DataTablePagination = ({
|
||||||
if (hasType.Date) {
|
if (hasType.Date) {
|
||||||
value = value ? Date.parse(String(value)) / 1000 : '' // to unix timestamp
|
value = value ? Date.parse(String(value)) / 1000 : '' // to unix timestamp
|
||||||
}
|
}
|
||||||
|
console.log(String(value))
|
||||||
String(value).length
|
String(value).length
|
||||||
? urlParams.set(key, String(value))
|
? urlParams.set(key, String(value))
|
||||||
: urlParams.delete(key)
|
: urlParams.delete(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Request to get data API
|
// Request to get data API
|
||||||
const res = await get(url, Object.fromEntries(urlParams.entries()))
|
const res = await get(url, Object.fromEntries(urlParams.entries()))
|
||||||
if (res.status) {
|
if (res.status || res.success) {
|
||||||
setBaseData(res)
|
setBaseData(res.success ? res?.data : res)
|
||||||
setTData(res.data)
|
setTData(res.success ? res.data.data : res.data)
|
||||||
setSkeletion(false)
|
setSkeletion(false)
|
||||||
navigate({
|
navigate({
|
||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
|
|
@ -765,7 +770,7 @@ export const DataTablePagination = ({
|
||||||
|
|
||||||
if (order_by_) {
|
if (order_by_) {
|
||||||
const sortParam = {
|
const sortParam = {
|
||||||
name: order_by_.split('=')[0].split('_')[2],
|
name: order_by_.split('=')[0].split('_').slice(2).join('_'),
|
||||||
status: order_by_.split('=')[1],
|
status: order_by_.split('=')[1],
|
||||||
}
|
}
|
||||||
if (JSON.stringify(sortParam) !== JSON.stringify(statusSort)) {
|
if (JSON.stringify(sortParam) !== JSON.stringify(statusSort)) {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import {
|
||||||
IconReport,
|
IconReport,
|
||||||
IconScan,
|
IconScan,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
|
IconShredder,
|
||||||
IconSun,
|
IconSun,
|
||||||
IconTicket,
|
IconTicket,
|
||||||
IconUsersGroup,
|
IconUsersGroup,
|
||||||
|
|
@ -156,6 +157,13 @@ const data = [
|
||||||
group: 'admin',
|
group: 'admin',
|
||||||
permissions: 'admin,accountant',
|
permissions: 'admin,accountant',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: '/office-support',
|
||||||
|
label: 'Office Support',
|
||||||
|
icon: IconShredder,
|
||||||
|
group: 'other',
|
||||||
|
permissions: 'admin,hr,accountant',
|
||||||
|
},
|
||||||
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
||||||
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||||
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
.title {
|
||||||
|
background-color: light-dark(var(white), var(--mantine-color-dark-7));
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
|
||||||
|
var(--mantine-spacing-sm);
|
||||||
|
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionIcon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteIcon {
|
||||||
|
color: red;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.editIcon {
|
||||||
|
color: rgb(9, 132, 132);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editIcon:hover {
|
||||||
|
background-color: rgba(203, 203, 203, 0.809);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteIcon:hover {
|
||||||
|
background-color: rgba(203, 203, 203, 0.809);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: light-dark(white, #2d353c);
|
||||||
|
text-align: center;
|
||||||
|
border: solid 1px rgb(255, 145, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogText {
|
||||||
|
color: light-dark(#2d353c, white);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,655 @@
|
||||||
|
import {
|
||||||
|
createElectricityBill,
|
||||||
|
deleteElectricityBill,
|
||||||
|
exportElectricityBillPdf,
|
||||||
|
getElectricityBills,
|
||||||
|
updateElectricityBill,
|
||||||
|
} from '@/api/Admin'
|
||||||
|
import { DataTablePagination } from '@/components/DataTable/DataTable'
|
||||||
|
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||||
|
import { get, post, put } from '@/rtk/helpers/apiService'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
Group,
|
||||||
|
Modal,
|
||||||
|
NumberInput,
|
||||||
|
Select,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
Tabs,
|
||||||
|
Flex,
|
||||||
|
ActionIcon,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { useForm } from '@mantine/form'
|
||||||
|
import { notifications } from '@mantine/notifications'
|
||||||
|
import {
|
||||||
|
IconDownload,
|
||||||
|
IconEdit,
|
||||||
|
IconFileInvoice,
|
||||||
|
IconHistory,
|
||||||
|
IconTrash,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import classes from './OfficeSupport.module.css'
|
||||||
|
import { _NOTIFICATION_MESS } from '@/rtk/helpers/notificationMess'
|
||||||
|
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
|
||||||
|
import { DateInput, DateTimePicker } from '@mantine/dates'
|
||||||
|
|
||||||
|
interface ElectricityBill {
|
||||||
|
id: number
|
||||||
|
billing_date: string
|
||||||
|
previous_reading: number
|
||||||
|
current_reading: number
|
||||||
|
unit_price: number
|
||||||
|
total_amount: number
|
||||||
|
notes: string | null
|
||||||
|
file_path: string | null
|
||||||
|
created_by: number | null
|
||||||
|
updated_by: number | null
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
creator_name?: string
|
||||||
|
updater_name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElectricityBillsResponse {
|
||||||
|
data: ElectricityBill[]
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
per_page: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const OfficeSupport = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>('calculate')
|
||||||
|
const [listBills, setListBills] = useState<ElectricityBillsResponse>({
|
||||||
|
data: [],
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 15,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
const [action, setAction] = useState('')
|
||||||
|
const [item, setItem] = useState<ElectricityBill | null>(null)
|
||||||
|
const [activeBtn, setActiveBtn] = useState(false)
|
||||||
|
const [disableBtn, setDisableBtn] = useState(false)
|
||||||
|
|
||||||
|
const [confirmModal, setConfirmModal] = useState(false)
|
||||||
|
const [confirmMessage, setConfirmMessage] = useState('')
|
||||||
|
const [confirmValues, setConfirmValues] = useState<any>(null)
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false)
|
||||||
|
const filterInfo: any[] = []
|
||||||
|
|
||||||
|
const getAllBills = async (page: number = 1) => {
|
||||||
|
try {
|
||||||
|
const params = { page }
|
||||||
|
const res = await get(getElectricityBills, params)
|
||||||
|
if (res?.data) {
|
||||||
|
setListBills(res?.data)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message ?? error,
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAllBills()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'billing_date',
|
||||||
|
size: '15%',
|
||||||
|
header: 'Date',
|
||||||
|
render: (row: ElectricityBill) => {
|
||||||
|
const date = new Date(row.billing_date)
|
||||||
|
return <Text fz={'sm'}>{moment(date).format('DD MMMM YYYY')}</Text>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'previous_reading',
|
||||||
|
size: '15%',
|
||||||
|
header: 'Previous Reading',
|
||||||
|
render: (row: ElectricityBill) => (
|
||||||
|
<Text fz={'sm'}>
|
||||||
|
{Number(row.previous_reading)?.toLocaleString()} kWh
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'current_reading',
|
||||||
|
size: '15%',
|
||||||
|
header: 'Current Reading',
|
||||||
|
render: (row: ElectricityBill) => (
|
||||||
|
<Text fz={'sm'}>
|
||||||
|
{Number(row.current_reading)?.toLocaleString()} kWh
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#',
|
||||||
|
size: '10%',
|
||||||
|
header: 'Consumption',
|
||||||
|
render: (row: ElectricityBill) => {
|
||||||
|
const consumption =
|
||||||
|
Number(row.current_reading) - Number(row.previous_reading)
|
||||||
|
return (
|
||||||
|
<Text fz={'sm'} fw={600}>
|
||||||
|
{consumption.toLocaleString()} kWh
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'unit_price',
|
||||||
|
size: '10%',
|
||||||
|
header: 'Unit Price',
|
||||||
|
render: (row: ElectricityBill) => (
|
||||||
|
<Text fz={'sm'}>
|
||||||
|
{Number(row.unit_price)?.toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
})}{' '}
|
||||||
|
VNĐ
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total_amount',
|
||||||
|
size: '15%',
|
||||||
|
header: 'Total Amount',
|
||||||
|
render: (row: ElectricityBill) => (
|
||||||
|
<Text fz={'sm'} fw={700} c="green">
|
||||||
|
{Number(row.total_amount)?.toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
})}{' '}
|
||||||
|
VNĐ
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
size: '15%',
|
||||||
|
header: 'Actions',
|
||||||
|
render: (row: ElectricityBill) => {
|
||||||
|
return (
|
||||||
|
<Group gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
disabled={disableBtn}
|
||||||
|
onClick={() => handleExportPdf(row.id, row.billing_date)}
|
||||||
|
variant="outline"
|
||||||
|
w={20}
|
||||||
|
h={20}
|
||||||
|
color={'blue'}
|
||||||
|
>
|
||||||
|
<IconDownload className={classes.deleteIcon} color="blue" />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={disableBtn}
|
||||||
|
onClick={() => {
|
||||||
|
setItem(row)
|
||||||
|
setAction('edit')
|
||||||
|
form.setFieldValue(
|
||||||
|
'billing_date',
|
||||||
|
row?.billing_date || moment().format('YYYY-MM-DD'),
|
||||||
|
)
|
||||||
|
form.setFieldValue(
|
||||||
|
'current_reading',
|
||||||
|
Number(row?.current_reading) || 0,
|
||||||
|
)
|
||||||
|
form.setFieldValue(
|
||||||
|
'previous_reading',
|
||||||
|
Number(row?.previous_reading) || 0,
|
||||||
|
)
|
||||||
|
form.setFieldValue('unit_price', row?.unit_price || 4000)
|
||||||
|
form.setFieldValue('notes', row?.notes || '')
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
w={20}
|
||||||
|
h={20}
|
||||||
|
color={'green'}
|
||||||
|
>
|
||||||
|
<IconEdit className={classes.deleteIcon} color="green" />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
disabled={disableBtn}
|
||||||
|
onClick={() => {
|
||||||
|
setAction('delete')
|
||||||
|
setItem(row)
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
w={20}
|
||||||
|
h={20}
|
||||||
|
color={'red'}
|
||||||
|
>
|
||||||
|
<IconTrash className={classes.deleteIcon} color="red" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleCreate = async (values: any) => {
|
||||||
|
try {
|
||||||
|
setDisableBtn(true)
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
billing_date: values.billing_date,
|
||||||
|
previous_reading: values.previous_reading,
|
||||||
|
current_reading: values.current_reading,
|
||||||
|
unit_price: values.unit_price,
|
||||||
|
notes: values.notes || null,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (action === 'add') {
|
||||||
|
res = await post(createElectricityBill, params)
|
||||||
|
} else if (action === 'edit' && item) {
|
||||||
|
res = await put(updateElectricityBill(item.id), params)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res?.success) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Success',
|
||||||
|
message:
|
||||||
|
action === 'add'
|
||||||
|
? _NOTIFICATION_MESS.create_success
|
||||||
|
: 'Updated successfully',
|
||||||
|
color: 'green',
|
||||||
|
})
|
||||||
|
setAction('')
|
||||||
|
form.reset()
|
||||||
|
|
||||||
|
// Auto export PDF after creating
|
||||||
|
if (action === 'add' && res.data?.id) {
|
||||||
|
handleExportPdf(res.data.id, res.data.billing_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllBills()
|
||||||
|
} else if (!res?.success && res?.errors) {
|
||||||
|
if (!res?.data?.success && res?.data?.message) {
|
||||||
|
setConfirmMessage(res.data?.message)
|
||||||
|
setConfirmValues(values)
|
||||||
|
setConfirmModal(true)
|
||||||
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: res.message ?? _NOTIFICATION_MESS.create_error,
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.message) {
|
||||||
|
const errorMess = error.response.message
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: errorMess,
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setDisableBtn(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await Xdelete(deleteElectricityBill(id), {}, () => getAllBills())
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExportPdf = async (id: number, date: string) => {
|
||||||
|
try {
|
||||||
|
setDisableBtn(true)
|
||||||
|
const header = await getHeaderInfo()
|
||||||
|
const res = await fetch(exportElectricityBillPdf(id), { ...header })
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error('Export failed')
|
||||||
|
|
||||||
|
const blob = await res.blob()
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const newDate = moment(new Date(date)).format('DD-M-YYYY')
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `Bảng thanh toán tiền điện APAC - ${newDate}.pdf`
|
||||||
|
a.click()
|
||||||
|
|
||||||
|
// notifications.show({
|
||||||
|
// title: 'Success',
|
||||||
|
// message: 'PDF exported successfully',
|
||||||
|
// color: 'green',
|
||||||
|
// })
|
||||||
|
setDisableBtn(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
setDisableBtn(false)
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message,
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLastReading = () => {
|
||||||
|
if (!listBills?.data?.length) return 0
|
||||||
|
|
||||||
|
const sorted = [...listBills.data].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.billing_date).getTime() - new Date(a.billing_date).getTime(),
|
||||||
|
)
|
||||||
|
return sorted[0] ? Number(sorted[0]?.current_reading) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
id: 0,
|
||||||
|
billing_date: moment().format('YYYY-MM-DD'),
|
||||||
|
previous_reading: 0,
|
||||||
|
current_reading: 0,
|
||||||
|
unit_price: 4000,
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
billing_date: (value) => (!value ? 'Date is required' : null),
|
||||||
|
previous_reading: (value) =>
|
||||||
|
value < 0 ? 'Previous reading must be positive' : null,
|
||||||
|
current_reading: (value) =>
|
||||||
|
value < 0 ? 'Current reading must be positive' : null,
|
||||||
|
unit_price: (value) =>
|
||||||
|
value <= 0 ? 'Unit price must be greater than 0' : null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate preview
|
||||||
|
const calculatePreview = () => {
|
||||||
|
const consumption =
|
||||||
|
form.values.current_reading - form.values.previous_reading
|
||||||
|
const total = consumption * form.values.unit_price
|
||||||
|
return { consumption, total }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={classes.title}>
|
||||||
|
<h3>Office Support</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Box p={20}>
|
||||||
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="calculate"
|
||||||
|
leftSection={<IconFileInvoice size={16} />}
|
||||||
|
>
|
||||||
|
Electricity Bill
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="calculate" pt="md">
|
||||||
|
{/* Calculate Tab Content */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
m={5}
|
||||||
|
onClick={() => {
|
||||||
|
setAction('add')
|
||||||
|
form.reset()
|
||||||
|
form.setFieldValue('previous_reading', getLastReading())
|
||||||
|
form.setFieldValue('current_reading', getLastReading())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Add New Bill
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{/* History Tab Content */}
|
||||||
|
{listBills.data.length > 0 ? (
|
||||||
|
<DataTablePagination
|
||||||
|
filterInfo={filterInfo}
|
||||||
|
data={listBills}
|
||||||
|
columns={columns}
|
||||||
|
searchInput
|
||||||
|
size=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">
|
||||||
|
No electricity bills found. Go to Calculate tab to add new bill.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Add/Edit Modal */}
|
||||||
|
<Modal
|
||||||
|
opened={action === 'add' || action === 'edit'}
|
||||||
|
onClose={() => {
|
||||||
|
setAction('')
|
||||||
|
setItem(null)
|
||||||
|
form.reset()
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Text pl={'sm'} fw={700} fz={'lg'}>
|
||||||
|
{action === 'add' && 'Add Electricity Bill'}
|
||||||
|
{action === 'edit' && 'Edit Electricity Bill'}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (values) => {
|
||||||
|
setDisableBtn(true)
|
||||||
|
await handleCreate(values)
|
||||||
|
setDisableBtn(false)
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box pl={'md'} pr={'md'}>
|
||||||
|
<DateInput
|
||||||
|
required
|
||||||
|
mb="md"
|
||||||
|
label="Billing Date"
|
||||||
|
placeholder="Pick date"
|
||||||
|
valueFormat="DD-MM-YYYY"
|
||||||
|
value={
|
||||||
|
form.values.billing_date
|
||||||
|
? new Date(form.values.billing_date)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
error={form.errors.billing_date}
|
||||||
|
onChange={(date) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
'billing_date',
|
||||||
|
date ? moment(date).format('YYYY-MM-DD') : '',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex gap={'md'}>
|
||||||
|
<Box style={{ flex: 1 }}>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
mb={'md'}
|
||||||
|
label={'Previous Reading (kWh)'}
|
||||||
|
value={form.values.previous_reading}
|
||||||
|
error={form.errors.previous_reading}
|
||||||
|
onChange={(e) =>
|
||||||
|
form.setFieldValue('previous_reading', Number(e))
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
thousandSeparator=","
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box style={{ flex: 1 }}>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
mb={'md'}
|
||||||
|
label={'Current Reading (kWh)'}
|
||||||
|
value={form.values.current_reading}
|
||||||
|
error={form.errors.current_reading}
|
||||||
|
onChange={(e) =>
|
||||||
|
form.setFieldValue('current_reading', Number(e))
|
||||||
|
}
|
||||||
|
min={0}
|
||||||
|
thousandSeparator=","
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
mb={'md'}
|
||||||
|
label={'Unit Price (VNĐ/kWh)'}
|
||||||
|
value={form.values.unit_price}
|
||||||
|
error={form.errors.unit_price}
|
||||||
|
onChange={(e) => form.setFieldValue('unit_price', Number(e))}
|
||||||
|
min={0}
|
||||||
|
thousandSeparator=","
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
<Box
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fw={600} mb="sm">
|
||||||
|
Preview:
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Consumption:{' '}
|
||||||
|
<Text span fw={600}>
|
||||||
|
{calculatePreview().consumption.toLocaleString()} kWh
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Total Amount:{' '}
|
||||||
|
<Text span fw={700} c="green" size="lg">
|
||||||
|
{calculatePreview().total.toLocaleString()} VNĐ
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box ta={'center'}>
|
||||||
|
<Button
|
||||||
|
mt={'lg'}
|
||||||
|
bg={'green'}
|
||||||
|
type="submit"
|
||||||
|
disabled={disableBtn}
|
||||||
|
>
|
||||||
|
{action === 'add' ? 'Create & Export PDF' : 'Update'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog
|
||||||
|
className={classes.dialog}
|
||||||
|
opened={action === 'delete'}
|
||||||
|
withCloseButton
|
||||||
|
onClose={() => setAction('')}
|
||||||
|
size="lg"
|
||||||
|
radius="md"
|
||||||
|
position={{ top: 30, right: 10 }}
|
||||||
|
>
|
||||||
|
<Text className={classes.dialogText} size="sm" mb="xs" fw={500}>
|
||||||
|
Do you want to delete this record?
|
||||||
|
<Group justify="center" m={10}>
|
||||||
|
<Button
|
||||||
|
disabled={activeBtn}
|
||||||
|
fw={700}
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
onClick={async () => {
|
||||||
|
setActiveBtn(true)
|
||||||
|
if (item) {
|
||||||
|
await handleDelete(item.id)
|
||||||
|
}
|
||||||
|
setActiveBtn(false)
|
||||||
|
setAction('')
|
||||||
|
setItem(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fw={700}
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
setAction('')
|
||||||
|
setItem(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Text>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Confirm Modal */}
|
||||||
|
<Modal
|
||||||
|
opened={confirmModal}
|
||||||
|
onClose={() => !confirmLoading && setConfirmModal(false)}
|
||||||
|
title={
|
||||||
|
<Text fw={700} fz="lg">
|
||||||
|
Warning
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
centered
|
||||||
|
closeOnClickOutside={!confirmLoading}
|
||||||
|
closeOnEscape={!confirmLoading}
|
||||||
|
>
|
||||||
|
<Box p="md">
|
||||||
|
<Text style={{ whiteSpace: 'pre-line' }} mb={20}>
|
||||||
|
{confirmMessage}
|
||||||
|
</Text>
|
||||||
|
<Group justify="center">
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
loading={confirmLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
if (confirmValues) {
|
||||||
|
try {
|
||||||
|
setConfirmLoading(true)
|
||||||
|
await handleCreate(confirmValues)
|
||||||
|
setConfirmLoading(false)
|
||||||
|
setConfirmModal(false)
|
||||||
|
} catch (error) {
|
||||||
|
setConfirmLoading(false)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
disabled={confirmLoading}
|
||||||
|
onClick={() => {
|
||||||
|
setConfirmModal(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OfficeSupport
|
||||||
|
|
@ -8,6 +8,7 @@ import PageLogin from '@/pages/Auth/Login/Login'
|
||||||
import Document from '@/pages/Document/Document'
|
import Document from '@/pages/Document/Document'
|
||||||
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||||
import PageNotFound from '@/pages/NotFound/NotFound'
|
import PageNotFound from '@/pages/NotFound/NotFound'
|
||||||
|
import OfficeSupport from '@/pages/OfficeSupport/OfficeSupport'
|
||||||
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
|
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
|
||||||
import Profile from '@/pages/Profile/Profile'
|
import Profile from '@/pages/Profile/Profile'
|
||||||
import SprintReview from '@/pages/SprintReview/SprintReview'
|
import SprintReview from '@/pages/SprintReview/SprintReview'
|
||||||
|
|
@ -264,6 +265,20 @@ const mainRoutes = [
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/office-support',
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute mode="route" permission="admin,hr,accountant">
|
||||||
|
<BasePage
|
||||||
|
main={
|
||||||
|
<>
|
||||||
|
<OfficeSupport />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
></BasePage>
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/packages',
|
// path: '/packages',
|
||||||
// element: (
|
// element: (
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@ export const create = async (
|
||||||
if (res.status === false) {
|
if (res.status === false) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: <div style={{ whiteSpace: 'pre-line' }}>{res.message ?? _NOTIFICATION_MESS.create_error}</div>,
|
message: (
|
||||||
|
<div style={{ whiteSpace: 'pre-line' }}>
|
||||||
|
{res.message ?? _NOTIFICATION_MESS.create_error}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
color: 'red',
|
color: 'red',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +120,7 @@ export const Xdelete = async (url: string, data: any, fnc?: () => void) => {
|
||||||
try {
|
try {
|
||||||
const res = await get(url, data)
|
const res = await get(url, data)
|
||||||
|
|
||||||
if (res.status) {
|
if (res.status || res.success) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: _NOTIFICATION_MESS.delete_success,
|
message: _NOTIFICATION_MESS.delete_success,
|
||||||
|
|
@ -124,7 +128,7 @@ export const Xdelete = async (url: string, data: any, fnc?: () => void) => {
|
||||||
})
|
})
|
||||||
fnc && fnc()
|
fnc && fnc()
|
||||||
}
|
}
|
||||||
if (res.status === false) {
|
if (res.status === false && !res.success) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: res.message ?? _NOTIFICATION_MESS.delete_error,
|
message: res.message ?? _NOTIFICATION_MESS.delete_error,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue