Add electricity bills module (API, model, PDF)
Introduce an Electricity Bills feature: adds ElectricityBill model, controller with CRUD + PDF export, Blade PDF template, and a migration to create the electricity_bills table. Registers routes (with admin permission middleware) and persists generated PDFs to public storage. Adds ResultSuccess/ResultError helpers to base Controller and updates composer to include dompdf and DBAL dependencies. Also includes frontend updates (Admin API, DataTable, Navbar, new OfficeSupport page and styles, route and CRUD helper adjustments) to support the new functionality.
This commit is contained in:
parent
de351f0877
commit
baa3216c69
|
|
@ -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\TechnicalController;
|
||||
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
|
||||
use Modules\Admin\app\Http\Controllers\ElectricityBillController;
|
||||
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');
|
||||
});
|
||||
|
||||
// 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([
|
||||
'prefix' => 'profile',
|
||||
], function () {
|
||||
|
|
|
|||
|
|
@ -9,4 +9,21 @@ use Illuminate\Routing\Controller as BaseController;
|
|||
class Controller extends BaseController
|
||||
{
|
||||
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",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-debugbar": "^3.9",
|
||||
"barryvdh/laravel-dompdf": "^2.0",
|
||||
"doctrine/dbal": "^3.10",
|
||||
"drnxloc/laravel-simple-html-dom": "^1.9",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"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
|
||||
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
|
||||
export const getFiles = API_URL + 'v1/admin/profile/files'
|
||||
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
|
||||
|
|
|
|||
|
|
@ -250,7 +250,14 @@ export const DataTableAll = ({
|
|||
if (query !== '') {
|
||||
setTData(
|
||||
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 {
|
||||
if (pagination) {
|
||||
|
|
@ -456,7 +463,7 @@ export const DataTablePagination = ({
|
|||
})
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([])
|
||||
const navigate = useNavigate()
|
||||
const urlParams = new URLSearchParams(location.search)
|
||||
let urlParams = new URLSearchParams(location.search)
|
||||
|
||||
// Render headers
|
||||
const headers = columns.map((col) => (
|
||||
|
|
@ -596,7 +603,7 @@ export const DataTablePagination = ({
|
|||
|
||||
// Remove specific parameters
|
||||
params.delete(name)
|
||||
|
||||
urlParams.delete(name)
|
||||
// Update the URL without reloading the page
|
||||
window.history.replaceState({}, document.title, url.toString())
|
||||
}
|
||||
|
|
@ -628,9 +635,9 @@ export const DataTablePagination = ({
|
|||
Array.isArray(dataFilter[key])
|
||||
? dataFilter[key]
|
||||
: key === 'to_date'
|
||||
? Math.floor(dataFilter[key].getTime() / 1000) +
|
||||
(60 * 60 * 23 + 60 * 59 + 59)
|
||||
: Math.floor(dataFilter[key].getTime() / 1000),
|
||||
? Math.floor(dataFilter[key].getTime() / 1000) +
|
||||
(60 * 60 * 23 + 60 * 59 + 59)
|
||||
: Math.floor(dataFilter[key].getTime() / 1000),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -660,9 +667,8 @@ export const DataTablePagination = ({
|
|||
date_used_to: date_used,
|
||||
})
|
||||
}
|
||||
|
||||
// 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]) => {
|
||||
const typeFilter = filterInfo.find((o) => o.key === key).type
|
||||
const hasType = {
|
||||
|
|
@ -679,17 +685,16 @@ export const DataTablePagination = ({
|
|||
if (hasType.Date) {
|
||||
value = value ? Date.parse(String(value)) / 1000 : '' // to unix timestamp
|
||||
}
|
||||
|
||||
console.log(String(value))
|
||||
String(value).length
|
||||
? urlParams.set(key, String(value))
|
||||
: urlParams.delete(key)
|
||||
})
|
||||
|
||||
// Request to get data API
|
||||
const res = await get(url, Object.fromEntries(urlParams.entries()))
|
||||
if (res.status) {
|
||||
setBaseData(res)
|
||||
setTData(res.data)
|
||||
if (res.status || res.success) {
|
||||
setBaseData(res.success ? res?.data : res)
|
||||
setTData(res.success ? res.data.data : res.data)
|
||||
setSkeletion(false)
|
||||
navigate({
|
||||
pathname: location.pathname,
|
||||
|
|
@ -765,7 +770,7 @@ export const DataTablePagination = ({
|
|||
|
||||
if (order_by_) {
|
||||
const sortParam = {
|
||||
name: order_by_.split('=')[0].split('_')[2],
|
||||
name: order_by_.split('=')[0].split('_').slice(2).join('_'),
|
||||
status: order_by_.split('=')[1],
|
||||
}
|
||||
if (JSON.stringify(sortParam) !== JSON.stringify(statusSort)) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
IconReport,
|
||||
IconScan,
|
||||
IconSettings,
|
||||
IconShredder,
|
||||
IconSun,
|
||||
IconTicket,
|
||||
IconUsersGroup,
|
||||
|
|
@ -156,6 +157,13 @@ const data = [
|
|||
group: 'admin',
|
||||
permissions: 'admin,accountant',
|
||||
},
|
||||
{
|
||||
link: '/office-support',
|
||||
label: 'Office Support',
|
||||
icon: IconShredder,
|
||||
group: 'other',
|
||||
permissions: 'admin,hr,accountant',
|
||||
},
|
||||
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
||||
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||
// { 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 LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||
import PageNotFound from '@/pages/NotFound/NotFound'
|
||||
import OfficeSupport from '@/pages/OfficeSupport/OfficeSupport'
|
||||
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
|
||||
import Profile from '@/pages/Profile/Profile'
|
||||
import SprintReview from '@/pages/SprintReview/SprintReview'
|
||||
|
|
@ -264,6 +265,20 @@ const mainRoutes = [
|
|||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/office-support',
|
||||
element: (
|
||||
<ProtectedRoute mode="route" permission="admin,hr,accountant">
|
||||
<BasePage
|
||||
main={
|
||||
<>
|
||||
<OfficeSupport />
|
||||
</>
|
||||
}
|
||||
></BasePage>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// path: '/packages',
|
||||
// element: (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ export const create = async (
|
|||
if (res.status === false) {
|
||||
notifications.show({
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
|
@ -116,7 +120,7 @@ export const Xdelete = async (url: string, data: any, fnc?: () => void) => {
|
|||
try {
|
||||
const res = await get(url, data)
|
||||
|
||||
if (res.status) {
|
||||
if (res.status || res.success) {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: _NOTIFICATION_MESS.delete_success,
|
||||
|
|
@ -124,7 +128,7 @@ export const Xdelete = async (url: string, data: any, fnc?: () => void) => {
|
|||
})
|
||||
fnc && fnc()
|
||||
}
|
||||
if (res.status === false) {
|
||||
if (res.status === false && !res.success) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: res.message ?? _NOTIFICATION_MESS.delete_error,
|
||||
|
|
|
|||
Loading…
Reference in New Issue