ساختار پروژه امیر
این راهنما ساختار کلی پروژه امیر و نحوه سازماندهی کدها را توضیح میدهد.
🏗️ ساختار کلی پروژه
امیر بر اساس Laravel Framework پیادهسازی شده و از معماری MVC پیروی میکند.
FreeAmir/
├── app/ # منطق اصلی اپلیکیشن
│ ├── Console/ # دستورات Artisan سفارشی
│ ├── Enums/ # انواع دادههای ثابت
│ ├── Helpers/ # توابع کمکی
│ ├── Http/ # کنترلرها، میدلویرها و درخواستها
│ ├── Models/ # مدلهای Eloquent
│ ├── Providers/ # ارائهدهندگان سرویس
│ ├── Services/ # منطق کسبوکار پیچیده
│ └── View/ # کامپوننتهای View
├── config/ # فایلهای پیکربندی
├── database/ # مایگریشنها و سیدرها
├── docs/ # مستندات پروژه
├── public/ # فایلهای عمومی
├── resources/ # ویوها، CSS، JS
├── routes/ # تعریف مسیرها
├── storage/ # فایلهای موقت و لاگها
├── tests/ # تستهای خودکار
└── vendor/ # پکیجهای Composer
📂 توضیح مسیرهای اصلی
مسیر app/ - هسته اپلیکیشن
Console/Commands/
دستورات سفارشی Artisan برای عملیات خاص:
// Example: دستور مدیریت سال مالی
php artisan fiscal-year:export --year=1403
php artisan fiscal-year:import --file=data.json --year=1404
/Enums
انواع دادههای ثابت پروژه:
// app/Enums/FiscalYearSection.php
enum FiscalYearSection: string
{
case SUBJECTS = 'subjects';
case CUSTOMERS = 'customers';
case PRODUCTS = 'products';
// ...
}
Helpers/
توابع کمکی عمومی:
- فایل
helpers.php- توابع عمومی - فایل
jdf.php- توابع تبدیل تاریخ فارسی - فایل
NumberToWordHelper.php- تبدیل عدد به حروف
Http/
لایه HTTP اپلیکیشن:
Http/
├── Controllers/ # کنترلرها
│ ├── Auth/ # احراز هویت
│ ├── Management/ # مدیریت کاربران و نقشها
│ ├── DocumentController.php
│ ├── InvoiceController.php
│ └── ...
├── Middleware/ # میدلویرها
├── Requests/ # اعتبارسنجی درخواستها
└── Kernel.php # تنظیمات HTTP
Models/
مدلهای Eloquent موجود در همین پوشه منطق داده را مدیریت میکنند. مهمترین فایلها عبارتاند از:
- مدل
Document.php– مدیریت اسناد حسابداری و ارتباط آنها با تراکنشها. - مدل
Transaction.php– ثبت تراکنشهای مرتبط با اسناد و سناریوهای فروش. - مدل
Subject.php– ساختار درختی سرفصلها و روابط والد/فرزند آنها. - مدل
Company.php– اطلاعات شرکت و نگهداشتن شناسه شرکت فعال. - مدل
User.php– کاربران سیستم و ارتباط آنها با شرکتها. - مدلهای
Customer.phpوCustomerGroup.php– مدیریت مشتریان و گروهبندی آنها. - مدلهای
Product.phpوProductGroup.php– کالاها و گروههای کالایی. - مدلهای
Invoice.phpوInvoiceItem.php– فاکتورهای فروش و اقلامشان. - مدلهای
Bank.php،BankAccount.php،Cheque.phpوChequeHistory.php– مدیریت اطلاعات بانکی و چکها. - مدلهای
Config.phpوPayment.php– پیکربندی سیستم و پرداختها.
زیرپوشه Scopes/ شامل FiscalYearScope.php است که بر روی مدلهای مرتبط اعمال میشود تا دادهها به شرکت/سال فعال محدود شوند.
نکته: مدلی با نام
FiscalYear.phpدر پروژه وجود ندارد؛ مدیریت سال/شرکت فعال از طریق مدلCompanyو همین اسکوپ انجام میشود.
Services/
منطق کسبوکار پیچیده:
// app/Services/DocumentService.php
class DocumentService
{
public static function createDocument(User $user, array $data, array $transactions)
{
// منطق ایجاد سند با کنترل موازنه
}
}
// app/Services/FiscalYearService.php
class FiscalYearService
{
public static function exportData($fiscalYearId, array $sections)
{
// خروجی گرفتن دادههای سال مالی
}
}
🎛️ پیکربندی (config/)
فایلهای پیکربندی مهم:
// config/app.php - تنظیمات کلی
'locale' => 'fa', // زبان پیشفرض فارسی
// config/database.php - تنظیمات پایگاه داده
'default' => env('DB_CONNECTION', 'mysql'),
// config/permission.php - تنظیمات نقشها و مجوزها
📊 پایگاه داده (database/)
migrations/
تعریف ساختار جداول:
// Example: مایگریشن جدول documents
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->decimal('number', 16, 2)->nullable();
$table->string('title')->nullable();
$table->date('date')->nullable();
$table->date('approved_at')->nullable();
$table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('approver_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('company_id')->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
seeders/
دادههای اولیه:
- فایل
DatabaseSeeder.php- دادههای ضروری سیستم - فایل
DemoSeeder.php- دادههای نمایشی
🎨 منابع (resources/)
views/
قالبهای Blade:
views/
├── layouts/ # قالبهای اصلی
├── documents/ # صفحات مربوط به اسناد
├── reports/ # صفحات گزارشها
├── auth/ # صفحات احراز هویت
└── components/ # کامپوننتهای قابل استفاده مجدد
فایلهای js/ و css/
فایلهای JavaScript و CSS با Vite مدیریت میشوند.
🛣️ مسیریابی (routes/)
web.php
مسیرهای وب اپلیکیشن:
use App\Http\Controllers;
use Illuminate\Support\Facades\Route;
Route::get('/login', [Controllers\Auth\LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [Controllers\Auth\LoginController::class, 'login']);
Route::get('/logout', [Controllers\Auth\LoginController::class, 'logout'])->name('logout');
Route::get('/about', [Controllers\AboutController::class, 'index'])->name('about')->middleware('auth');
Route::group(['middleware' => ['auth', 'ensure-employee'], 'prefix' => 'employee-portal', 'as' => 'employee-portal.'], function () {
Route::get('/employee', [Controllers\EmployeePortalController::class, 'employeeShow'])->name('employee.show');
Route::get('/change-employee-information', [Controllers\EmployeePortalController::class, 'changeEmployeeInformation'])->name('change-employee-information');
Route::put('/change-employee-information', [Controllers\EmployeePortalController::class, 'updateEmployeeInformation'])->name('update-employee-information');
Route::get('/', [Controllers\EmployeePortalController::class, 'dashboard'])->name('dashboard');
Route::get('/attendance-logs', [Controllers\EmployeePortalController::class, 'attendanceLogs'])->name('attendance-logs');
Route::get('/monthly-attendances', [Controllers\EmployeePortalController::class, 'monthlyAttendances'])->name('monthly-attendances');
Route::get('/monthly-attendances/{monthly_attendance}', [Controllers\EmployeePortalController::class, 'monthlyAttendanceShow'])->name('monthly-attendances.show');
Route::get('/payrolls', [Controllers\EmployeePortalController::class, 'payrolls'])->name('payrolls');
Route::get('/payrolls/{payroll}', [Controllers\EmployeePortalController::class, 'payrollShow'])->name('payrolls.show');
Route::get('/personnel-requests', [Controllers\EmployeePortalController::class, 'personnelRequests'])->name('personnel-requests.index');
Route::get('/personnel-requests/create', [Controllers\EmployeePortalController::class, 'createPersonnelRequest'])->name('personnel-requests.create');
Route::post('/personnel-requests', [Controllers\EmployeePortalController::class, 'storePersonnelRequest'])->name('personnel-requests.store');
Route::get('/personnel-requests/{personnel_request}/edit', [Controllers\EmployeePortalController::class, 'editPersonnelRequest'])->name('personnel-requests.edit');
Route::put('/personnel-requests/{personnel_request}', [Controllers\EmployeePortalController::class, 'updatePersonnelRequest'])->name('personnel-requests.update');
Route::delete('/personnel-requests/{personnel_request}', [Controllers\EmployeePortalController::class, 'destroyPersonnelRequest'])->name('personnel-requests.destroy');
});
Route::group(['middleware' => ['auth', 'check-permission']], function () {
Route::put('/about/change-global-configs', [Controllers\AboutController::class, 'updateGlobalConfigs'])->name('update-global-configs');
Route::get('api-tokens', [Controllers\ApiTokenController::class, 'index'])->name('api-tokens.index');
Route::get('api-tokens/create', [Controllers\ApiTokenController::class, 'create'])->name('api-tokens.create');
Route::post('api-tokens', [Controllers\ApiTokenController::class, 'store'])->name('api-tokens.store');
Route::delete('api-tokens/{tokenId}', [Controllers\ApiTokenController::class, 'destroy'])->name('api-tokens.destroy');
Route::get('change-company/{company}', [Controllers\CompanyController::class, 'setActiveCompany'])->name('change-company');
Route::post('/seed-demo-data', [Controllers\HomeController::class, 'seedDemoData'])->name('home.seed-demo-data');
Route::post('/refresh-database', [Controllers\HomeController::class, 'refreshDatabase'])->name('home.refresh-database');
Route::group(['prefix' => 'backups', 'as' => 'backups.'], function () {
Route::get('/create', [Controllers\BackupController::class, 'create'])->name('create');
Route::get('/upload', [Controllers\BackupController::class, 'upload'])->name('upload');
Route::get('/document-files-size', [Controllers\BackupController::class, 'documentFilesSize'])->name('document-files-size');
Route::post('/export', [Controllers\BackupController::class, 'export'])->name('export');
Route::post('/import', [Controllers\BackupController::class, 'import'])->name('import');
});
Route::get('/', [Controllers\HomeController::class, 'index'])->name('home');
Route::get('/home/cash-banks', [Controllers\HomeController::class, 'cashAndBanksBalances'])->name('home.cash-banks');
Route::get('/home/bank-account', [Controllers\HomeController::class, 'bankAccount'])->name('home.bank-account');
Route::get('subjects/search', [Controllers\SubjectController::class, 'search'])->name('subjects.search');
Route::get('subjects/search-code', [Controllers\SubjectController::class, 'searchCode'])->name('subjects.search-code');
Route::resource('subjects', Controllers\SubjectController::class);
Route::post('documents/{document}/change-status', [Controllers\DocumentController::class, 'changeStatus'])->name('documents.change-status');
Route::post('documents/approve-all', [Controllers\DocumentController::class, 'approveAll'])->name('documents.approve-all');
Route::get('documents/search-account-balance', [Controllers\DocumentController::class, 'searchAccountBalance'])->name('documents.search-account-balance');
Route::get('documents/sort-numbers', [Controllers\DocumentController::class, 'sortNumbers'])->name('documents.sort-numbers');
Route::post('documents/sort-numbers/start', [Controllers\DocumentController::class, 'startSorting'])->name('documents.sort-numbers.start');
Route::post('documents/sort-numbers/process', [Controllers\DocumentController::class, 'processSorting'])->name('documents.sort-numbers.process');
Route::get('documents/export', [Controllers\DocumentController::class, 'exportForm'])->name('documents.export');
Route::post('documents/export', [Controllers\DocumentController::class, 'export'])->name('documents.export.download');
Route::get('documents/import', [Controllers\DocumentController::class, 'importForm'])->name('documents.import');
Route::post('documents/import', [Controllers\DocumentController::class, 'import'])->name('documents.import.store');
Route::resource('documents', Controllers\DocumentController::class);
Route::get('documents/{document}/print', [Controllers\DocumentController::class, 'print'])->name('documents.print');
Route::get('documents/{document}/duplicate', [Controllers\DocumentController::class, 'duplicate'])->name('documents.duplicate');
Route::post('documents/{document}/transfer', [Controllers\DocumentController::class, 'transfer'])->name('documents.transfer');
Route::resource('transactions', Controllers\TransactionController::class)->only(['index', 'show']);
Route::get('products/search-product-group', [Controllers\ProductController::class, 'searchProductGroup'])->name('products.search-product-group');
Route::get('products/report', [Controllers\ProductController::class, 'report'])->name('products.report');
Route::get('products/export', [Controllers\ProductController::class, 'export'])->name('products.export');
Route::get('products/import', [Controllers\ProductController::class, 'importForm'])->name('products.import');
Route::post('products/import', [Controllers\ProductController::class, 'import'])->name('products.import.store');
Route::resource('products', Controllers\ProductController::class);
Route::resource('product-groups', Controllers\ProductGroupController::class);
Route::get('services/search-service-group', [Controllers\ServiceController::class, 'searchServiceGroup'])->name('services.search-service-group');
Route::get('services/export', [Controllers\ServiceController::class, 'export'])->name('services.export');
Route::get('services/import', [Controllers\ServiceController::class, 'importForm'])->name('services.import');
Route::post('services/import', [Controllers\ServiceController::class, 'import'])->name('services.import.store');
Route::resource('services', Controllers\ServiceController::class);
Route::resource('service-groups', Controllers\ServiceGroupController::class);
Route::get('crm/dashboard', [Controllers\CrmDashboardController::class, 'index'])->name('crm.dashboard');
Route::get('customers/export', [Controllers\CustomerController::class, 'export'])->name('customers.export');
Route::get('customers/import', [Controllers\CustomerController::class, 'importForm'])->name('customers.import');
Route::post('customers/import', [Controllers\CustomerController::class, 'import'])->name('customers.import.store');
Route::resource('customers', Controllers\CustomerController::class);
Route::resource('customer-groups', Controllers\CustomerGroupController::class);
Route::resource('companies', Controllers\CompanyController::class);
Route::post('companies/close-fiscal-year/{company}', [Controllers\CompanyController::class, 'closeFiscalYear'])->name('companies.close-fiscal-year');
Route::get('companies/{company}/closing-wizard', [Controllers\CompanyController::class, 'closingWizard'])->name('companies.closing-wizard');
Route::post('companies/{company}/closing-wizard/step1', [Controllers\CompanyController::class, 'closingWizardStep1'])->name('companies.closing-wizard.step1');
Route::post('companies/{company}/closing-wizard/step3', [Controllers\CompanyController::class, 'closingWizardStep3'])->name('companies.closing-wizard.step3');
Route::get('bank-accounts/search-bank', [Controllers\BankAccountController::class, 'searchBank'])->name('bank-accounts.search-bank');
Route::resource('bank-accounts', Controllers\BankAccountController::class);
Route::resource('banks', Controllers\BankController::class);
Route::get('invoices/search/{invoice_type}', [Controllers\InvoiceController::class, 'search'])->name('invoices.search');
Route::get('invoices/get-items/{invoice}', [Controllers\InvoiceController::class, 'getItems'])->name('invoices.get-items');
Route::get('invoices/search-customer', [Controllers\InvoiceController::class, 'searchCustomer'])->name('invoices.search-customer');
Route::get('invoices/search-product-service', [Controllers\InvoiceController::class, 'searchProductService'])->name('invoices.search-product-service');
Route::get('invoices/inactive', [Controllers\InvoiceController::class, 'inactiveInvoices'])->name('invoices.inactive');
Route::get('invoices/inactive/approve', [Controllers\InvoiceController::class, 'approveInactiveInvoices'])->name('invoices.inactive.approve');
Route::prefix('invoices')->group(function () {
Route::get('ancillary-costs/search-customer', [Controllers\AncillaryCostController::class, 'searchCustomer'])->name('ancillary-costs.search-customer');
Route::get('ancillary-costs/search-invoice', [Controllers\AncillaryCostController::class, 'searchInvoice'])->name('ancillary-costs.search-invoice');
Route::get('ancillary-costs/get-products/{invoice_id}', [Controllers\AncillaryCostController::class, 'getBuyInvoiceProducts'])->name('ancillary-costs.get-products');
Route::post('ancillary-costs/{ancillary_cost}/change-status/{status}', [Controllers\AncillaryCostController::class, 'changeStatus'])->name('ancillary-costs.change-status');
Route::get('ancillary-costs/', [Controllers\AncillaryCostController::class, 'index'])->name('ancillary-costs.index');
Route::get('{invoice}/ancillary-costs/create', [Controllers\AncillaryCostController::class, 'create'])->name('invoices.ancillary-costs.create');
Route::post('{invoice}/ancillary-costs', [Controllers\AncillaryCostController::class, 'store'])->name('invoices.ancillary-costs.store');
Route::get('{invoice}/ancillary-costs/{ancillary_cost}', [Controllers\AncillaryCostController::class, 'show'])->name('invoices.ancillary-costs.show');
Route::get('{invoice}/ancillary-costs/{ancillary_cost}/edit', [Controllers\AncillaryCostController::class, 'edit'])->name('invoices.ancillary-costs.edit');
Route::put('{invoice}/ancillary-costs/{ancillary_cost}', [Controllers\AncillaryCostController::class, 'update'])->name('invoices.ancillary-costs.update');
Route::delete('{invoice}/ancillary-costs/{ancillary_cost}', [Controllers\AncillaryCostController::class, 'destroy'])->name('invoices.ancillary-costs.destroy');
});
Route::get('invoices/moadian-histories', [Controllers\MoadianHistoryController::class, 'index'])->name('invoices.moadian-histories.index');
Route::resource('invoices', Controllers\InvoiceController::class);
Route::get('invoices/{invoice}/moadian-histories', [Controllers\MoadianHistoryController::class, 'show'])->name('invoices.moadian-histories.show');
Route::post('invoices/{invoice}/send-moadian', [Controllers\InvoiceController::class, 'sendMoadian'])->name('invoices.send-moadian');
Route::post('invoices/{invoice}/moadian-check-status', [Controllers\MoadianHistoryController::class, 'checkStatus'])->name('invoices.moadian-check-status');
Route::get('invoices/{invoice}/conflicts', [Controllers\InvoiceController::class, 'conflicts'])->name('invoices.conflicts');
Route::get('invoices/{invoice}/conflicts/{type}', [Controllers\InvoiceController::class, 'showMoreConflictsByType'])->name('invoices.conflicts.more');
Route::get('invoices/{invoice}/group-action', [Controllers\InvoiceController::class, 'groupAction'])->name('invoices.group-action');
Route::get('invoices/{invoice}/print', [Controllers\InvoiceController::class, 'print'])->name('invoices.print');
Route::get('invoices/{invoice}/void', [Controllers\InvoiceController::class, 'showVoidForm'])->name('invoices.void-form');
Route::post('invoices/{invoice}/void', [Controllers\InvoiceController::class, 'voidInvoice'])->name('invoices.void');
Route::post('invoices/{invoice}/change-status/{status}', [Controllers\InvoiceController::class, 'changeStatus'])->name('invoices.change-status');
Route::post('invoices/{invoice}/payments', [Controllers\PaymentController::class, 'store'])->name('invoices.payments.store');
Route::delete('invoices/{invoice}/payments/{payment}', [Controllers\PaymentController::class, 'destroy'])->name('invoices.payments.destroy');
Route::post('invoices/{invoice}/payments/{payment}/document', [Controllers\PaymentController::class, 'createDocument'])->name('invoices.payments.create-document');
Route::delete('invoices/{invoice}/payments/{payment}/document', [Controllers\PaymentController::class, 'destroyDocument'])->name('invoices.payments.destroy-document');
Route::post('invoices/{invoice}/transfer', [Controllers\InvoiceController::class, 'transfer'])->name('invoices.transfer');
Route::group(['prefix' => 'management'], function () {
Route::post('users/{user}/create-employee', [Controllers\Management\UserController::class, 'createEmployee'])
->name('users.create-employee');
Route::resource('users', Controllers\Management\UserController::class);
Route::resource('permissions', Controllers\Management\PermissionController::class)->except(['show']);
Route::resource('roles', Controllers\Management\RoleController::class)->except(['show']);
Route::resource('configs', Controllers\ConfigController::class);
});
Route::group(['prefix' => 'hr', 'as' => 'hr.'], function () {
Route::get('employees/export', [Controllers\EmployeeController::class, 'export'])->name('employees.export');
Route::resource('employees', Controllers\EmployeeController::class);
Route::resource('organization-units', Controllers\OrganizationUnitController::class);
Route::resource('org-charts', Controllers\OrgChartController::class);
Route::resource('personnel-requests', Controllers\PersonnelRequestController::class);
Route::patch('personnel-requests/{personnel_request}/approve', [Controllers\PersonnelRequestController::class, 'approve'])->name('personnel-requests.approve');
Route::patch('personnel-requests/{personnel_request}/reject', [Controllers\PersonnelRequestController::class, 'reject'])->name('personnel-requests.reject');
});
Route::group(['prefix' => 'attendance', 'as' => 'attendance.'], function () {
Route::resource('attendance-logs', Controllers\AttendanceLogController::class)->except(['show']);
Route::get('attendance-logs/{attendance_log}', [Controllers\AttendanceLogController::class, 'show'])->name('attendance-logs.show');
Route::post('attendance-logs/{attendance_log}/recalculate', [Controllers\AttendanceLogController::class, 'recalculate'])->name('attendance-logs.recalculate');
Route::get('attendance-logs-bulk/create', [Controllers\AttendanceLogController::class, 'bulkCreate'])->name('attendance-logs.bulk-create');
Route::get('attendance-logs-bulk/search-employee', [Controllers\AttendanceLogController::class, 'searchEmployee'])->name('attendance-logs.search-employee');
Route::post('attendance-logs-bulk', [Controllers\AttendanceLogController::class, 'bulkStore'])->name('attendance-logs.bulk-store');
Route::get('attendance-logs-import', [Controllers\AttendanceLogController::class, 'importForm'])->name('attendance-logs.import');
Route::post('attendance-logs-import/preview', [Controllers\AttendanceLogController::class, 'importPreview'])->name('attendance-logs.import.preview');
Route::post('attendance-logs-import', [Controllers\AttendanceLogController::class, 'importStore'])->name('attendance-logs.import.store');
Route::post('monthly-attendances/{monthly_attendance}/attendance-logs/recalculate', [Controllers\AttendanceLogController::class, 'recalculateAll'])->name('attendance-logs.recalculate-all');
Route::post('monthly-attendances/{monthly_attendance}/recalculate', [Controllers\MonthlyAttendanceController::class, 'recalculate'])->name('monthly-attendances.recalculate');
Route::post('monthly-attendances/{monthly_attendance}/payroll', [Controllers\PayrollController::class, 'store'])->name('monthly-attendances.payroll.store');
Route::get('monthly-attendances-bulk/create', [Controllers\MonthlyAttendanceController::class, 'bulkCreate'])->name('monthly-attendances.bulk-create');
Route::post('monthly-attendances-bulk', [Controllers\MonthlyAttendanceController::class, 'bulkStore'])->name('monthly-attendances.bulk-store');
Route::resource('monthly-attendances', Controllers\MonthlyAttendanceController::class);
Route::resource('work-shifts', Controllers\WorkShiftController::class)->except(['show']);
});
Route::group(['prefix' => 'salary', 'as' => 'salary.'], function () {
Route::get('payrolls/dashboard', [Controllers\PayrollController::class, 'dashboard'])->name('payrolls.dashboard');
Route::resource('tax-slabs', Controllers\TaxSlabController::class);
Route::resource('work-sites', Controllers\WorkSiteController::class)->except(['show']);
Route::resource('work-site-contracts', Controllers\WorkSiteContractController::class)->except(['show']);
Route::resource('public-holidays', Controllers\PublicHolidayController::class)->except(['show']);
Route::resource('payroll-elements', Controllers\PayrollElementController::class)->except(['show']);
Route::resource('salary-decrees', Controllers\SalaryDecreeController::class)->except(['show']);
Route::patch('payrolls/{payroll}/submit-for-approval', [Controllers\PayrollController::class, 'submitForApproval'])->name('payrolls.transition.draft-to-pending-manager-approval');
Route::patch('payrolls/{payroll}/approve', [Controllers\PayrollController::class, 'approve'])->name('payrolls.transition.pending-manager-approval-to-approved');
Route::patch('payrolls/{payroll}/mark-paid', [Controllers\PayrollController::class, 'markPaid'])->name('payrolls.transition.approved-to-paid');
Route::get('payrolls/{payroll}', [Controllers\PayrollController::class, 'show'])->name('payrolls.show');
Route::get('payrolls', [Controllers\PayrollController::class, 'index'])->name('payrolls.index');
Route::delete('payrolls/{payroll}', [Controllers\PayrollController::class, 'destroy'])->name('payrolls.destroy');
Route::get('payroll-items/{payroll_item}/edit', [Controllers\PayrollController::class, 'editItem'])->name('payroll-items.edit');
Route::put('payroll-items/{payroll_item}', [Controllers\PayrollController::class, 'updateItem'])->name('payroll-items.update');
});
Route::get('warehouse/dashboard', [Controllers\WarehouseDashboardController::class, 'index'])->name('warehouse.dashboard');
Route::group(['prefix' => 'reports', 'as' => 'reports.'], function () {
Route::get('ledger', [Controllers\ReportsController::class, 'ledger'])->name('ledger');
Route::get('journal', [Controllers\ReportsController::class, 'journal'])->name('journal');
Route::get('sub-ledger', [Controllers\ReportsController::class, 'subLedger'])->name('sub-ledger');
Route::get('trial-balance', [Controllers\ReportsController::class, 'trialBalance'])->name('trial-balance');
Route::get('trial-balance.print', [Controllers\ReportsController::class, 'printTrialBalance'])->name('trial-balance.print');
Route::get('trial-balance/export-csv', [Controllers\ReportsController::class, 'exportTrialBalanceCsv'])->name('trial-balance.export-csv');
Route::get('documents', [Controllers\ReportsController::class, 'documents'])->name('documents');
Route::get('result', [Controllers\ReportsController::class, 'result'])->name('result');
Route::post('documents/export', [Controllers\DocumentController::class, 'export'])->name('documents.export');
Route::get('cost-income', [Controllers\CostIncomeController::class, 'index'])->name('cost-income');
});
Route::group(['prefix' => 'invoices', 'as' => 'invoices.index'], function () {
Route::get('', [Controllers\InvoiceController::class, 'index']);
});
Route::prefix('customers/{customer}/comments')->as('comments.')->controller(Controllers\CommentController::class)->group(function () {
Route::get('', 'index')->name('index');
Route::get('create', 'create')->name('create');
Route::post('', 'store')->name('store');
Route::get('{comment}/edit', 'edit')->name('edit');
Route::put('{comment}', 'update')->name('update');
Route::delete('{comment}', 'destroy')->name('destroy');
});
Route::prefix('documents/{document}/files')->as('documents.files.')->controller(Controllers\DocumentFileController::class)->group(function () {
Route::get('create', 'create')->name('create');
Route::post('store', 'store')->name('store');
Route::get('{documentFile}/edit', 'edit')->name('edit');
Route::put('{documentFile}', 'update')->name('update');
Route::delete('{documentFile}', 'destroy')->name('destroy');
Route::get('{documentFile}/view', 'view')->name('view');
Route::get('{documentFile}/download', 'download')->name('download');
});
});
api.php
مسیرهای API:
Route::middleware('auth:sanctum')->group(function () {
Route::get('companies', [CompanyController::class, 'index'])
->middleware('check-permission:companies.index')
->name('api.companies.index');
});
Route::prefix('companies/{company}')->middleware(['auth:sanctum', 'api-company'])->group(function () {
Route::post('attendance/logs', [AttendanceLogController::class, 'store'])
->middleware('check-permission:attendance.attendance-logs.store')
->name('api.attendance-logs.store');
Route::get('attendance/logs', [AttendanceLogController::class, 'index'])
->middleware('check-permission:attendance.attendance-logs.index')
->name('api.attendance-logs.index');
Route::get('employees', [EmployeeController::class, 'index'])
->middleware('check-permission:hr.employees.index')
->name('api.employees.index');
Route::post('employees', [EmployeeController::class, 'store'])
->middleware('check-permission:hr.employees.store')
->name('api.employees.store');
Route::post('documents', [DocumentController::class, 'store'])
->middleware('check-permission:documents.store')
->name('api.documents.store');
Route::get('documents/{documentId}', [DocumentController::class, 'show'])
->middleware('check-permission:documents.show')
->name('api.documents.show');
Route::post('documents/{documentId}/files', [DocumentController::class, 'attachFile'])
->middleware('check-permission:documents.files.store')
->name('api.documents.files.store');
});
در حال حاضر فایل تنها شامل نمونهی پیشفرض لاراول است و میتوانید مسیرهای API جدید را در همین گروه اضافه کنید.
🧪 تستها (tests/)
ساختار تستها مطابق استاندارد لاراول است و دو تست نمونه به صورت پیشفرض در مخزن حضور دارند:
tests/
├── Feature/ExampleTest.php # بررسی پاسخ موفق صفحهی اصلی
└── Unit/ExampleTest.php # تست ساده صحت true
برای گسترش پوشش تستها میتوانید فایلهای جدید با دستور php artisan make:test بسازید یا همین نمونهها را ویرایش کنید. راهنمای تست در docs/testing-guide.md توضیحات بیشتری ارائه میدهد.
🔧 ابزارهای توسعه
Composer
مدیریت dependency های PHP:
composer install # نصب پکیجها
composer dump-autoload # بازسازی autoloader
composer update # بهروزرسانی پکیجها
NPM
مدیریت dependency های JavaScript:
npm install # نصب پکیجها
npm run dev # اجرای توسعه
npm run build # ساخت نهایی
Artisan
دستورات Laravel:
php artisan migrate # اجرای مایگریشنها
php artisan db:seed # اجرای سیدرها
php artisan make:model # ایجاد مدل جدید
php artisan serve # اجرای سرور توسعه
📝 کنوانسیونهای نامگذاری
کلاسها
- کلاسهای کنترلر (Controllers):
PascalCase+Controller(مثلDocumentController) - کلاسهای مدل (Models):
PascalCaseمنفرد (مثلDocument) - کلاسهای سرویس (Services):
PascalCase+Service(مثلDocumentService)
فایلها
- فایلهای نما (Views):
kebab-case(مثلdocument-create.blade.php) - فایلهای مایگریشن (Migrations): تاریخ +
snake_case(مثل2024_01_01_create_documents_table)
متغیرها
- متغیرهای PHP:
camelCase(مثل$fiscalYear) - ستونهای دیتابیس (Database):
snake_case(مثلfiscal_year_id)
مسیرها
- نشانیهای وب (URLs):
kebab-case(مثل/customer-groups) - نامهای مسیر (Route names):
dot.notation(مثلdocuments.create)
🔒 امنیت
Middleware
- میدلویر
auth- احراز هویت کاربر - میدلویر
check-permission- کنترل مجوزها - سایر میدلویر های Laravel
مجوزها
استفاده از پکیج Spatie Permission:
// permission in Controller (Not Used):
$this->authorize('documents.create');
// کنترل در Blade
@can('documents.edit')
<button>ویرایش</button>
@endcan
🚀 بهینهسازی
Caching
- کش پیکربندی (Config):
php artisan config:cache - کش مسیرها (Routes):
php artisan route:cache - کش نماها (Views):
php artisan view:cache
Database
- نمایهها (Indexes): روی ستونهای پرجستجو
- روابط (Relations): استفاده از Eager Loading
- صفحهبندی (Pagination): برای لیستهای بزرگ
نکته مهم: همیشه قبل از اعمال تغییرات، ساختار پروژه موجود را مطالعه کنید و از الگوهای استفاده شده پیروی کنید.