Initial commit: Sistema de comisiones y gastos personales
This commit is contained in:
18
.editorconfig
Executable file
18
.editorconfig
Executable file
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[{compose,docker-compose}.{yml,yaml}]
|
||||
indent_size = 4
|
||||
44
.env.example
Executable file
44
.env.example
Executable file
@@ -0,0 +1,44 @@
|
||||
# Nomina Pegaso - Configuración
|
||||
APP_NAME="Nomina Pegaso"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:OKIa4LjLBL6WSZ7HZXKDVpFwJ/KMI6bljtmxWQEcHKY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://nomina-pegaso.casa
|
||||
|
||||
APP_LOCALE=es
|
||||
APP_FALLBACK_LOCALE=es
|
||||
APP_FAKER_LOCALE=es_MX
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=10.10.4.17
|
||||
DB_PORT=3391
|
||||
DB_DATABASE=nomina_pegaso
|
||||
DB_USERNAME=nickpons666
|
||||
DB_PASSWORD=MiPo6425@@
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=true
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# Telegram Bot
|
||||
TELEGRAM_BOT_TOKEN=8324407449:AAF2awMeZ9pgSIp0MvV1r5owu8lO7SEK70E
|
||||
TELEGRAM_WEBHOOK_URL=
|
||||
11
.gitattributes
vendored
Executable file
11
.gitattributes
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
25
.gitignore
vendored
Executable file
25
.gitignore
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.cursor/
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
_ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
42
README.md
Executable file
42
README.md
Executable file
@@ -0,0 +1,42 @@
|
||||
# Sistema de Comisiones y Gastos
|
||||
|
||||
Aplicación Laravel para control de ingresos por comisiones y gastos personales con soporte multiusuario y bot de Telegram integrado.
|
||||
|
||||
## Características
|
||||
|
||||
- **Gestión de Comisiones**: Registro y cálculo automático de comisiones por ventas
|
||||
- **Control de Gastos**: Seguimiento de gastos personales con categorización (quincena 1, quincena 2, mensual)
|
||||
- **Calendario Visual**: Visualización de ventas y gastos en un calendario interactivo
|
||||
- **Configuración de Usuario**: Porcentaje de comisión, salario base, datos laborales
|
||||
- **Datos Laborales**: Fecha de ingreso, razón social, sueldo íntegro diario (para cálculo de vacaciones)
|
||||
- **Interfaz Responsive**: Diseño adaptado para dispositivos móviles
|
||||
|
||||
## Instalación
|
||||
|
||||
```bash
|
||||
composer install
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
php artisan migrate
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
## Estructura de la Base de Datos
|
||||
|
||||
- **users**: Usuarios del sistema con configuración de comisiones
|
||||
- **months**: Meses del año fiscal
|
||||
- **daily_sales**: Ventas diarias registradas
|
||||
- **expenses**: Gastos personales categorizados
|
||||
- **telegram_accounts**: Cuentas de Telegram vinculadas
|
||||
|
||||
## Rutas Principales
|
||||
|
||||
- `/dashboard` - Panel principal
|
||||
- `/calendar` - Calendario de ventas y gastos
|
||||
- `/sales` - Gestión de ventas
|
||||
- `/expenses` - Gestión de gastos
|
||||
- `/settings` - Configuración del usuario
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
101
app/Http/Controllers/AuthController.php
Executable file
101
app/Http/Controllers/AuthController.php
Executable file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar formulario de login
|
||||
*/
|
||||
public function showLoginForm()
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar login
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'username' => ['required', 'string'],
|
||||
'password' => ['required'],
|
||||
]);
|
||||
|
||||
$user = User::where('username', $credentials['username'])->first();
|
||||
|
||||
if (!$user || !Hash::check($credentials['password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => ['Las credenciales no coinciden con nuestros registros.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->is_active) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => ['Tu cuenta está inactiva. Contacta al administrador.'],
|
||||
]);
|
||||
}
|
||||
|
||||
Auth::login($user, $request->boolean('remember'));
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de registro
|
||||
*/
|
||||
public function showRegisterForm()
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar registro
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'username' => ['required', 'string', 'max:50', 'unique:users,username'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
'password' => ['required', 'confirmed', 'min:8'],
|
||||
'commission_percentage' => ['required', 'numeric', 'min:0', 'max:100'],
|
||||
'monthly_salary' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'username' => $validated['username'],
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => $validated['password'],
|
||||
'commission_percentage' => $validated['commission_percentage'],
|
||||
'monthly_salary' => $validated['monthly_salary'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar sesión
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
}
|
||||
154
app/Http/Controllers/CalendarController.php
Executable file
154
app/Http/Controllers/CalendarController.php
Executable file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DailySale;
|
||||
use App\Models\Month;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CalendarController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar calendario interactivo
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$year = $request->get('year', now()->year);
|
||||
$monthName = $request->get('month');
|
||||
|
||||
// Obtener meses del usuario
|
||||
$months = $user->months()
|
||||
->where('year', $year)
|
||||
->get()
|
||||
->keyBy('name');
|
||||
|
||||
// Si se especifica mes, mostrar ese mes
|
||||
if ($monthName) {
|
||||
$currentMonth = $months->get($monthName);
|
||||
} else {
|
||||
// Si no, buscar mes abierto o el último del año
|
||||
$currentMonth = $user->months()
|
||||
->where('year', $year)
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre')")
|
||||
->first();
|
||||
}
|
||||
|
||||
// Obtener ventas diarias del mes actual
|
||||
$dailySales = [];
|
||||
if ($currentMonth) {
|
||||
$sales = $currentMonth->dailySales()->get()->keyBy('date');
|
||||
foreach ($sales as $sale) {
|
||||
$dailySales[$sale->date->format('Y-m-d')] = $sale;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener gastos del mes
|
||||
$expenses = [];
|
||||
if ($currentMonth) {
|
||||
$expenseList = $currentMonth->expenses()->get();
|
||||
foreach ($expenseList as $expense) {
|
||||
$expenses[$expense->date->format('Y-m-d')] = $expense;
|
||||
}
|
||||
}
|
||||
|
||||
return view('calendar.index', compact('year', 'months', 'currentMonth', 'dailySales', 'expenses'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener datos de un día específico (para AJAX)
|
||||
*/
|
||||
public function day(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$date = $request->get('date');
|
||||
|
||||
if (!$date) {
|
||||
return response()->json(['error' => 'Fecha requerida'], 400);
|
||||
}
|
||||
|
||||
// Buscar venta del día
|
||||
$sale = DailySale::whereHas('month', function ($query) use ($user) {
|
||||
$query->where('user_id', $user->id);
|
||||
})
|
||||
->where('date', $date)
|
||||
->first();
|
||||
|
||||
// Buscar gastos del día
|
||||
$expenses = $user->expenses()
|
||||
->where('date', $date)
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'date' => $date,
|
||||
'sale' => $sale,
|
||||
'expenses' => $expenses,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar venta y gasto del día (para AJAX)
|
||||
*/
|
||||
public function storeDay(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$data = $request->all();
|
||||
|
||||
$monthId = $request->input('month_id');
|
||||
$date = $request->input('date');
|
||||
$userSales = floatval($request->input('user_sales', 0));
|
||||
$systemSales = floatval($request->input('system_sales', 0));
|
||||
$expenseAmount = floatval($request->input('expense_amount', 0));
|
||||
|
||||
if (!$monthId || !$date) {
|
||||
return response()->json(['success' => false, 'message' => 'Faltan datos requeridos']);
|
||||
}
|
||||
|
||||
$month = Month::where('id', $monthId)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$month) {
|
||||
return response()->json(['success' => false, 'message' => 'Mes no encontrado']);
|
||||
}
|
||||
|
||||
// Guardar o actualizar venta solo si hay datos
|
||||
if ($userSales > 0) {
|
||||
DailySale::updateOrCreate(
|
||||
[
|
||||
'month_id' => $month->id,
|
||||
'date' => $date,
|
||||
],
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'user_sales' => $userSales,
|
||||
'system_sales' => $systemSales,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Guardar o actualizar gasto si hay monto
|
||||
$expenseType = $request->input('expense_type', 'q1');
|
||||
|
||||
if ($expenseAmount > 0) {
|
||||
\App\Models\Expense::updateOrCreate(
|
||||
[
|
||||
'month_id' => $month->id,
|
||||
'date' => $date,
|
||||
],
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'description' => $request->input('expense_description', 'Gasto del día'),
|
||||
'amount' => $expenseAmount,
|
||||
'expense_type' => $expenseType,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Datos guardados correctamente']);
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Executable file
8
app/Http/Controllers/Controller.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
44
app/Http/Controllers/DashboardController.php
Executable file
44
app/Http/Controllers/DashboardController.php
Executable file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DailySale;
|
||||
use App\Models\Month;
|
||||
use App\Services\CommissionCalculator;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar dashboard del usuario
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Obtener mes actual o último mes
|
||||
$currentMonth = $user->getCurrentMonth();
|
||||
|
||||
// Si no hay mes abierto, buscar el último mes
|
||||
if (!$currentMonth) {
|
||||
$currentMonth = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC")
|
||||
->first();
|
||||
}
|
||||
|
||||
$data = null;
|
||||
if ($currentMonth) {
|
||||
$data = CommissionCalculator::calculateForMonth($user, $currentMonth);
|
||||
}
|
||||
|
||||
// Últimos meses del usuario
|
||||
$recentMonths = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC")
|
||||
->limit(6)
|
||||
->get();
|
||||
|
||||
return view('dashboard.index', compact('currentMonth', 'data', 'recentMonths'));
|
||||
}
|
||||
}
|
||||
152
app/Http/Controllers/ExpenseController.php
Executable file
152
app/Http/Controllers/ExpenseController.php
Executable file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Expense;
|
||||
use App\Models\Month;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ExpenseController extends Controller
|
||||
{
|
||||
/**
|
||||
* Listar todos los gastos
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$monthId = $request->get('month_id');
|
||||
|
||||
if ($monthId) {
|
||||
$month = $user->months()->findOrFail($monthId);
|
||||
$expenses = $month->expenses()
|
||||
->orderBy('date', 'desc')
|
||||
->paginate(30);
|
||||
} else {
|
||||
$month = null;
|
||||
$expenses = $user->expenses()
|
||||
->orderBy('date', 'desc')
|
||||
->paginate(30);
|
||||
}
|
||||
|
||||
$months = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC")
|
||||
->get();
|
||||
|
||||
return view('expenses.index', compact('expenses', 'month', 'months'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario para crear gasto
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$monthId = $request->get('month_id');
|
||||
|
||||
if ($monthId) {
|
||||
$month = $user->months()->findOrFail($monthId);
|
||||
} else {
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$month = $user->months()->latest()->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$month) {
|
||||
return redirect()->route('months.index')->with('info', 'Necesitas un mes de trabajo primero.');
|
||||
}
|
||||
|
||||
return view('expenses.create', compact('month'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nuevo gasto
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'month_id' => ['required', 'exists:months,id'],
|
||||
'description' => ['required', 'string', 'max:255'],
|
||||
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||
'date' => ['required', 'date'],
|
||||
'expense_type' => ['required', 'in:q1,q2,mensual'],
|
||||
]);
|
||||
|
||||
// Verificar que el mes pertenece al usuario
|
||||
$month = $user->months()->findOrFail($validated['month_id']);
|
||||
|
||||
$month->expenses()->create([
|
||||
'user_id' => $user->id,
|
||||
'description' => $validated['description'],
|
||||
'amount' => $validated['amount'],
|
||||
'date' => $validated['date'],
|
||||
'expense_type' => $validated['expense_type'],
|
||||
]);
|
||||
|
||||
return redirect()->route('expenses.index', ['month_id' => $month->id])
|
||||
->with('success', 'Gasto registrado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(Expense $expense)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($expense->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('expenses.edit', compact('expense'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar gasto
|
||||
*/
|
||||
public function update(Request $request, Expense $expense)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($expense->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'description' => ['required', 'string', 'max:255'],
|
||||
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||
'date' => ['required', 'date'],
|
||||
'expense_type' => ['required', 'in:q1,q2,mensual'],
|
||||
]);
|
||||
|
||||
$expense->update($validated);
|
||||
|
||||
return redirect()->route('expenses.index', ['month_id' => $expense->month_id])
|
||||
->with('success', 'Gasto actualizado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar gasto
|
||||
*/
|
||||
public function destroy(Expense $expense)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($expense->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$monthId = $expense->month_id;
|
||||
$expense->delete();
|
||||
|
||||
return redirect()->route('expenses.index', ['month_id' => $monthId])
|
||||
->with('success', 'Gasto eliminado correctamente.');
|
||||
}
|
||||
}
|
||||
163
app/Http/Controllers/MonthController.php
Executable file
163
app/Http/Controllers/MonthController.php
Executable file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Month;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class MonthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Listar todos los meses
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$months = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Diciembre', 'Noviembre', 'Octubre', 'Septiembre', 'Agosto', 'Julio', 'Junio', 'Mayo', 'Abril', 'Marzo', 'Febrero', 'Enero')")
|
||||
->paginate(12);
|
||||
|
||||
return view('months.index', compact('months'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario para crear mes
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('months.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nuevo mes
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'year' => ['required', 'integer', 'min:2020', 'max:2100'],
|
||||
]);
|
||||
|
||||
// Verificar que no exista ya el mes para el usuario
|
||||
$exists = $user->months()
|
||||
->where('name', $validated['name'])
|
||||
->where('year', $validated['year'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return back()->withErrors([
|
||||
'name' => 'Ya existe un mes con ese nombre y año.',
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
$user->months()->create([
|
||||
'name' => $validated['name'],
|
||||
'year' => $validated['year'],
|
||||
'status' => 'open',
|
||||
]);
|
||||
|
||||
return redirect()->route('months.index')
|
||||
->with('success', 'Mes creado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar detalles del mes
|
||||
*/
|
||||
public function show(Month $month)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($month->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$month->load(['dailySales', 'expenses']);
|
||||
|
||||
return view('months.show', compact('month'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(Month $month)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($month->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('months.edit', compact('month'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar mes
|
||||
*/
|
||||
public function update(Request $request, Month $month)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($month->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'year' => ['required', 'integer', 'min:2020', 'max:2100'],
|
||||
'status' => ['required', 'in:open,closed,paid'],
|
||||
]);
|
||||
|
||||
$month->update($validated);
|
||||
|
||||
return redirect()->route('months.show', $month->id)
|
||||
->with('success', 'Mes actualizado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar mes
|
||||
*/
|
||||
public function close(Month $month)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($month->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if ($month->status !== 'open') {
|
||||
return back()->with('error', 'Solo se pueden cerrar meses abiertos.');
|
||||
}
|
||||
|
||||
$month->update(['status' => 'closed']);
|
||||
|
||||
return redirect()->route('months.show', $month->id)
|
||||
->with('success', 'Mes cerrado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar mes
|
||||
*/
|
||||
public function destroy(Month $month)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($month->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Verificar que no tenga ventas o gastos asociados
|
||||
if ($month->dailySales()->count() > 0 || $month->expenses()->count() > 0) {
|
||||
return back()->with('error', 'No puedes eliminar un mes que tiene ventas o gastos asociados.');
|
||||
}
|
||||
|
||||
$month->delete();
|
||||
|
||||
return redirect()->route('months.index')
|
||||
->with('success', 'Mes eliminado correctamente.');
|
||||
}
|
||||
}
|
||||
147
app/Http/Controllers/ReportController.php
Executable file
147
app/Http/Controllers/ReportController.php
Executable file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Month;
|
||||
use App\Services\CommissionCalculator;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar reporte mensual
|
||||
*/
|
||||
public function monthly(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$monthId = $request->get('month_id');
|
||||
|
||||
if ($monthId) {
|
||||
$month = $user->months()->findOrFail($monthId);
|
||||
} else {
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$month = $user->months()->latest()->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$month) {
|
||||
return redirect()->route('dashboard')->with('info', 'No hay meses disponibles.');
|
||||
}
|
||||
|
||||
$report = CommissionCalculator::calculateForMonth($user, $month);
|
||||
|
||||
// Cargar ventas y gastos detalles
|
||||
$dailySales = $month->dailySales()
|
||||
->orderBy('date', 'asc')
|
||||
->get();
|
||||
|
||||
$expenses = $month->expenses()
|
||||
->orderBy('date', 'desc')
|
||||
->get();
|
||||
|
||||
$months = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC")
|
||||
->get();
|
||||
|
||||
return view('reports.monthly', compact('report', 'month', 'dailySales', 'expenses', 'months'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar reporte quincenal
|
||||
*/
|
||||
public function biweekly(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$monthId = $request->get('month_id');
|
||||
$biweekly = $request->get('biweekly', 1);
|
||||
|
||||
if ($monthId) {
|
||||
$month = $user->months()->findOrFail($monthId);
|
||||
} else {
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$month = $user->months()->latest()->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$month) {
|
||||
return redirect()->route('dashboard')->with('info', 'No hay meses disponibles.');
|
||||
}
|
||||
|
||||
$report = CommissionCalculator::calculateBiweekly($user, $month, $biweekly);
|
||||
|
||||
// Obtener ventas del mes
|
||||
$dailySales = $month->dailySales()
|
||||
->orderBy('date', 'asc')
|
||||
->get();
|
||||
|
||||
// Obtener gastos según la quincena seleccionada
|
||||
$expenses = $month->expenses()
|
||||
->where(function($query) use ($biweekly) {
|
||||
if ($biweekly === 1) {
|
||||
$query->where('expense_type', 'q1')
|
||||
->orWhere('expense_type', 'mensual');
|
||||
} else {
|
||||
$query->where('expense_type', 'q2')
|
||||
->orWhere('expense_type', 'mensual');
|
||||
}
|
||||
})
|
||||
->orderBy('date', 'desc')
|
||||
->get();
|
||||
|
||||
$months = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC")
|
||||
->get();
|
||||
|
||||
return view('reports.biweekly', compact('report', 'month', 'dailySales', 'expenses', 'months', 'biweekly'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumen anual
|
||||
*/
|
||||
public function yearly(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$year = $request->get('year', now()->year);
|
||||
|
||||
$report = CommissionCalculator::calculateYearly($user, $year);
|
||||
|
||||
// Obtener meses del año
|
||||
$months = $user->months()
|
||||
->where('year', $year)
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre')")
|
||||
->get();
|
||||
|
||||
// Años disponibles
|
||||
$years = $user->months()
|
||||
->select('year')
|
||||
->distinct()
|
||||
->orderBy('year', 'desc')
|
||||
->pluck('year');
|
||||
|
||||
return view('reports.yearly', compact('report', 'year', 'months', 'years'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener número del mes por nombre
|
||||
*/
|
||||
private static function getMonthNumber(string $monthName): int
|
||||
{
|
||||
$months = [
|
||||
'Enero' => 1, 'Febrero' => 2, 'Marzo' => 3, 'Abril' => 4,
|
||||
'Mayo' => 5, 'Junio' => 6, 'Julio' => 7, 'Agosto' => 8,
|
||||
'Septiembre' => 9, 'Octubre' => 10, 'Noviembre' => 11, 'Diciembre' => 12
|
||||
];
|
||||
|
||||
return $months[$monthName] ?? 1;
|
||||
}
|
||||
}
|
||||
172
app/Http/Controllers/SaleController.php
Executable file
172
app/Http/Controllers/SaleController.php
Executable file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\DailySale;
|
||||
use App\Models\Month;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SaleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Listar todas las ventas del mes actual
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Obtener mes seleccionado
|
||||
$monthId = $request->get('month_id');
|
||||
|
||||
if ($monthId) {
|
||||
$month = $user->months()->findOrFail($monthId);
|
||||
} else {
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$month = $user->months()->latest()->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$month) {
|
||||
return redirect()->route('months.create')->with('info', 'Primero debes crear un mes de trabajo.');
|
||||
}
|
||||
|
||||
$sales = $month->dailySales()
|
||||
->orderBy('date', 'desc')
|
||||
->paginate(31);
|
||||
|
||||
$months = $user->months()
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre') DESC")
|
||||
->get();
|
||||
|
||||
return view('sales.index', compact('sales', 'month', 'months'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario para crear venta
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$monthId = $request->get('month_id');
|
||||
|
||||
if ($monthId) {
|
||||
$month = $user->months()->findOrFail($monthId);
|
||||
} else {
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$month = $user->months()->latest()->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$month) {
|
||||
return redirect()->route('months.index')->with('info', 'Necesitas un mes de trabajo primero.');
|
||||
}
|
||||
|
||||
return view('sales.create', compact('month'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nueva venta
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'month_id' => ['required', 'exists:months,id'],
|
||||
'date' => ['required', 'date'],
|
||||
'user_sales' => ['required', 'numeric', 'min:0'],
|
||||
'system_sales' => ['nullable', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
// Verificar que el mes pertenece al usuario
|
||||
$month = $user->months()->findOrFail($validated['month_id']);
|
||||
|
||||
// Verificar si ya existe una venta para esa fecha
|
||||
$existingSale = $month->dailySales()->where('date', $validated['date'])->first();
|
||||
|
||||
if ($existingSale) {
|
||||
// Actualizar venta existente
|
||||
$existingSale->update([
|
||||
'user_sales' => $validated['user_sales'],
|
||||
'system_sales' => $validated['system_sales'] ?? $existingSale->system_sales,
|
||||
]);
|
||||
|
||||
return redirect()->route('sales.index', ['month_id' => $month->id])
|
||||
->with('success', 'Venta actualizada correctamente.');
|
||||
}
|
||||
|
||||
// Crear nueva venta
|
||||
$month->dailySales()->create([
|
||||
'user_id' => $user->id,
|
||||
'date' => $validated['date'],
|
||||
'user_sales' => $validated['user_sales'],
|
||||
'system_sales' => $validated['system_sales'] ?? 0,
|
||||
]);
|
||||
|
||||
return redirect()->route('sales.index', ['month_id' => $month->id])
|
||||
->with('success', 'Venta registrada correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(DailySale $sale)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Verificar que la venta pertenece al usuario
|
||||
if ($sale->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('sales.edit', compact('sale'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar venta
|
||||
*/
|
||||
public function update(Request $request, DailySale $sale)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($sale->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'date' => ['required', 'date'],
|
||||
'user_sales' => ['required', 'numeric', 'min:0'],
|
||||
'system_sales' => ['nullable', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
$sale->update($validated);
|
||||
|
||||
return redirect()->route('sales.index', ['month_id' => $sale->month_id])
|
||||
->with('success', 'Venta actualizada correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar venta
|
||||
*/
|
||||
public function destroy(DailySale $sale)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($sale->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$monthId = $sale->month_id;
|
||||
$sale->delete();
|
||||
|
||||
return redirect()->route('sales.index', ['month_id' => $monthId])
|
||||
->with('success', 'Venta eliminada correctamente.');
|
||||
}
|
||||
}
|
||||
76
app/Http/Controllers/SettingsController.php
Executable file
76
app/Http/Controllers/SettingsController.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar configuración del usuario
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('settings.index', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar configuración del usuario
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
foreach (['fecha_ingreso', 'razon_social', 'sueldo_integro_diario'] as $field) {
|
||||
if ($request->has($field) && $request->input($field) === '') {
|
||||
$request->merge([$field => null]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('commission_percentage')) {
|
||||
$request->validate([
|
||||
'commission_percentage' => ['required', 'numeric', 'min:0', 'max:100'],
|
||||
]);
|
||||
$user->commission_percentage = floatval($request->input('commission_percentage', 0));
|
||||
}
|
||||
|
||||
if ($request->has('monthly_salary')) {
|
||||
$request->validate([
|
||||
'monthly_salary' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
$user->monthly_salary = floatval($request->input('monthly_salary', 0));
|
||||
}
|
||||
|
||||
if ($request->has('fecha_ingreso')) {
|
||||
if ($request->filled('fecha_ingreso')) {
|
||||
$user->fecha_ingreso = $request->input('fecha_ingreso');
|
||||
} else {
|
||||
$user->fecha_ingreso = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('razon_social')) {
|
||||
if ($request->filled('razon_social')) {
|
||||
$user->razon_social = $request->input('razon_social');
|
||||
} else {
|
||||
$user->razon_social = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('sueldo_integro_diario')) {
|
||||
if ($request->filled('sueldo_integro_diario')) {
|
||||
$user->sueldo_integro_diario = floatval($request->input('sueldo_integro_diario'));
|
||||
} else {
|
||||
$user->sueldo_integro_diario = null;
|
||||
}
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return redirect()->route('settings.index')->with('success', 'Configuración actualizada correctamente.');
|
||||
}
|
||||
}
|
||||
140
app/Http/Controllers/TelegramController.php
Executable file
140
app/Http/Controllers/TelegramController.php
Executable file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\TelegramAccount;
|
||||
use App\Models\User;
|
||||
use App\Services\TelegramBotService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TelegramController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar página de vinculación de Telegram
|
||||
*/
|
||||
public function showVerifyPage()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Obtener o crear cuenta de Telegram
|
||||
$telegramAccount = $user->telegramAccount()->first();
|
||||
|
||||
if (!$telegramAccount) {
|
||||
// Generar nuevo código de verificación
|
||||
$verificationCode = TelegramBotService::generateVerificationCode();
|
||||
|
||||
$telegramAccount = TelegramAccount::create([
|
||||
'user_id' => $user->id,
|
||||
'chat_id' => null,
|
||||
'verification_code' => $verificationCode,
|
||||
'is_verified' => false,
|
||||
]);
|
||||
} elseif (!$telegramAccount->is_verified && !$telegramAccount->verification_code) {
|
||||
// Regenerar código si no existe
|
||||
$verificationCode = TelegramBotService::generateVerificationCode();
|
||||
$telegramAccount->update(['verification_code' => $verificationCode]);
|
||||
}
|
||||
|
||||
return view('telegram.verify', compact('telegramAccount'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerar código de verificación
|
||||
*/
|
||||
public function regenerateCode(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$telegramAccount = $user->telegramAccount;
|
||||
|
||||
if (!$telegramAccount) {
|
||||
return back()->with('error', 'No tienes una cuenta de Telegram vinculada.');
|
||||
}
|
||||
|
||||
if ($telegramAccount->is_verified) {
|
||||
return back()->with('error', 'Tu cuenta ya está verificada.');
|
||||
}
|
||||
|
||||
$verificationCode = TelegramBotService::generateVerificationCode();
|
||||
$telegramAccount->update(['verification_code' => $verificationCode]);
|
||||
|
||||
return back()->with('success', 'Nuevo código de verificación generado.');
|
||||
}
|
||||
|
||||
/**
|
||||
* desvincular cuenta de Telegram
|
||||
*/
|
||||
public function unlink(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$telegramAccount = $user->telegramAccount;
|
||||
|
||||
if ($telegramAccount) {
|
||||
$telegramAccount->delete();
|
||||
|
||||
// Crear nueva cuenta sin verificar
|
||||
TelegramAccount::create([
|
||||
'user_id' => $user->id,
|
||||
'chat_id' => null,
|
||||
'verification_code' => TelegramBotService::generateVerificationCode(),
|
||||
'is_verified' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Cuenta de Telegram desvinculada.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook para recibir mensajes de Telegram
|
||||
*/
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
try {
|
||||
$update = $request->all();
|
||||
|
||||
Log::info('Telegram webhook received', $update);
|
||||
|
||||
if (empty($update)) {
|
||||
return response()->json(['ok' => true, 'message' => 'No update
|
||||
received']);
|
||||
}
|
||||
|
||||
$botService = new TelegramBotService();
|
||||
$result = $botService->handleUpdate($update);
|
||||
|
||||
return response()->json($result);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Telegram webhook error', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return response()->json(['ok' => false, 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurar webhook (ruta temporal para configurar desde el navegador)
|
||||
*/
|
||||
public function setupWebhook(Request $request)
|
||||
{
|
||||
$token = $request->get('token');
|
||||
|
||||
// Verificar token de seguridad simple
|
||||
if ($token !== config('app.key')) {
|
||||
abort(403, 'Token inválido');
|
||||
}
|
||||
|
||||
$botService = new TelegramBotService();
|
||||
$result = $botService->setWebhook();
|
||||
|
||||
if ($result) {
|
||||
return response()->json(['success' => true, 'message' => 'Webhook configurado correctamente']);
|
||||
}
|
||||
|
||||
return response()->json(['success' => false, 'error' => 'Error al configurar webhook'], 500);
|
||||
}
|
||||
}
|
||||
67
app/Models/DailySale.php
Executable file
67
app/Models/DailySale.php
Executable file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable(['user_id', 'month_id', 'date', 'user_sales', 'system_sales'])]
|
||||
class DailySale extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'daily_sales';
|
||||
|
||||
/**
|
||||
* Los atributos que son asignables en masa.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'month_id',
|
||||
'date',
|
||||
'user_sales',
|
||||
'system_sales',
|
||||
];
|
||||
|
||||
/**
|
||||
* Los atributos que deben ser convertidos.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'date' => 'date',
|
||||
'user_sales' => 'decimal:2',
|
||||
'system_sales' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con usuario
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con mes
|
||||
*/
|
||||
public function month(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Month::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener la diferencia entre ventas usuario y sistema
|
||||
*/
|
||||
public function getDifferenceAttribute(): float
|
||||
{
|
||||
return $this->user_sales - $this->system_sales;
|
||||
}
|
||||
}
|
||||
59
app/Models/Expense.php
Executable file
59
app/Models/Expense.php
Executable file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable(['user_id', 'month_id', 'description', 'amount', 'date', 'expense_type'])]
|
||||
class Expense extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'expenses';
|
||||
|
||||
/**
|
||||
* Los atributos que son asignables en masa.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'month_id',
|
||||
'description',
|
||||
'amount',
|
||||
'date',
|
||||
'expense_type',
|
||||
];
|
||||
|
||||
/**
|
||||
* Los atributos que deben ser convertidos.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'date' => 'date',
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con usuario
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con mes
|
||||
*/
|
||||
public function month(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Month::class);
|
||||
}
|
||||
}
|
||||
97
app/Models/Month.php
Executable file
97
app/Models/Month.php
Executable file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['user_id', 'name', 'year', 'status'])]
|
||||
class Month extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'months';
|
||||
|
||||
/**
|
||||
* Los atributos que son asignables en masa.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'year',
|
||||
'status',
|
||||
];
|
||||
|
||||
/**
|
||||
* Los atributos que deben ser convertidos.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con usuario
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con ventas diarias
|
||||
*/
|
||||
public function dailySales(): HasMany
|
||||
{
|
||||
return $this->hasMany(DailySale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con gastos
|
||||
*/
|
||||
public function expenses(): HasMany
|
||||
{
|
||||
return $this->hasMany(Expense::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener el nombre del mes con formato
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return $this->name . ' ' . $this->year;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular ventas totales del mes
|
||||
*/
|
||||
public function getTotalUserSalesAttribute(): float
|
||||
{
|
||||
return $this->dailySales()->sum('user_sales');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular ventas del sistema
|
||||
*/
|
||||
public function getTotalSystemSalesAttribute(): float
|
||||
{
|
||||
return $this->dailySales()->sum('system_sales');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular gastos totales del mes
|
||||
*/
|
||||
public function getTotalExpensesAttribute(): float
|
||||
{
|
||||
return $this->expenses()->sum('amount');
|
||||
}
|
||||
}
|
||||
48
app/Models/TelegramAccount.php
Executable file
48
app/Models/TelegramAccount.php
Executable file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable(['user_id', 'chat_id', 'verification_code', 'is_verified'])]
|
||||
class TelegramAccount extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'telegram_accounts';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'chat_id',
|
||||
'verification_code',
|
||||
'is_verified',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_verified' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con usuario
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
84
app/Models/User.php
Executable file
84
app/Models/User.php
Executable file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['username', 'name', 'email', 'password', 'commission_percentage', 'monthly_salary', 'is_active', 'fecha_ingreso', 'razon_social', 'sueldo_integro_diario'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'commission_percentage' => 'decimal:2',
|
||||
'monthly_salary' => 'decimal:2',
|
||||
'sueldo_integro_diario' => 'decimal:2',
|
||||
'fecha_ingreso' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con cuenta de Telegram
|
||||
*/
|
||||
public function telegramAccount(): HasOne
|
||||
{
|
||||
return $this->hasOne(TelegramAccount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con meses de trabajo
|
||||
*/
|
||||
public function months(): HasMany
|
||||
{
|
||||
return $this->hasMany(Month::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con ventas diarias
|
||||
*/
|
||||
public function dailySales(): HasMany
|
||||
{
|
||||
return $this->hasMany(DailySale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con gastos
|
||||
*/
|
||||
public function expenses(): HasMany
|
||||
{
|
||||
return $this->hasMany(Expense::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener el mes activo (si existe)
|
||||
*/
|
||||
public function getCurrentMonth(): ?Month
|
||||
{
|
||||
return $this->months()
|
||||
->where('status', 'open')
|
||||
->orderBy('year', 'desc')
|
||||
->orderByRaw("FIELD(name, 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre')")
|
||||
->first();
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Executable file
24
app/Providers/AppServiceProvider.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
211
app/Services/CommissionCalculator.php
Executable file
211
app/Services/CommissionCalculator.php
Executable file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\DailySale;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Month;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CommissionCalculator
|
||||
{
|
||||
/**
|
||||
* Calcular comisión para un usuario en un mes específico
|
||||
*/
|
||||
public static function calculateForMonth(User $user, Month $month): array
|
||||
{
|
||||
// Verificar que el mes pertenece al usuario
|
||||
if ($month->user_id !== $user->id) {
|
||||
throw new \InvalidArgumentException('El mes no pertenece al usuario');
|
||||
}
|
||||
|
||||
// Total de ventas del usuario
|
||||
$totalUserSales = $month->dailySales()->sum('user_sales');
|
||||
|
||||
// Total de ventas del sistema
|
||||
$totalSystemSales = $month->dailySales()->sum('system_sales');
|
||||
|
||||
// Total de gastos
|
||||
$totalExpenses = $month->expenses()->sum('amount');
|
||||
|
||||
// Salario base
|
||||
$monthlySalary = $user->monthly_salary;
|
||||
|
||||
// Porcentaje de comisión
|
||||
$commissionPercentage = $user->commission_percentage;
|
||||
|
||||
// Calcular comisión basada en ventas del sistema (ventas consolidadas)
|
||||
$commission = ($totalSystemSales * $commissionPercentage) / 100;
|
||||
|
||||
// Calcular percepción total (salario + comisión - gastos)
|
||||
$totalEarning = $monthlySalary + $commission - $totalExpenses;
|
||||
|
||||
return [
|
||||
'user_id' => $user->id,
|
||||
'month_id' => $month->id,
|
||||
'month_name' => $month->name . ' ' . $month->year,
|
||||
'total_user_sales' => round($totalUserSales, 2),
|
||||
'total_system_sales' => round($totalSystemSales, 2),
|
||||
'total_expenses' => round($totalExpenses, 2),
|
||||
'monthly_salary' => round($monthlySalary, 2),
|
||||
'commission_percentage' => round($commissionPercentage, 2),
|
||||
'commission_amount' => round($commission, 2),
|
||||
'total_earning' => round($totalEarning, 2),
|
||||
'has_difference' => ($totalUserSales !== $totalSystemSales),
|
||||
'sales_difference' => round($totalUserSales - $totalSystemSales, 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular quincena (primera o segunda)
|
||||
*
|
||||
* QUINCENA 1 (ANTICIPO): mitad salary + comisiones del MES completo
|
||||
* QUINCENA 2 (LIQUIDACIÓN): mitad salary - gastos de la segunda quincena
|
||||
*/
|
||||
public static function calculateBiweekly(User $user, Month $month, int $biweekly): array
|
||||
{
|
||||
if ($month->user_id !== $user->id) {
|
||||
throw new \InvalidArgumentException('El mes no pertenece al usuario');
|
||||
}
|
||||
|
||||
$monthlySalary = $user->monthly_salary;
|
||||
$biweeklySalary = $monthlySalary / 2; // Mitad del sueldo
|
||||
$commissionPercentage = $user->commission_percentage;
|
||||
|
||||
// Get month number
|
||||
$monthNumber = self::getMonthNumber($month->name);
|
||||
$year = $month->year;
|
||||
$lastDay = self::getLastDayOfMonth($month->name, $year);
|
||||
|
||||
if ($biweekly === 1) {
|
||||
// =====================
|
||||
// QUINCENA 1 - ANTICIPO
|
||||
// =====================
|
||||
// Anticipo = mitad del sueldo + comisiones del MES completo
|
||||
$totalSystemSales = $month->dailySales()->sum('system_sales');
|
||||
$commission = ($totalSystemSales * $commissionPercentage) / 100;
|
||||
|
||||
// Gastos: q1 completo + mensual/2
|
||||
$expensesQ1Amount = $month->expenses()
|
||||
->where(function($q) {
|
||||
$q->where('expense_type', 'q1')
|
||||
->orWhere('expense_type', 'mensual');
|
||||
})
|
||||
->get()
|
||||
->sum(function($e) {
|
||||
return $e->expense_type === 'mensual' ? $e->amount / 2 : $e->amount;
|
||||
});
|
||||
|
||||
$totalEarning = $biweeklySalary + $commission - $expensesQ1Amount;
|
||||
|
||||
return [
|
||||
'user_id' => $user->id,
|
||||
'month_id' => $month->id,
|
||||
'month_name' => $month->name . ' ' . $year,
|
||||
'biweekly' => $biweekly,
|
||||
'period' => '1ra Quincena (1-15) - ANTICIPO',
|
||||
'description' => 'Mitad del sueldo + comisiones del mes completo',
|
||||
'biweekly_salary' => round($biweeklySalary, 2),
|
||||
'total_system_sales' => round($totalSystemSales, 2),
|
||||
'commission_percentage' => round($commissionPercentage, 2),
|
||||
'commission_amount' => round($commission, 2),
|
||||
'total_expenses_month' => 0,
|
||||
'expenses_q1' => round($expensesQ1Amount, 2),
|
||||
'expenses_q2' => 0,
|
||||
'total_earning' => round($totalEarning, 2),
|
||||
'type' => 'anticipo',
|
||||
];
|
||||
} else {
|
||||
// =====================
|
||||
// QUINCENA 2 - LIQUIDACIÓN
|
||||
// =====================
|
||||
// Liquidación = mitad del sueldo - gastos de Q2
|
||||
|
||||
// Gastos: q2 completo + mensual/2
|
||||
$expensesQ2Amount = $month->expenses()
|
||||
->where(function($q) {
|
||||
$q->where('expense_type', 'q2')
|
||||
->orWhere('expense_type', 'mensual');
|
||||
})
|
||||
->get()
|
||||
->sum(function($e) {
|
||||
return $e->expense_type === 'mensual' ? $e->amount / 2 : $e->amount;
|
||||
});
|
||||
|
||||
// Total a pagar en liquidacion
|
||||
$totalEarning = $biweeklySalary - $expensesQ2Amount;
|
||||
|
||||
return [
|
||||
'user_id' => $user->id,
|
||||
'month_id' => $month->id,
|
||||
'month_name' => $month->name . ' ' . $year,
|
||||
'biweekly' => $biweekly,
|
||||
'period' => "2da Quincena (16-$lastDay) - LIQUIDACIÓN",
|
||||
'description' => 'Mitad del sueldo - mitad de gastos del mes',
|
||||
'biweekly_salary' => round($biweeklySalary, 2),
|
||||
'total_system_sales' => 0,
|
||||
'expenses_q2' => round($expensesQ2Amount, 2),
|
||||
'total_earning' => round($totalEarning, 2),
|
||||
'type' => 'liquidacion',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumen anual del usuario
|
||||
*/
|
||||
public static function calculateYearly(User $user, int $year): array
|
||||
{
|
||||
$months = $user->months()->where('year', $year)->get();
|
||||
|
||||
$totalUserSales = 0;
|
||||
$totalSystemSales = 0;
|
||||
$totalExpenses = 0;
|
||||
$totalSalary = 0;
|
||||
$totalCommission = 0;
|
||||
|
||||
foreach ($months as $month) {
|
||||
$totalUserSales += $month->dailySales()->sum('user_sales');
|
||||
$totalSystemSales += $month->dailySales()->sum('system_sales');
|
||||
$totalExpenses += $month->expenses()->sum('amount');
|
||||
$totalSalary += $user->monthly_salary;
|
||||
$totalCommission += ($month->dailySales()->sum('system_sales') * $user->commission_percentage) / 100;
|
||||
}
|
||||
|
||||
return [
|
||||
'user_id' => $user->id,
|
||||
'year' => $year,
|
||||
'months_count' => $months->count(),
|
||||
'total_user_sales' => round($totalUserSales, 2),
|
||||
'total_system_sales' => round($totalSystemSales, 2),
|
||||
'total_expenses' => round($totalExpenses, 2),
|
||||
'total_salary' => round($totalSalary, 2),
|
||||
'total_commission' => round($totalCommission, 2),
|
||||
'total_earning' => round($totalSalary + $totalCommission - $totalExpenses, 2),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener número del mes por nombre
|
||||
*/
|
||||
private static function getMonthNumber(string $monthName): int
|
||||
{
|
||||
$months = [
|
||||
'Enero' => 1, 'Febrero' => 2, 'Marzo' => 3, 'Abril' => 4,
|
||||
'Mayo' => 5, 'Junio' => 6, 'Julio' => 7, 'Agosto' => 8,
|
||||
'Septiembre' => 9, 'Octubre' => 10, 'Noviembre' => 11, 'Diciembre' => 12
|
||||
];
|
||||
|
||||
return $months[$monthName] ?? 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener último día del mes
|
||||
*/
|
||||
private static function getLastDayOfMonth(string $monthName, int $year): int
|
||||
{
|
||||
$monthNumber = self::getMonthNumber($monthName);
|
||||
return (int) date('t', mktime(0, 0, 0, $monthNumber, 1, $year));
|
||||
}
|
||||
}
|
||||
300
app/Services/TelegramBotService.php
Executable file
300
app/Services/TelegramBotService.php
Executable file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\TelegramAccount;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TelegramBotService
|
||||
{
|
||||
private ?string $botToken;
|
||||
private ?string $webhookUrl;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->botToken = config('services.telegram.bot_token', env('TELEGRAM_BOT_TOKEN'));
|
||||
|
||||
$appUrl = config('app.url', env('APP_URL', 'http://nomina-pegaso.casa'));
|
||||
$this->webhookUrl = rtrim($appUrl, '/') . '/telegram/webhook';
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar actualización recibida del webhook
|
||||
*/
|
||||
public function handleUpdate(array $update): array
|
||||
{
|
||||
if (!isset($update['message'])) {
|
||||
return ['ok' => false, 'error' => 'No message found'];
|
||||
}
|
||||
|
||||
$message = $update['message'];
|
||||
$chatId = $message['chat']['id'];
|
||||
$text = $message['text'] ?? '';
|
||||
$from = $message['from'] ?? [];
|
||||
|
||||
Log::info('Telegram update received', [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'from' => $from
|
||||
]);
|
||||
|
||||
// Verificar si el usuario está verificado
|
||||
$telegramAccount = TelegramAccount::where('chat_id', $chatId)->first();
|
||||
|
||||
if (!$telegramAccount || !$telegramAccount->is_verified) {
|
||||
return $this->handleUnverifiedUser($chatId, $text);
|
||||
}
|
||||
|
||||
// Procesar comandos del usuario verificado
|
||||
return $this->handleCommand($telegramAccount->user, $text, $chatId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manejar usuario no verificado
|
||||
*/
|
||||
private function handleUnverifiedUser(string $chatId, string $text): array
|
||||
{
|
||||
// Si es un código de verificación
|
||||
if (strlen($text) === 6 && is_numeric($text)) {
|
||||
$telegramAccount = TelegramAccount::where('chat_id', $chatId)
|
||||
->where('verification_code', $text)
|
||||
->first();
|
||||
|
||||
if ($telegramAccount) {
|
||||
$telegramAccount->update([
|
||||
'is_verified' => true,
|
||||
'verification_code' => null
|
||||
]);
|
||||
|
||||
$user = $telegramAccount->user;
|
||||
$this->sendMessage($chatId, "¡Verificación exitosa! Tu cuenta de Telegram está vinculada a {$user->name}. Ahora recibirás notificaciones de tus comisiones.");
|
||||
|
||||
return ['ok' => true, 'verified' => true];
|
||||
} else {
|
||||
$this->sendMessage($chatId, "Código de verificación inválido. Por favor intenta con el código correcto.");
|
||||
return ['ok' => true, 'verified' => false];
|
||||
}
|
||||
}
|
||||
|
||||
// Mensaje de bienvenida para usuarios no verificados
|
||||
$this->sendMessage($chatId, "¡Hola! Para usar este bot necesitas verificar tu cuenta.\n\nPor favor ingresa el código de verificación de 6 dígitos que encontrarás en la sección de Telegram de tu panel de usuario.");
|
||||
|
||||
return ['ok' => true, 'verified' => false];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manejar comandos de usuario verificado
|
||||
*/
|
||||
private function handleCommand(User $user, string $text, string $chatId): array
|
||||
{
|
||||
$command = strtolower(trim($text));
|
||||
$commandParts = explode(' ', $command);
|
||||
$mainCommand = $commandParts[0] ?? '';
|
||||
|
||||
switch ($mainCommand) {
|
||||
case '/start':
|
||||
$this->sendMessage($chatId, "¡Hola {$user->name}! Usa /help para ver los comandos disponibles.");
|
||||
break;
|
||||
|
||||
case '/help':
|
||||
$this->sendHelp($chatId);
|
||||
break;
|
||||
|
||||
case '/mes':
|
||||
$this->showCurrentMonth($user, $chatId);
|
||||
break;
|
||||
|
||||
case '/ventas':
|
||||
$this->showSales($user, $chatId);
|
||||
break;
|
||||
|
||||
case '/gastos':
|
||||
$this->showExpenses($user, $chatId);
|
||||
break;
|
||||
|
||||
case '/resumen':
|
||||
$this->showSummary($user, $chatId);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->sendMessage($chatId, "Comando no reconocido. Usa /help para ver los comandos disponibles.");
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar mensaje
|
||||
*/
|
||||
public function sendMessage(string $chatId, string $text): array
|
||||
{
|
||||
if (!$this->botToken) {
|
||||
Log::warning('Telegram bot token not configured');
|
||||
return ['ok' => false, 'error' => 'Bot token not configured'];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::post("https://api.telegram.org/bot{$this->botToken}/sendMessage", [
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'parse_mode' => 'Markdown'
|
||||
]);
|
||||
|
||||
return $response->json();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Telegram send message error', ['error' => $e->getMessage()]);
|
||||
return ['ok' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar mensaje de ayuda
|
||||
*/
|
||||
private function sendHelp(string $chatId): void
|
||||
{
|
||||
$text = "📋 *Comandos disponibles:*\n\n" .
|
||||
"• /start - Iniciar bot\n" .
|
||||
"• /help - Mostrar ayuda\n" .
|
||||
"• /mes - Ver mes actual\n" .
|
||||
"• /ventas - Ver ventas del mes\n" .
|
||||
"• /gastos - Ver gastos del mes\n" .
|
||||
"• /resumen - Resumen de comisiones\n";
|
||||
|
||||
$this->sendMessage($chatId, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar mes actual
|
||||
*/
|
||||
private function showCurrentMonth(User $user, string $chatId): void
|
||||
{
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
|
||||
return;
|
||||
}
|
||||
|
||||
$statusText = match($month->status) {
|
||||
'open' => '🟢 Abierto',
|
||||
'closed' => '🟡 Cerrado',
|
||||
'paid' => '✅ Pagado',
|
||||
default => 'Desconocido'
|
||||
};
|
||||
|
||||
$text = "📅 *Mes Actual*\n\n" .
|
||||
"• *Nombre:* {$month->name} {$month->year}\n" .
|
||||
"• *Estado:* {$statusText}";
|
||||
|
||||
$this->sendMessage($chatId, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar ventas del mes
|
||||
*/
|
||||
private function showSales(User $user, string $chatId): void
|
||||
{
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
|
||||
return;
|
||||
}
|
||||
|
||||
$totalUserSales = $month->dailySales()->sum('user_sales');
|
||||
$totalSystemSales = $month->dailySales()->sum('system_sales');
|
||||
$diff = $totalUserSales - $totalSystemSales;
|
||||
|
||||
$text = "💰 *Ventas del Mes*\n\n" .
|
||||
"• *Usuario:* $" . number_format($totalUserSales, 2) . "\n" .
|
||||
"• *Sistema:* $" . number_format($totalSystemSales, 2) . "\n" .
|
||||
"• *Diferencia:* $" . number_format($diff, 2);
|
||||
|
||||
$this->sendMessage($chatId, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar gastos del mes
|
||||
*/
|
||||
private function showExpenses(User $user, string $chatId): void
|
||||
{
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
|
||||
return;
|
||||
}
|
||||
|
||||
$totalExpenses = $month->expenses()->sum('amount');
|
||||
$expensesList = $month->expenses()->latest()->limit(5)->get();
|
||||
|
||||
$text = "📝 *Gastos del Mes*\n\n" .
|
||||
"• *Total:* $" . number_format($totalExpenses, 2) . "\n\n";
|
||||
|
||||
if ($expensesList->count() > 0) {
|
||||
$text .= "Últimos gastos:\n";
|
||||
foreach ($expensesList as $expense) {
|
||||
$text .= "• {$expense->description}: \$" . number_format($expense->amount, 2) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$this->sendMessage($chatId, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar resumen de comisiones
|
||||
*/
|
||||
private function showSummary(User $user, string $chatId): void
|
||||
{
|
||||
$month = $user->getCurrentMonth();
|
||||
|
||||
if (!$month) {
|
||||
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
|
||||
return;
|
||||
}
|
||||
|
||||
$data = CommissionCalculator::calculateForMonth($user, $month);
|
||||
|
||||
$text = "💵 *Resumen de Comisiones*\n\n" .
|
||||
"• *Mes:* {$data['month_name']}\n" .
|
||||
"• *Ventas Sistema:* \$" . number_format($data['total_system_sales'], 2) . "\n" .
|
||||
"• *Comisión ({$data['commission_percentage']}%):* \$" . number_format($data['commission_amount'], 2) . "\n" .
|
||||
"• *Salario:* \$" . number_format($data['monthly_salary'], 2) . "\n" .
|
||||
"• *Gastos:* \$" . number_format($data['total_expenses'], 2) . "\n" .
|
||||
"• *Total a Recibir:* \$" . number_format($data['total_earning'], 2);
|
||||
|
||||
$this->sendMessage($chatId, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generar código de verificación
|
||||
*/
|
||||
public static function generateVerificationCode(): string
|
||||
{
|
||||
return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurar webhook
|
||||
*/
|
||||
public function setWebhook(): bool
|
||||
{
|
||||
if (!$this->botToken || !$this->webhookUrl) {
|
||||
Log::warning('Cannot set webhook: missing configuration');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::post("https://api.telegram.org/bot{$this->botToken}/setWebhook", [
|
||||
'url' => $this->webhookUrl
|
||||
]);
|
||||
|
||||
return $response->json('ok', false);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Telegram set webhook error', ['error' => $e->getMessage()]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
artisan
Executable file
18
artisan
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
18
bootstrap/app.php
Executable file
18
bootstrap/app.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Executable file
2
bootstrap/cache/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
7
bootstrap/providers.php
Executable file
7
bootstrap/providers.php
Executable file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
85
composer.json
Executable file
85
composer.json
Executable file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/tinker": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pint": "^1.27",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^12.5.12"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install --ignore-scripts",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
8153
composer.lock
generated
Executable file
8153
composer.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
126
config/app.php
Executable file
126
config/app.php
Executable file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
117
config/auth.php
Executable file
117
config/auth.php
Executable file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
130
config/cache.php
Executable file
130
config/cache.php
Executable file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
|
||||
];
|
||||
184
config/database.php
Executable file
184
config/database.php
Executable file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
80
config/filesystems.php
Executable file
80
config/filesystems.php
Executable file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
132
config/logging.php
Executable file
132
config/logging.php
Executable file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
118
config/mail.php
Executable file
118
config/mail.php
Executable file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
129
config/queue.php
Executable file
129
config/queue.php
Executable file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
38
config/services.php
Executable file
38
config/services.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
233
config/session.php
Executable file
233
config/session.php
Executable file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Serialization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the serialization strategy for session data, which
|
||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||
| objects in the session but can make an application vulnerable to
|
||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||
|
|
||||
| Supported: "json", "php"
|
||||
|
|
||||
*/
|
||||
|
||||
'serialization' => 'json',
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Executable file
1
database/.gitignore
vendored
Executable file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
45
database/factories/UserFactory.php
Executable file
45
database/factories/UserFactory.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
database/migrations/0001_01_01_000000_create_users_table.php
Executable file
52
database/migrations/0001_01_01_000000_create_users_table.php
Executable file
@@ -0,0 +1,52 @@
|
||||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->decimal('commission_percentage', 5, 2)->default(10.00)->comment('Porcentaje de comisión');
|
||||
$table->decimal('monthly_salary', 10, 2)->default(0)->comment('Salario mensual base');
|
||||
$table->boolean('is_active')->default(true)->comment('Si el usuario está activo');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Executable file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?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('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Executable file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Executable file
@@ -0,0 +1,57 @@
|
||||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
22
database/migrations/2024_01_01_000001_add_username_to_users_table.php
Executable file
22
database/migrations/2024_01_01_000001_add_username_to_users_table.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('username')->unique()->after('id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('username');
|
||||
});
|
||||
}
|
||||
};
|
||||
31
database/migrations/2024_01_01_000003_create_telegram_accounts_table.php
Executable file
31
database/migrations/2024_01_01_000003_create_telegram_accounts_table.php
Executable file
@@ -0,0 +1,31 @@
|
||||
<?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('telegram_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('chat_id')->unique()->nullable();
|
||||
$table->string('verification_code', 6)->nullable();
|
||||
$table->boolean('is_verified')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('telegram_accounts');
|
||||
}
|
||||
};
|
||||
33
database/migrations/2024_01_01_000004_create_months_table.php
Executable file
33
database/migrations/2024_01_01_000004_create_months_table.php
Executable file
@@ -0,0 +1,33 @@
|
||||
<?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('months', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('name'); // Nombre del mes (Enero, Febrero, etc.)
|
||||
$table->integer('year'); // Año
|
||||
$table->enum('status', ['open', 'closed', 'paid'])->default('open')->comment('Estado del mes');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'name', 'year']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('months');
|
||||
}
|
||||
};
|
||||
35
database/migrations/2024_01_01_000005_create_daily_sales_table.php
Executable file
35
database/migrations/2024_01_01_000005_create_daily_sales_table.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?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('daily_sales', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('month_id')->constrained()->onDelete('cascade');
|
||||
$table->date('date');
|
||||
$table->decimal('user_sales', 12, 2)->default(0)->comment('Ventas reportadas por el usuario');
|
||||
$table->decimal('system_sales', 12, 2)->default(0)->comment('Ventas del sistema (conciliadas)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['month_id', 'date']);
|
||||
$table->index('date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('daily_sales');
|
||||
}
|
||||
};
|
||||
34
database/migrations/2024_01_01_000006_create_expenses_table.php
Executable file
34
database/migrations/2024_01_01_000006_create_expenses_table.php
Executable file
@@ -0,0 +1,34 @@
|
||||
<?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('expenses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('month_id')->constrained()->onDelete('cascade');
|
||||
$table->string('description');
|
||||
$table->decimal('amount', 10, 2);
|
||||
$table->date('date');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('expenses');
|
||||
}
|
||||
};
|
||||
22
database/migrations/2024_01_01_000007_fix_telegram_chat_id_nullable.php
Executable file
22
database/migrations/2024_01_01_000007_fix_telegram_chat_id_nullable.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('telegram_accounts', function (Blueprint $table) {
|
||||
$table->string('chat_id')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('telegram_accounts', function (Blueprint $table) {
|
||||
$table->string('chat_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
23
database/migrations/2024_01_01_000008_add_expense_type_to_expenses_table.php
Executable file
23
database/migrations/2024_01_01_000008_add_expense_type_to_expenses_table.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('expenses', function (Blueprint $table) {
|
||||
$table->enum('expense_type', ['q1', 'q2', 'mensual'])->default('q1')->after('amount');
|
||||
$table->index('expense_type');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('expenses', function (Blueprint $table) {
|
||||
$table->dropColumn('expense_type');
|
||||
});
|
||||
}
|
||||
};
|
||||
24
database/migrations/2024_01_01_000009_add_employment_details_to_users.php
Executable file
24
database/migrations/2024_01_01_000009_add_employment_details_to_users.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->date('fecha_ingreso')->nullable()->after('monthly_salary');
|
||||
$table->string('razon_social')->nullable()->after('fecha_ingreso');
|
||||
$table->decimal('sueldo_integro_diario', 10, 2)->nullable()->after('razon_social');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['fecha_ingreso', 'razon_social', 'sueldo_integro_diario']);
|
||||
});
|
||||
}
|
||||
};
|
||||
25
database/seeders/DatabaseSeeder.php
Executable file
25
database/seeders/DatabaseSeeder.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
16
package.json
Executable file
16
package.json
Executable file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^3.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
36
phpunit.xml
Executable file
36
phpunit.xml
Executable file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
25
public/.htaccess
Executable file
25
public/.htaccess
Executable file
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
0
public/favicon.ico
Executable file
0
public/favicon.ico
Executable file
20
public/index.php
Executable file
20
public/index.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
242
public/js/app.js
Executable file
242
public/js/app.js
Executable file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Nómina Pegaso - Global JavaScript Functions
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize all components
|
||||
initTooltips();
|
||||
initAnimations();
|
||||
initFormValidation();
|
||||
initDeleteConfirmations();
|
||||
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function() {
|
||||
const alerts = document.querySelectorAll('.alert:not(.alert-permanent)');
|
||||
alerts.forEach(function(alert) {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize Bootstrap tooltips
|
||||
*/
|
||||
function initTooltips() {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subtle animations on page load
|
||||
*/
|
||||
function initAnimations() {
|
||||
// Fade in cards
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach(function(card, index) {
|
||||
card.style.opacity = '0';
|
||||
card.style.transform = 'translateY(20px)';
|
||||
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
|
||||
setTimeout(function() {
|
||||
card.style.opacity = '1';
|
||||
card.style.transform = 'translateY(0)';
|
||||
}, index * 100);
|
||||
});
|
||||
|
||||
// Animate stat numbers
|
||||
const statNumbers = document.querySelectorAll('.stat-number');
|
||||
statNumbers.forEach(function(el) {
|
||||
const target = parseFloat(el.getAttribute('data-target'));
|
||||
if (target) {
|
||||
animateValue(el, 0, target, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate number counting
|
||||
*/
|
||||
function animateValue(el, start, end, duration) {
|
||||
let startTimestamp = null;
|
||||
const step = function(timestamp) {
|
||||
if (!startTimestamp) startTimestamp = timestamp;
|
||||
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
||||
const value = progress * (end - start) + start;
|
||||
el.textContent = formatCurrency(value);
|
||||
if (progress < 1) {
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
function formatCurrency(amount) {
|
||||
return '$' + parseFloat(amount).toLocaleString('es-ES', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize form validation feedback
|
||||
*/
|
||||
function initFormValidation() {
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
|
||||
forms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add confirmation to delete buttons
|
||||
*/
|
||||
function initDeleteConfirmations() {
|
||||
const deleteButtons = document.querySelectorAll('.btn-delete');
|
||||
|
||||
deleteButtons.forEach(function(btn) {
|
||||
btn.addEventListener('click', function(e) {
|
||||
if (!confirm('¿Estás seguro de que deseas eliminar este elemento?')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
window.formatDate = function(dateStr, format = 'short') {
|
||||
if (!dateStr) return '';
|
||||
|
||||
const date = new Date(dateStr);
|
||||
|
||||
if (format === 'short') {
|
||||
return date.toLocaleDateString('es-ES');
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('es-ES', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
window.showToast = function(message, type = 'info') {
|
||||
const toastContainer = document.querySelector('.toast-container');
|
||||
|
||||
if (!toastContainer) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast show align-items-center text-white bg-${type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
const icons = {
|
||||
success: 'bi-check-circle',
|
||||
error: 'bi-exclamation-circle',
|
||||
warning: 'bi-exclamation-triangle',
|
||||
info: 'bi-info-circle'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="${icons[type] || 'bi-info-circle'} me-2"></i>
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
setTimeout(function() {
|
||||
toast.remove();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mobile sidebar functionality
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileNavBtn = document.getElementById('mobileNavBtn');
|
||||
const mobileNavBtn2 = document.getElementById('mobileNavBtn2');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
function toggleSidebar() {
|
||||
if (sidebar && sidebarOverlay) {
|
||||
sidebar.classList.toggle('show');
|
||||
sidebarOverlay.classList.toggle('show');
|
||||
}
|
||||
}
|
||||
|
||||
if (mobileNavBtn) {
|
||||
mobileNavBtn.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
if (mobileNavBtn2) {
|
||||
mobileNavBtn2.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
// Close sidebar on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && sidebar && sidebar.classList.contains('show')) {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to parse numeric input
|
||||
*/
|
||||
window.parseNumber = function(value, decimals = 2) {
|
||||
const parsed = parseFloat(value);
|
||||
return isNaN(parsed) ? 0 : parseFloat(parsed.toFixed(decimals));
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate percentage
|
||||
*/
|
||||
window.calculatePercentage = function(value, total) {
|
||||
if (total === 0) return 0;
|
||||
return ((value / total) * 100).toFixed(2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounce function for search inputs
|
||||
*/
|
||||
window.debounce = function(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
138
public/js/calendar.js
Executable file
138
public/js/calendar.js
Executable file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Calendar JavaScript - FullCalendar Configuration
|
||||
* Nómina Pegaso
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Calendar initialization is now handled in the blade template
|
||||
// This file provides additional utility functions
|
||||
|
||||
/**
|
||||
* Format currency values
|
||||
*/
|
||||
window.formatCurrency = function(amount) {
|
||||
return '$' + parseFloat(amount).toLocaleString('es-ES', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
window.formatDate = function(dateStr, format = 'long') {
|
||||
const date = new Date(dateStr + 'T00:00:00');
|
||||
|
||||
if (format === 'short') {
|
||||
return date.toLocaleDateString('es-ES', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('es-ES', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show notification toast
|
||||
*/
|
||||
window.showToast = function(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast show align-items-center text-white bg-${type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.querySelector('.toast-container') || createToastContainer();
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
function createToastContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm action with modal
|
||||
*/
|
||||
window.confirmAction = function(message, onConfirm) {
|
||||
if (confirm(message)) {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate date is within current month
|
||||
*/
|
||||
window.validateDateInMonth = function(dateInput, monthId) {
|
||||
// This can be extended to validate against specific month
|
||||
const selectedDate = new Date(dateInput.value);
|
||||
const now = new Date();
|
||||
|
||||
return selectedDate <= now;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate difference between two values
|
||||
*/
|
||||
window.calculateDifference = function(value1, value2) {
|
||||
const diff = parseFloat(value1) - parseFloat(value2);
|
||||
const sign = diff >= 0 ? '+' : '';
|
||||
return sign + formatCurrency(diff);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update summary cards dynamically
|
||||
*/
|
||||
window.updateSummaryCard = function(cardId, value) {
|
||||
const card = document.getElementById(cardId);
|
||||
if (card) {
|
||||
card.textContent = formatCurrency(value);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Mobile sidebar toggle
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileNavBtn = document.getElementById('mobileNavBtn');
|
||||
const mobileNavBtn2 = document.getElementById('mobileNavBtn2');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebar.classList.toggle('show');
|
||||
sidebarOverlay.classList.toggle('show');
|
||||
}
|
||||
|
||||
if (mobileNavBtn) mobileNavBtn.addEventListener('click', toggleSidebar);
|
||||
if (mobileNavBtn2) mobileNavBtn2.addEventListener('click', toggleSidebar);
|
||||
if (sidebarOverlay) sidebarOverlay.addEventListener('click', toggleSidebar);
|
||||
|
||||
// Close sidebar on escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && sidebar.classList.contains('show')) {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
2
public/robots.txt
Executable file
2
public/robots.txt
Executable file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
11
resources/css/app.css
Executable file
11
resources/css/app.css
Executable file
@@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
1
resources/js/app.js
Executable file
1
resources/js/app.js
Executable file
@@ -0,0 +1 @@
|
||||
//
|
||||
53
resources/views/auth/login.blade.php
Executable file
53
resources/views/auth/login.blade.php
Executable file
@@ -0,0 +1,53 @@
|
||||
@extends('layouts.guest')
|
||||
|
||||
@section('title', 'Iniciar Sesión')
|
||||
|
||||
@section('content')
|
||||
<div class="auth-card p-4">
|
||||
<h3 class="text-center mb-4">
|
||||
<i class="bi bi-person-circle"></i> Nómina Pegaso
|
||||
</h3>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Usuario</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
value="{{ old('username') }}" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Contraseña</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">Recordarme</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Iniciar Sesión</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="{{ route('register') }}">¿No tienes cuenta? Regístrate</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
84
resources/views/auth/register.blade.php
Executable file
84
resources/views/auth/register.blade.php
Executable file
@@ -0,0 +1,84 @@
|
||||
@extends('layouts.guest')
|
||||
|
||||
@section('title', 'Registrarse')
|
||||
|
||||
@section('content')
|
||||
<div class="auth-card p-4">
|
||||
<h3 class="text-center mb-4">
|
||||
<i class="bi bi-person-plus"></i> Registrarse
|
||||
</h3>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nombre de Usuario</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
value="{{ old('username') }}" required>
|
||||
<small class="text-muted">Único - sin espacios ni acentos</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nombre Completo</label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
value="{{ old('name') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Correo Electrónico</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
value="{{ old('email') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Contraseña</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
<small class="text-muted">Mínimo 8 caracteres</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password_confirmation" class="form-label">Confirmar Contraseña</label>
|
||||
<input type="password" class="form-control" id="password_confirmation"
|
||||
name="password_confirmation" required>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5 class="mb-3">Configuración de Comisión</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="commission_percentage" class="form-label">Porcentaje de Comisión (%)</label>
|
||||
<input type="number" class="form-control" id="commission_percentage"
|
||||
name="commission_percentage" value="{{ old('commission_percentage', 10) }}"
|
||||
min="0" max="100" step="0.01" required>
|
||||
<small class="text-muted">Porcentaje que recibirás sobre tus ventas</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="monthly_salary" class="form-label">Salario Mensual Base</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="monthly_salary"
|
||||
name="monthly_salary" value="{{ old('monthly_salary', 0) }}"
|
||||
min="0" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success w-100">Registrarse</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="{{ route('login') }}">¿Ya tienes cuenta? Inicia Sesión</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
584
resources/views/calendar/index.blade.php
Executable file
584
resources/views/calendar/index.blade.php
Executable file
@@ -0,0 +1,584 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Calendario')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">
|
||||
<i class="bi bi-calendar3 text-primary"></i> Calendario de Ventas y Gastos
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector de mes -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="GET" class="d-flex gap-2 flex-wrap">
|
||||
<select name="year" class="form-select" style="width: auto;" onchange="this.form.submit()">
|
||||
@php($years = range(date('Y'), date('Y') - 5))
|
||||
@foreach($years as $y)
|
||||
<option value="{{ $y }}" {{ $year == $y ? 'selected' : '' }}>{{ $y }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="month" class="form-select" style="width: auto;" onchange="this.form.submit()">
|
||||
<option value="">Todos los meses</option>
|
||||
@foreach(['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] as $m)
|
||||
<option value="{{ $m }}" {{ request('month') == $m ? 'selected' : '' }}>{{ $m }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
@if($currentMonth)
|
||||
<span class="badge bg-primary fs-6">
|
||||
<i class="bi bi-calendar"></i> {{ $currentMonth->name }} {{ $currentMonth->year }}
|
||||
</span>
|
||||
<span class="badge bg-{{ $currentMonth->status === 'open' ? 'success' : ($currentMonth->status === 'closed' ? 'warning' : 'info') }}">
|
||||
{{ ucfirst($currentMonth->status) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($currentMonth)
|
||||
<!-- Resumen del mes -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card stat-card primary">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Ventas del Usuario</h6>
|
||||
<h4 class="text-primary">${{ number_format($currentMonth->dailySales()->sum('user_sales'), 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card stat-card success">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Ventas del Sistema</h6>
|
||||
<h4 class="text-success">${{ number_format($currentMonth->dailySales()->sum('system_sales'), 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card stat-card warning">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Gastos</h6>
|
||||
<h4 class="text-warning">${{ number_format($currentMonth->expenses()->sum('amount'), 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendario FullCalendar -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leyenda -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
<span class="badge bg-light text-dark border p-2">
|
||||
<i class="bi bi-square text-secondary"></i> Sin datos
|
||||
</span>
|
||||
<span class="badge bg-success-subtle text-success border p-2">
|
||||
<i class="bi bi-square text-success"></i> Con ventas
|
||||
</span>
|
||||
<span class="badge bg-danger-subtle text-danger border p-2">
|
||||
<i class="bi bi-square text-danger"></i> Con gastos
|
||||
</span>
|
||||
<span class="badge bg-warning-subtle text-warning border p-2">
|
||||
<i class="bi bi-square text-warning"></i> Con ambos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="bi bi-info-circle"></i> No hay un mes seleccionado</h5>
|
||||
<p>Selecciona un mes del dropdown o crea uno nuevo.</p>
|
||||
<a href="{{ route('months.index') }}" class="btn btn-primary">Ver Meses</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Modal para capturar ventas -->
|
||||
<div class="modal fade" id="dayModal" tabindex="-1" aria-labelledby="dayModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="dayModalLabel">
|
||||
<i class="bi bi-calendar-plus"></i> Capturar Día
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form method="POST" id="dayForm">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="month_id" id="formMonthId" value="{{ $currentMonth->id ?? '' }}">
|
||||
<input type="hidden" name="date" id="modalDate">
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Fecha: <strong id="modalDateDisplay"></strong>
|
||||
</div>
|
||||
|
||||
<!-- Ventas -->
|
||||
<h6 class="border-bottom pb-2 mb-3">
|
||||
<i class="bi bi-currency-dollar text-success"></i> Ventas
|
||||
</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="user_sales" class="form-label">Ventas del Usuario</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="user_sales" name="user_sales"
|
||||
value="0" step="0.01" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="system_sales" class="form-label">Ventas del Sistema</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="system_sales" name="system_sales"
|
||||
value="0" step="0.01" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gastos -->
|
||||
<h6 class="border-bottom pb-2 mb-3 mt-4">
|
||||
<i class="bi bi-receipt text-danger"></i> Gastos
|
||||
</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expense_description" class="form-label">Descripción del Gasto</label>
|
||||
<input type="text" class="form-control" id="expense_description" name="expense_description"
|
||||
placeholder="Opcional">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expense_amount" class="form-label">Monto del Gasto</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="expense_amount" name="expense_amount"
|
||||
value="0" step="0.01" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expense_type" class="form-label">Quincena del Gasto</label>
|
||||
<select class="form-select" id="expense_type" name="expense_type">
|
||||
<option value="q1">1ra Quincena (1-15)</option>
|
||||
<option value="q2">2da Quincena (16-31)</option>
|
||||
<option value="mensual">Gasto Mensual (se divide en 2)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para ver detalles del día -->
|
||||
<div class="modal fade" id="dayDetailModal" tabindex="-1" aria-labelledby="dayDetailModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-white">
|
||||
<h5 class="modal-title" id="dayDetailModalLabel">
|
||||
<i class="bi bi-eye"></i> Detalles del Día
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-3"><strong>Fecha:</strong> <span id="detailDate"></span></p>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-muted">Ventas Usuario</h6>
|
||||
<h5 class="text-success" id="detailUserSales">$0.00</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-muted">Ventas Sistema</h6>
|
||||
<h5 class="text-info" id="detailSystemSales">$0.00</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Gastos del Día</h6>
|
||||
<div id="detailExpenses">
|
||||
<p class="text-muted mb-0">No hay gastos registrados</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
<button type="button" class="btn btn-primary" id="editDayBtn">
|
||||
<i class="bi bi-pencil"></i> Editar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Data for calendar events
|
||||
const dailySales = @json($dailySales ?? []);
|
||||
const expenses = @json($expenses ?? []);
|
||||
const year = {{ $year }};
|
||||
const currentMonthId = {{ $currentMonth->id ?? 0 }};
|
||||
|
||||
console.log('Month ID:', currentMonthId);
|
||||
console.log('DailySales raw:', dailySales);
|
||||
console.log('DailySales type:', typeof dailySales);
|
||||
console.log('Expenses:', expenses);
|
||||
|
||||
// Map data to calendar events
|
||||
const events = [];
|
||||
|
||||
// Add sales events - iterate over object values
|
||||
Object.keys(dailySales).forEach(date => {
|
||||
const saleData = dailySales[date];
|
||||
if (saleData && saleData.user_sales > 0) {
|
||||
events.push({
|
||||
title: 'V: $' + parseFloat(saleData.user_sales).toLocaleString(),
|
||||
start: date,
|
||||
className: 'bg-success',
|
||||
extendedProps: {
|
||||
type: 'sale',
|
||||
user_sales: saleData.user_sales,
|
||||
system_sales: saleData.system_sales
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add expense events
|
||||
Object.keys(expenses).forEach(date => {
|
||||
const expData = expenses[date];
|
||||
if (expData && expData.amount > 0) {
|
||||
events.push({
|
||||
title: 'G: $' + parseFloat(expData.amount).toLocaleString(),
|
||||
start: date,
|
||||
className: 'bg-danger',
|
||||
extendedProps: {
|
||||
type: 'expense',
|
||||
amount: expData.amount,
|
||||
description: expData.description
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Events:', events);
|
||||
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
|
||||
// FullCalendar initialization
|
||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
initialDate: year + '-04-01',
|
||||
locale: 'es',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,listMonth'
|
||||
},
|
||||
buttonText: {
|
||||
today: 'Hoy',
|
||||
month: 'Mes',
|
||||
list: 'Lista'
|
||||
},
|
||||
dayMaxEvents: true,
|
||||
eventDisplay: 'block',
|
||||
events: events,
|
||||
dateClick: function(info) {
|
||||
// Verify month exists
|
||||
const monthId = {{ $currentMonth->id ?? 0 }};
|
||||
if (!monthId || monthId === 0) {
|
||||
console.log('No hay mes seleccionado');
|
||||
return;
|
||||
}
|
||||
openDayModal(info.dateStr);
|
||||
},
|
||||
eventClick: function(info) {
|
||||
showDayDetails(info.event.startStr);
|
||||
},
|
||||
eventDidMount: function(info) {
|
||||
// Add custom styling based on data
|
||||
const date = info.event.startStr;
|
||||
if (dailySales[date] && expenses[date]) {
|
||||
info.el.style.background = 'linear-gradient(135deg, #f39c12 0%, #e74c3c 100%)';
|
||||
} else if (dailySales[date]) {
|
||||
info.el.style.background = 'linear-gradient(135deg, #27ae60 0%, #2ecc71 100%)';
|
||||
} else if (expenses[date]) {
|
||||
info.el.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
|
||||
// Initialize selected date variable
|
||||
window.selectedDate = null;
|
||||
|
||||
// Functions
|
||||
window.openDayModal = function(dateStr) {
|
||||
const modalEl = document.getElementById('dayModal');
|
||||
if (!modalEl) return;
|
||||
|
||||
const modal = new bootstrap.Modal(modalEl);
|
||||
|
||||
const modalDateEl = document.getElementById('modalDate');
|
||||
const modalDateDisplayEl = document.getElementById('modalDateDisplay');
|
||||
|
||||
if (modalDateEl && modalDateDisplayEl) {
|
||||
modalDateEl.value = dateStr;
|
||||
modalDateDisplayEl.textContent = formatDate(dateStr);
|
||||
}
|
||||
|
||||
// Check if there's existing data
|
||||
const userSalesEl = document.getElementById('user_sales');
|
||||
const systemSalesEl = document.getElementById('system_sales');
|
||||
const expenseDescEl = document.getElementById('expense_description');
|
||||
const expenseAmountEl = document.getElementById('expense_amount');
|
||||
|
||||
if (userSalesEl && systemSalesEl) {
|
||||
// Buscar coincidencia parcial
|
||||
let saleData = dailySales[dateStr];
|
||||
if (!saleData) {
|
||||
// Buscar en claves
|
||||
for (const key in dailySales) {
|
||||
if (key.includes(dateStr) || dateStr.includes(key)) {
|
||||
saleData = dailySales[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (saleData) {
|
||||
userSalesEl.value = saleData.user_sales;
|
||||
systemSalesEl.value = saleData.system_sales;
|
||||
} else {
|
||||
userSalesEl.value = 0;
|
||||
systemSalesEl.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (expenseDescEl && expenseAmountEl) {
|
||||
let expData = expenses[dateStr];
|
||||
if (!expData) {
|
||||
for (const key in expenses) {
|
||||
if (key.includes(dateStr) || dateStr.includes(key)) {
|
||||
expData = expenses[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expData) {
|
||||
expenseDescEl.value = expData.description || '';
|
||||
expenseAmountEl.value = expData.amount;
|
||||
} else {
|
||||
expenseDescEl.value = '';
|
||||
expenseAmountEl.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
modal.show();
|
||||
};
|
||||
|
||||
window.showDayDetails = function(dateStr) {
|
||||
const modal = new bootstrap.Modal(document.getElementById('dayDetailModal'));
|
||||
document.getElementById('detailDate').textContent = formatDate(dateStr);
|
||||
|
||||
// Buscar en todas las claves
|
||||
let foundKey = null;
|
||||
for (const key in dailySales) {
|
||||
if (key.includes(dateStr) || dateStr.includes(key)) {
|
||||
foundKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundKey) {
|
||||
// Found
|
||||
document.getElementById('detailUserSales').textContent = '$' + dailySales[foundKey].user_sales.toLocaleString();
|
||||
document.getElementById('detailSystemSales').textContent = '$' + dailySales[foundKey].system_sales.toLocaleString();
|
||||
} else {
|
||||
document.getElementById('detailUserSales').textContent = '$0.00';
|
||||
document.getElementById('detailSystemSales').textContent = '$0.00';
|
||||
}
|
||||
|
||||
if (expenses[dateStr]) {
|
||||
document.getElementById('detailExpenses').innerHTML = `
|
||||
<p class="mb-1"><strong>${expenses[dateStr].description || 'Gasto'}</strong></p>
|
||||
<h5 class="text-danger mb-0">$${expenses[dateStr].amount.toLocaleString()}</h5>
|
||||
`;
|
||||
} else {
|
||||
document.getElementById('detailExpenses').innerHTML = '<p class="text-muted mb-0">No hay gastos registrados</p>';
|
||||
}
|
||||
|
||||
// Set up edit button
|
||||
document.getElementById('editDayBtn').onclick = function() {
|
||||
window.selectedDate = dateStr;
|
||||
modal.hide();
|
||||
setTimeout(() => openDayModal(dateStr), 300);
|
||||
};
|
||||
|
||||
modal.show();
|
||||
};
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return 'Sin fecha';
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
const year = parseInt(parts[0]);
|
||||
const month = parseInt(parts[1]) - 1;
|
||||
const day = parseInt(parts[2]);
|
||||
const date = new Date(year, month, day);
|
||||
return date.toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
// Form submission handler
|
||||
document.getElementById('dayForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get values directly from DOM elements
|
||||
const monthIdEl = document.getElementById('formMonthId');
|
||||
const dateEl = document.getElementById('modalDate');
|
||||
const userSalesEl = document.getElementById('user_sales');
|
||||
const systemSalesEl = document.getElementById('system_sales');
|
||||
const expenseAmountEl = document.getElementById('expense_amount');
|
||||
const expenseDescEl = document.getElementById('expense_description');
|
||||
const expenseTypeEl = document.getElementById('expense_type');
|
||||
|
||||
let monthId = monthIdEl ? monthIdEl.value : '';
|
||||
let date = dateEl ? dateEl.value : '';
|
||||
|
||||
// Use window.selectedDate as fallback
|
||||
if (!date && window.selectedDate) date = window.selectedDate;
|
||||
|
||||
// Try fallback
|
||||
if (!monthId) monthId = '{{ $currentMonth->id ?? 0 }}';
|
||||
|
||||
const userSales = userSalesEl ? (parseFloat(userSalesEl.value) || 0) : 0;
|
||||
const systemSales = systemSalesEl ? (parseFloat(systemSalesEl.value) || 0) : 0;
|
||||
const expenseAmount = expenseAmountEl ? (parseFloat(expenseAmountEl.value) || 0) : 0;
|
||||
const expenseDesc = expenseDescEl ? expenseDescEl.value : '';
|
||||
const expenseType = expenseTypeEl ? expenseTypeEl.value : 'q1';
|
||||
|
||||
// Save data
|
||||
if (!monthId || !date) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
month_id: monthId,
|
||||
date: date,
|
||||
user_sales: userSales,
|
||||
system_sales: systemSales,
|
||||
expense_description: expenseDesc,
|
||||
expense_amount: expenseAmount,
|
||||
expense_type: expenseType
|
||||
};
|
||||
|
||||
// Send data via fetch
|
||||
fetch('{{ route("calendar.day.store") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
console.error('Error saving:', data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.fc {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.fc .fc-toolbar-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-day-number {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
padding: 2px 4px;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.fc .fc-col-header-cell-cushion {
|
||||
padding: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fc-daygrid-day.has-data {
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||
}
|
||||
|
||||
.fc-daygrid-day.has-expense {
|
||||
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
||||
}
|
||||
|
||||
.fc-daygrid-day.has-both {
|
||||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
||||
}
|
||||
|
||||
.day-cell-clickable {
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.day-cell-clickable:hover {
|
||||
background: #e3f2fd !important;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
170
resources/views/dashboard/index.blade.php
Executable file
170
resources/views/dashboard/index.blade.php
Executable file
@@ -0,0 +1,170 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Bienvenido, {{ auth()->user()->name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($currentMonth)
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
Resumen de {{ $currentMonth->name }} {{ $currentMonth->year }}
|
||||
@if($currentMonth->status === 'open')
|
||||
<span class="badge bg-success ms-2">Abierto</span>
|
||||
@elseif($currentMonth->status === 'closed')
|
||||
<span class="badge bg-warning ms-2">Cerrado</span>
|
||||
@else
|
||||
<span class="badge bg-info ms-2">Pagado</span>
|
||||
@endif
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($data)
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card primary mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Ventas del Usuario</h6>
|
||||
<h4>${{ number_format($data['total_user_sales'], 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card success mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Ventas del Sistema</h6>
|
||||
<h4>${{ number_format($data['total_system_sales'], 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card warning mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Comisión ({{ $data['commission_percentage'] }}%)</h6>
|
||||
<h4>${{ number_format($data['commission_amount'], 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card danger mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total a Recibir</h6>
|
||||
<h4>${{ number_format($data['total_earning'], 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Salario Mensual
|
||||
<span class="badge bg-primary rounded-pill">${{ number_format($data['monthly_salary'], 2) }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Gastos del Mes
|
||||
<span class="badge bg-danger rounded-pill">${{ number_format($data['total_expenses'], 2) }}</span>
|
||||
</li>
|
||||
@if($data['has_difference'])
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
Diferencia Ventas
|
||||
<span class="badge bg-warning rounded-pill">${{ number_format($data['sales_difference'], 2) }}</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<a href="{{ route('reports.monthly', ['month_id' => $currentMonth->id]) }}" class="btn btn-primary">
|
||||
<i class="bi bi-file-earmark-text"></i> Ver Reporte Mensual
|
||||
</a>
|
||||
<a href="{{ route('reports.biweekly', ['month_id' => $currentMonth->id, 'biweekly' => 1]) }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-file-earmark-text"></i> Ver Quincena
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
No hay datos suficientes para calcular el resumen. Agrega ventas y gastos.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-warning">
|
||||
<h5>No hay un mes de trabajo activo</h5>
|
||||
<p>Para comenzar, crea un nuevo mes de trabajo.</p>
|
||||
<a href="{{ route('months.create') }}" class="btn btn-primary">Crear Mes</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Últimos meses -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h4 class="mb-3">Meses Recientes</h4>
|
||||
</div>
|
||||
@forelse($recentMonths as $month)
|
||||
<div class="col-md-4 col-lg-2 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body text-center">
|
||||
<h6>{{ $month->name }}</h6>
|
||||
<small class="text-muted">{{ $month->year }}</small>
|
||||
<div class="mt-2">
|
||||
@if($month->status === 'open')
|
||||
<span class="badge bg-success">Abierto</span>
|
||||
@elseif($month->status === 'closed')
|
||||
<span class="badge bg-warning">Cerrado</span>
|
||||
@else
|
||||
<span class="badge bg-info">Pagado</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<a href="{{ route('months.show', $month->id) }}" class="btn btn-sm btn-outline-primary w-100">Ver</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">No hay meses registrados.</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<!-- Acceso rápido -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h4 class="mb-3">Acceso Rápido</h4>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{{ route('sales.create') }}" class="btn btn-success w-100 mb-2">
|
||||
<i class="bi bi-plus-circle"></i> Nueva Venta
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{{ route('expenses.create') }}" class="btn btn-warning w-100 mb-2">
|
||||
<i class="bi bi-plus-circle"></i> Nuevo Gasto
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{{ route('calendar') }}" class="btn btn-info w-100 mb-2">
|
||||
<i class="bi bi-calendar3"></i> Ver Calendario
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{{ route('reports.monthly') }}" class="btn btn-secondary w-100 mb-2">
|
||||
<i class="bi bi-graph-up"></i> Ver Reportes
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
63
resources/views/expenses/create.blade.php
Executable file
63
resources/views/expenses/create.blade.php
Executable file
@@ -0,0 +1,63 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Nuevo Gasto')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Nuevo Gasto</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('expenses.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="month_id" value="{{ $month->id }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Descripción</label>
|
||||
<input type="text" class="form-control" id="description" name="description"
|
||||
value="{{ old('description') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">Monto</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="amount" name="amount"
|
||||
value="{{ old('amount') }}" step="0.01" min="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="date" class="form-label">Fecha</label>
|
||||
<input type="date" class="form-control" id="date" name="date"
|
||||
value="{{ old('date', now()->format('Y-m-d')) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expense_type" class="form-label">Quincena</label>
|
||||
<select class="form-select" id="expense_type" name="expense_type" required>
|
||||
<option value="q1">1ra Quincena (1-15)</option>
|
||||
<option value="q2">2da Quincena (16-31)</option>
|
||||
<option value="mensual">Gasto Mensual (se divide en 2)</option>
|
||||
</select>
|
||||
<small class="text-muted">
|
||||
Selecciona a qué quincena se restará el gasto.
|
||||
Los gastos mensuales se dividen entre 2.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-warning">Guardar Gasto</button>
|
||||
<a href="{{ route('expenses.index', ['month_id' => $month->id]) }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
59
resources/views/expenses/edit.blade.php
Executable file
59
resources/views/expenses/edit.blade.php
Executable file
@@ -0,0 +1,59 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Editar Gasto')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Editar Gasto</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('expenses.update', $expense->id) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Descripción</label>
|
||||
<input type="text" class="form-control" id="description" name="description"
|
||||
value="{{ old('description', $expense->description) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">Monto</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="amount" name="amount"
|
||||
value="{{ old('amount', $expense->amount) }}" step="0.01" min="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="date" class="form-label">Fecha</label>
|
||||
<input type="date" class="form-control" id="date" name="date"
|
||||
value="{{ old('date', $expense->date->format('Y-m-d')) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expense_type" class="form-label">Quincena</label>
|
||||
<select class="form-select" id="expense_type" name="expense_type" required>
|
||||
<option value="q1" {{ $expense->expense_type == 'q1' ? 'selected' : '' }}>1ra Quincena (1-15)</option>
|
||||
<option value="q2" {{ $expense->expense_type == 'q2' ? 'selected' : '' }}>2da Quincena (16-31)</option>
|
||||
<option value="mensual" {{ $expense->expense_type == 'mensual' ? 'selected' : '' }}>Gasto Mensual (se divide en 2)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Actualizar</button>
|
||||
<a href="{{ route('expenses.index', ['month_id' => $expense->month_id]) }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
99
resources/views/expenses/index.blade.php
Executable file
99
resources/views/expenses/index.blade.php
Executable file
@@ -0,0 +1,99 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Gastos')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Gestión de Gastos</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector de mes -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<select name="month_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">Todos los meses</option>
|
||||
@foreach($months as $m)
|
||||
<option value="{{ $m->id }}" {{ $month && $month->id == $m->id ? 'selected' : '' }}>
|
||||
{{ $m->name }} {{ $m->year }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<a href="{{ route('expenses.create', ['month_id' => $month?->id]) }}" class="btn btn-warning">
|
||||
<i class="bi bi-plus-circle"></i> Nuevo Gasto
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($month)
|
||||
<!-- Resumen -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Gastos del Mes</h6>
|
||||
<h3>${{ number_format($month->expenses()->sum('amount'), 2) }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Lista de gastos -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Descripción</th>
|
||||
<th>Monto</th>
|
||||
<th>Quincena</th>
|
||||
<th>Mes</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($expenses as $expense)
|
||||
<tr>
|
||||
<td>{{ $expense->date->format('d/m/Y') }}</td>
|
||||
<td>{{ $expense->description }}</td>
|
||||
<td class="text-danger">-${{ number_format($expense->amount, 2) }}</td>
|
||||
<td>
|
||||
@if($expense->expense_type == 'q1')
|
||||
<span class="badge bg-primary">Q1</span>
|
||||
@elseif($expense->expense_type == 'q2')
|
||||
<span class="badge bg-warning">Q2</span>
|
||||
@else
|
||||
<span class="badge bg-info">Mensual</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $expense->month->name }} {{ $expense->month->year }}</td>
|
||||
<td>
|
||||
<a href="{{ route('expenses.edit', $expense->id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('expenses.destroy', $expense->id) }}" class="d-inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar este gasto?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">No hay gastos registrados.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ $expenses->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
310
resources/views/layouts/app.blade.php
Executable file
310
resources/views/layouts/app.blade.php
Executable file
@@ -0,0 +1,310 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'Nómina Pegaso')</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/main.min.css' rel='stylesheet' />
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%);
|
||||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
color: rgba(255,255,255,0.8);
|
||||
text-decoration: none;
|
||||
padding: 12px 20px;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
margin: 4px 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.sidebar a.active {
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar a i {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
/* Card Styles */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border-left: 4px solid;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat-card.primary { border-color: #3498db; }
|
||||
.stat-card.success { border-color: #27ae60; }
|
||||
.stat-card.warning { border-color: #f39c12; }
|
||||
.stat-card.danger { border-color: #e74c3c; }
|
||||
|
||||
/* Calendar Styles */
|
||||
.fc .fc-daygrid-day:hover {
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.day-with-sales {
|
||||
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%) !important;
|
||||
}
|
||||
|
||||
.day-with-expenses {
|
||||
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%) !important;
|
||||
}
|
||||
|
||||
.day-empty {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Mobile Styles */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 280px;
|
||||
z-index: 1050;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.show {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1040;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-overlay.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-nav-btn {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-btn {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
z-index: 1030;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-control, .form-select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
/* Badge Styles */
|
||||
.badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Toast Styles */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
@auth
|
||||
<!-- Sidebar Overlay (Mobile) -->
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
|
||||
<!-- Mobile Navigation Button -->
|
||||
<button class="btn btn-primary mobile-nav-btn" id="mobileNavBtn">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<nav class="col-md-2 d-none d-md-block sidebar py-3" id="sidebar">
|
||||
<div class="sidebar-brand text-center mb-4">
|
||||
<h5 class="text-white mb-1">
|
||||
<i class="bi bi-cash-coin"></i> Nómina Pegaso
|
||||
</h5>
|
||||
<small class="text-white-50">{{ auth()->user()->name }}</small>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-house-door"></i> Dashboard
|
||||
</a>
|
||||
<a href="{{ route('calendar') }}" class="{{ request()->routeIs('calendar*') ? 'active' : '' }}">
|
||||
<i class="bi bi-calendar3"></i> Calendario
|
||||
</a>
|
||||
<a href="{{ route('sales.index') }}" class="{{ request()->routeIs('sales.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-currency-dollar"></i> Ventas
|
||||
</a>
|
||||
<a href="{{ route('expenses.index') }}" class="{{ request()->routeIs('expenses.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-receipt"></i> Gastos
|
||||
</a>
|
||||
<a href="{{ route('months.index') }}" class="{{ request()->routeIs('months.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-calendar-month"></i> Meses
|
||||
</a>
|
||||
<hr class="border-secondary mx-3">
|
||||
<a href="{{ route('reports.monthly') }}" class="{{ request()->routeIs('reports.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-graph-up"></i> Reportes
|
||||
</a>
|
||||
<a href="{{ route('settings.index') }}" class="{{ request()->routeIs('settings.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-gear"></i> Configuración
|
||||
</a>
|
||||
<a href="{{ route('telegram.verify') }}" class="{{ request()->routeIs('telegram.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-telegram"></i> Telegram
|
||||
</a>
|
||||
<hr class="border-secondary mx-3">
|
||||
<form method="POST" action="{{ route('logout') }}" class="px-3">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-link text-white w-100 text-start p-2">
|
||||
<i class="bi bi-box-arrow-right"></i> Cerrar Sesión
|
||||
</button>
|
||||
</form>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="col-md-10 ms-sm-auto px-4 py-4">
|
||||
<!-- Mobile Header -->
|
||||
<div class="d-md-none mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">Nómina Pegaso</span>
|
||||
<button class="btn btn-outline-primary btn-sm" id="mobileNavBtn2">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@yield('content')
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@yield('content')
|
||||
@endauth
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container">
|
||||
@if(session('success'))
|
||||
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header bg-success text-white">
|
||||
<strong class="me-auto"><i class="bi bi-check-circle"></i> Éxito</strong>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header bg-danger text-white">
|
||||
<strong class="me-auto"><i class="bi bi-exclamation-circle"></i> Error</strong>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
|
||||
<script src="{{ asset('js/app.js') }}"></script>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
35
resources/views/layouts/guest.blade.php
Executable file
35
resources/views/layouts/guest.blade.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', 'Nómina Pegaso')</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.auth-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
</style>
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center min-vh-100">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
53
resources/views/months/create.blade.php
Executable file
53
resources/views/months/create.blade.php
Executable file
@@ -0,0 +1,53 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Crear Mes')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Crear Nuevo Mes</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('months.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nombre del Mes</label>
|
||||
<select class="form-select" id="name" name="name" required>
|
||||
<option value="">Selecciona un mes</option>
|
||||
<option value="Enero">Enero</option>
|
||||
<option value="Febrero">Febrero</option>
|
||||
<option value="Marzo">Marzo</option>
|
||||
<option value="Abril">Abril</option>
|
||||
<option value="Mayo">Mayo</option>
|
||||
<option value="Junio">Junio</option>
|
||||
<option value="Julio">Julio</option>
|
||||
<option value="Agosto">Agosto</option>
|
||||
<option value="Septiembre">Septiembre</option>
|
||||
<option value="Octubre">Octubre</option>
|
||||
<option value="Noviembre">Noviembre</option>
|
||||
<option value="Diciembre">Diciembre</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="year" class="form-label">Año</label>
|
||||
<input type="number" class="form-control" id="year" name="year"
|
||||
value="{{ old('year', now()->year) }}" min="2020" max="2100" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">Crear Mes</button>
|
||||
<a href="{{ route('months.index') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
53
resources/views/months/edit.blade.php
Executable file
53
resources/views/months/edit.blade.php
Executable file
@@ -0,0 +1,53 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Editar Mes')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Editar Mes</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('months.update', $month->id) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nombre del Mes</label>
|
||||
<select class="form-select" id="name" name="name" required>
|
||||
@foreach(['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] as $m)
|
||||
<option value="{{ $m }}" {{ $month->name == $m ? 'selected' : '' }}>{{ $m }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="year" class="form-label">Año</label>
|
||||
<input type="number" class="form-control" id="year" name="year"
|
||||
value="{{ old('year', $month->year) }}" min="2020" max="2100" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">Estado</label>
|
||||
<select class="form-select" id="status" name="status" required>
|
||||
<option value="open" {{ $month->status == 'open' ? 'selected' : '' }}>Abierto</option>
|
||||
<option value="closed" {{ $month->status == 'closed' ? 'selected' : '' }}>Cerrado</option>
|
||||
<option value="paid" {{ $month->status == 'paid' ? 'selected' : '' }}>Pagado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Actualizar</button>
|
||||
<a href="{{ route('months.show', $month->id) }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
58
resources/views/months/index.blade.php
Executable file
58
resources/views/months/index.blade.php
Executable file
@@ -0,0 +1,58 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Meses')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Meses de Trabajo</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<a href="{{ route('months.create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Crear Nuevo Mes
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de meses -->
|
||||
<div class="row">
|
||||
@forelse($months as $month)
|
||||
<div class="col-md-4 col-lg-3 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{{ $month->name }} {{ $month->year }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-1">
|
||||
<strong>Estado:</strong>
|
||||
@if($month->status === 'open')
|
||||
<span class="badge bg-success">Abierto</span>
|
||||
@elseif($month->status === 'closed')
|
||||
<span class="badge bg-warning">Cerrado</span>
|
||||
@else
|
||||
<span class="badge bg-info">Pagado</span>
|
||||
@endif
|
||||
</p>
|
||||
<p class="mb-1"><small class="text-muted">Ventas: ${{ number_format($month->dailySales()->sum('user_sales'), 2) }}</small></p>
|
||||
<p class="mb-0"><small class="text-muted">Gastos: ${{ number_format($month->expenses()->sum('amount'), 2) }}</small></p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<a href="{{ route('months.show', $month->id) }}" class="btn btn-sm btn-primary">Ver Detalles</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
No hay meses registrados.
|
||||
<a href="{{ route('months.create') }}">Crea tu primer mes</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $months->links() }}
|
||||
@endsection
|
||||
142
resources/views/months/show.blade.php
Executable file
142
resources/views/months/show.blade.php
Executable file
@@ -0,0 +1,142 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Detalles del Mes')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">
|
||||
{{ $month->name }} {{ $month->year }}
|
||||
@if($month->status === 'open')
|
||||
<span class="badge bg-success">Abierto</span>
|
||||
@elseif($month->status === 'closed')
|
||||
<span class="badge bg-warning">Cerrado</span>
|
||||
@else
|
||||
<span class="badge bg-info">Pagado</span>
|
||||
@endif
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card primary">
|
||||
<div class="card-body">
|
||||
<h6>Ventas Usuario</h6>
|
||||
<h4>${{ number_format($month->dailySales()->sum('user_sales'), 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card success">
|
||||
<div class="card-body">
|
||||
<h6>Ventas Sistema</h6>
|
||||
<h4>${{ number_format($month->dailySales()->sum('system_sales'), 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card warning">
|
||||
<div class="card-body">
|
||||
<h6>Total Gastos</h6>
|
||||
<h4>${{ number_format($month->expenses()->sum('amount'), 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card stat-card danger">
|
||||
<div class="card-body">
|
||||
<h6>Días Registrados</h6>
|
||||
<h4>{{ $month->dailySales()->count() }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<a href="{{ route('sales.create', ['month_id' => $month->id]) }}" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> Agregar Venta
|
||||
</a>
|
||||
<a href="{{ route('expenses.create', ['month_id' => $month->id]) }}" class="btn btn-warning">
|
||||
<i class="bi bi-plus-circle"></i> Agregar Gasto
|
||||
</a>
|
||||
<a href="{{ route('reports.monthly', ['month_id' => $month->id]) }}" class="btn btn-info">
|
||||
<i class="bi bi-graph-up"></i> Ver Reporte
|
||||
</a>
|
||||
|
||||
@if($month->status === 'open')
|
||||
<form method="POST" action="{{ route('months.close', $month->id) }}" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('¿Cerrar este mes?')">
|
||||
<i class="bi bi-lock"></i> Cerrar Mes
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Últimas ventas -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Últimas Ventas</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Venta Usuario</th>
|
||||
<th>Venta Sistema</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($month->dailySales()->latest()->limit(10)->get() as $sale)
|
||||
<tr>
|
||||
<td>{{ $sale->date->format('d/m/Y') }}</td>
|
||||
<td>${{ number_format($sale->user_sales, 2) }}</td>
|
||||
<td>${{ number_format($sale->system_sales, 2) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="text-center">No hay ventas</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Últimos gastos -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Últimos Gastos</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Descripción</th>
|
||||
<th>Monto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($month->expenses()->latest()->limit(10)->get() as $expense)
|
||||
<tr>
|
||||
<td>{{ $expense->date->format('d/m/Y') }}</td>
|
||||
<td>{{ $expense->description }}</td>
|
||||
<td class="text-danger">${{ number_format($expense->amount, 2) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="text-center">No hay gastos</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="{{ route('months.index') }}" class="btn btn-secondary">Volver a Meses</a>
|
||||
</div>
|
||||
@endsection
|
||||
131
resources/views/reports/biweekly.blade.php
Executable file
131
resources/views/reports/biweekly.blade.php
Executable file
@@ -0,0 +1,131 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Reporte Quincenal')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Reporte Quincenal</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<select name="month_id" class="form-select" onchange="this.form.submit()">
|
||||
@foreach($months as $m)
|
||||
<option value="{{ $m->id }}" {{ $month->id == $m->id ? 'selected' : '' }}>
|
||||
{{ $m->name }} {{ $m->year }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<input type="hidden" name="month_id" value="{{ $month->id }}">
|
||||
<select name="biweekly" class="form-select" onchange="this.form.submit()">
|
||||
<option value="1" {{ $biweekly == 1 ? 'selected' : '' }}>1ra Quincena (1-15) - ANTICIPO</option>
|
||||
<option value="2" {{ $biweekly == 2 ? 'selected' : '' }}>2da Quincena (16-31) - LIQUIDACIÓN</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="{{ route('reports.monthly', ['month_id' => $month->id]) }}" class="btn btn-outline-primary">Ver Mensual</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($report)
|
||||
<!-- Resumen Quincenal -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card {{ $report['type'] === 'anticipo' ? 'border-success' : 'border-warning' }}">
|
||||
<div class="card-header {{ $report['type'] === 'anticipo' ? 'bg-success' : 'bg-warning' }} text-white">
|
||||
<h5 class="mb-0">{{ $report['period'] }}</h5>
|
||||
<small>{{ $report['description'] }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
@if($report['type'] === 'anticipo')
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-muted">Mitad Sueldo</h6>
|
||||
<h3>${{ number_format($report['biweekly_salary'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-muted">Comisiones del Mes</h6>
|
||||
<h3 class="text-success">+${{ number_format($report['commission_amount'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-muted">Total ANTICIPO</h6>
|
||||
<h2 class="text-success">${{ number_format($report['total_earning'], 2) }}</h2>
|
||||
</div>
|
||||
@else
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-muted">Mitad Sueldo</h6>
|
||||
<h3>${{ number_format($report['biweekly_salary'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-muted">Gastos Q{{ $biweekly }}</h6>
|
||||
<h3 class="text-danger">-${{ number_format($report['expenses_q2'], 2) }}</h3>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="text-muted">Total LIQUIDACIÓN</h6>
|
||||
<h2 class="{{ $report['total_earning'] >= 0 ? 'text-success' : 'text-danger' }}">${{ number_format($report['total_earning'], 2) }}</h2>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desglose de gastos -->
|
||||
@if(count($expenses) > 0)
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5>Gastos de la Quincena</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Descripción</th>
|
||||
<th>Tipo</th>
|
||||
<th class="text-end">Monto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($expenses as $expense)
|
||||
<tr>
|
||||
<td>{{ $expense->date->format('d/m/Y') }}</td>
|
||||
<td>{{ $expense->description }}</td>
|
||||
<td>
|
||||
@if($expense->expense_type == 'q1')
|
||||
<span class="badge bg-primary">Q1</span>
|
||||
@elseif($expense->expense_type == 'q2')
|
||||
<span class="badge bg-warning">Q2</span>
|
||||
@else
|
||||
<span class="badge bg-info">Mensual (mitad)</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-end text-danger">${{ number_format($expense->expense_type == 'mensual' ? $expense->amount / 2 : $expense->amount, 2) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@else
|
||||
<div class="alert alert-warning">No hay datos para mostrar.</div>
|
||||
@endif
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<a href="{{ route('dashboard') }}" class="btn btn-secondary">← Volver al Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
199
resources/views/reports/monthly.blade.php
Executable file
199
resources/views/reports/monthly.blade.php
Executable file
@@ -0,0 +1,199 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Reporte Mensual')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Reporte Mensual</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<select name="month_id" class="form-select" onchange="this.form.submit()">
|
||||
@foreach($months as $m)
|
||||
<option value="{{ $m->id }}" {{ $month->id == $m->id ? 'selected' : '' }}>
|
||||
{{ $m->name }} {{ $m->year }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<a href="{{ route('reports.biweekly', ['month_id' => $month->id, 'biweekly' => 1]) }}" class="btn btn-outline-primary">Quincenal</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($report)
|
||||
<!-- Resumen General -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">{{ $report['month_name'] }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Ventas del Usuario</h6>
|
||||
<h4>${{ number_format($report['total_user_sales'], 2) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Ventas del Sistema</h6>
|
||||
<h4>${{ number_format($report['total_system_sales'], 2) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Comisión ({{ $report['commission_percentage'] }}%)</h6>
|
||||
<h4 class="text-success">${{ number_format($report['commission_amount'], 2) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="text-muted">Total a Recibir</h6>
|
||||
<h4 class="text-primary">${{ number_format($report['total_earning'], 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detalles -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Desglose</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>Salario Mensual</td>
|
||||
<td class="text-end">${{ number_format($report['monthly_salary'], 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comisión ({{ $report['commission_percentage'] }}%)</td>
|
||||
<td class="text-end text-success">+${{ number_format($report['commission_amount'], 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gastos del Mes</td>
|
||||
<td class="text-end text-danger">-${{ number_format($report['total_expenses'], 2) }}</td>
|
||||
</tr>
|
||||
<tr class="table-light">
|
||||
<td><strong>Total a Recibir</strong></td>
|
||||
<td class="text-end"><strong>${{ number_format($report['total_earning'], 2) }}</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Diferencia de Ventas</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($report['has_difference'])
|
||||
<div class="alert alert-{{ $report['sales_difference'] > 0 ? 'warning' : 'danger' }}">
|
||||
<strong>Diferencia:</strong> ${{ number_format($report['sales_difference'], 2) }}
|
||||
<br>
|
||||
<small>
|
||||
{{ $report['sales_difference'] > 0 ? 'Ventas del usuario mayores que sistema' : 'Ventas del sistema mayores que usuario' }}
|
||||
</small>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-success">
|
||||
<i class="bi bi-check-circle"></i> Ventas conciliadas (sin diferencia)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ventas Diarias -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Ventas Diarias</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th class="text-end">Venta Usuario</th>
|
||||
<th class="text-end">Venta Sistema</th>
|
||||
<th class="text-end">Diferencia</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($dailySales as $sale)
|
||||
<tr>
|
||||
<td>{{ $sale->date->format('d/m/Y') }}</td>
|
||||
<td class="text-end">${{ number_format($sale->user_sales, 2) }}</td>
|
||||
<td class="text-end">${{ number_format($sale->system_sales, 2) }}</td>
|
||||
<td class="text-end">
|
||||
@php $diff = $sale->user_sales - $sale->system_sales; @endphp
|
||||
@if($diff != 0)
|
||||
<span class="text-{{ $diff > 0 ? 'warning' : 'danger' }}">
|
||||
${{ number_format($diff, 2) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-success">-</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="text-center">No hay ventas</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="table-light">
|
||||
<th>Total</th>
|
||||
<th class="text-end">${{ number_format($dailySales->sum('user_sales'), 2) }}</th>
|
||||
<th class="text-end">${{ number_format($dailySales->sum('system_sales'), 2) }}</th>
|
||||
<th class="text-end">${{ number_format($dailySales->sum('user_sales') - $dailySales->sum('system_sales'), 2) }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gastos -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Gastos del Mes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Descripción</th>
|
||||
<th class="text-end">Monto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($expenses as $expense)
|
||||
<tr>
|
||||
<td>{{ $expense->date->format('d/m/Y') }}</td>
|
||||
<td>{{ $expense->description }}</td>
|
||||
<td class="text-end text-danger">${{ number_format($expense->amount, 2) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="text-center">No hay gastos</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="table-light">
|
||||
<th colspan="2">Total Gastos</th>
|
||||
<th class="text-end">${{ number_format($expenses->sum('amount'), 2) }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-warning">No hay datos disponibles para este mes.</div>
|
||||
@endif
|
||||
@endsection
|
||||
109
resources/views/reports/yearly.blade.php
Executable file
109
resources/views/reports/yearly.blade.php
Executable file
@@ -0,0 +1,109 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Reporte Anual')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Reporte Anual</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector de año -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<select name="year" class="form-select" onchange="this.form.submit()">
|
||||
@foreach($years as $y)
|
||||
<option value="{{ $y }}" {{ $year == $y ? 'selected' : '' }}>{{ $y }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($report)
|
||||
<!-- Resumen Anual -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">Resumen del Año {{ $year }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<h6 class="text-muted">Meses</h6>
|
||||
<h4>{{ $report['months_count'] }}</h4>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="text-muted">Ventas Usuario</h6>
|
||||
<h4>${{ number_format($report['total_user_sales'], 0) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="text-muted">Ventas Sistema</h6>
|
||||
<h4>${{ number_format($report['total_system_sales'], 0) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="text-muted">Total Salario</h6>
|
||||
<h4>${{ number_format($report['total_salary'], 2) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="text-muted">Total Comisión</h6>
|
||||
<h4 class="text-success">${{ number_format($report['total_commission'], 2) }}</h4>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<h6 class="text-muted">Total Año</h6>
|
||||
<h4 class="text-primary">${{ number_format($report['total_earning'], 2) }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meses del año -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Detalle por Mes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mes</th>
|
||||
<th class="text-end">Ventas Usuario</th>
|
||||
<th class="text-end">Ventas Sistema</th>
|
||||
<th class="text-end">Gastos</th>
|
||||
<th class="text-end">Comisión</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($months as $month)
|
||||
@php
|
||||
$userSales = $month->dailySales()->sum('user_sales');
|
||||
$systemSales = $month->dailySales()->sum('system_sales');
|
||||
$expenses = $month->expenses()->sum('amount');
|
||||
$commission = ($systemSales * auth()->user()->commission_percentage) / 100;
|
||||
$total = auth()->user()->monthly_salary + $commission - $expenses;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $month->name }}</td>
|
||||
<td class="text-end">${{ number_format($userSales, 2) }}</td>
|
||||
<td class="text-end">${{ number_format($systemSales, 2) }}</td>
|
||||
<td class="text-end text-danger">${{ number_format($expenses, 2) }}</td>
|
||||
<td class="text-end text-success">${{ number_format($commission, 2) }}</td>
|
||||
<td class="text-end"><strong>${{ number_format($total, 2) }}</strong></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center">No hay meses en este año</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-warning">No hay datos disponibles para este año.</div>
|
||||
@endif
|
||||
@endsection
|
||||
54
resources/views/sales/create.blade.php
Executable file
54
resources/views/sales/create.blade.php
Executable file
@@ -0,0 +1,54 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Nueva Venta')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Nueva Venta</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('sales.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="month_id" value="{{ $month->id }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="date" class="form-label">Fecha</label>
|
||||
<input type="date" class="form-control" id="date" name="date"
|
||||
value="{{ old('date', now()->format('Y-m-d')) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="user_sales" class="form-label">Ventas del Usuario</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="user_sales" name="user_sales"
|
||||
value="{{ old('user_sales') }}" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="system_sales" class="form-label">Ventas del Sistema (Opcional)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="system_sales" name="system_sales"
|
||||
value="{{ old('system_sales', 0) }}" step="0.01" min="0">
|
||||
</div>
|
||||
<small class="text-muted">Ventas consolidadas del sistema</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">Guardar Venta</button>
|
||||
<a href="{{ route('sales.index', ['month_id' => $month->id]) }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
53
resources/views/sales/edit.blade.php
Executable file
53
resources/views/sales/edit.blade.php
Executable file
@@ -0,0 +1,53 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Editar Venta')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Editar Venta</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('sales.update', $sale->id) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="date" class="form-label">Fecha</label>
|
||||
<input type="date" class="form-control" id="date" name="date"
|
||||
value="{{ old('date', $sale->date->format('Y-m-d')) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="user_sales" class="form-label">Ventas del Usuario</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="user_sales" name="user_sales"
|
||||
value="{{ old('user_sales', $sale->user_sales) }}" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="system_sales" class="form-label">Ventas del Sistema</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="system_sales" name="system_sales"
|
||||
value="{{ old('system_sales', $sale->system_sales) }}" step="0.01" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Actualizar</button>
|
||||
<a href="{{ route('sales.index', ['month_id' => $sale->month_id]) }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
109
resources/views/sales/index.blade.php
Executable file
109
resources/views/sales/index.blade.php
Executable file
@@ -0,0 +1,109 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Ventas')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Gestión de Ventas</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selector de mes -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<form method="GET" class="d-flex gap-2">
|
||||
<select name="month_id" class="form-select" onchange="this.form.submit()">
|
||||
@foreach($months as $m)
|
||||
<option value="{{ $m->id }}" {{ $month->id == $m->id ? 'selected' : '' }}>
|
||||
{{ $m->name }} {{ $m->year }} ({{ $m->status }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<a href="{{ route('sales.create', ['month_id' => $month->id]) }}" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle"></i> Nueva Venta
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="btn-group">
|
||||
<a href="{{ route('sales.index', ['month_id' => $month->id]) }}" class="btn btn-outline-secondary">Todas</a>
|
||||
<a href="{{ route('expenses.index', ['month_id' => $month->id]) }}" class="btn btn-outline-warning">Ver Gastos</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Ventas Usuario</h6>
|
||||
<h3>${{ number_format($month->dailySales()->sum('user_sales'), 2) }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">Total Ventas Sistema</h6>
|
||||
<h3>${{ number_format($month->dailySales()->sum('system_sales'), 2) }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de ventas -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Venta Usuario</th>
|
||||
<th>Venta Sistema</th>
|
||||
<th>Diferencia</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($sales as $sale)
|
||||
<tr>
|
||||
<td>{{ $sale->date->format('d/m/Y') }}</td>
|
||||
<td>${{ number_format($sale->user_sales, 2) }}</td>
|
||||
<td>${{ number_format($sale->system_sales, 2) }}</td>
|
||||
<td>
|
||||
@php $diff = $sale->user_sales - $sale->system_sales; @endphp
|
||||
@if($diff != 0)
|
||||
<span class="badge bg-{{ $diff > 0 ? 'warning' : 'danger' }}">
|
||||
${{ number_format($diff, 2) }}
|
||||
</span>
|
||||
@else
|
||||
<span class="badge bg-success">conciliada</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('sales.edit', $sale->id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('sales.destroy', $sale->id) }}" class="d-inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('¿Eliminar esta venta?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">No hay ventas registradas.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ $sales->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
257
resources/views/settings/index.blade.php
Executable file
257
resources/views/settings/index.blade.php
Executable file
@@ -0,0 +1,257 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Configuración')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">
|
||||
<i class="bi bi-gear text-primary"></i> Configuración
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Datos Laborales -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-briefcase"></i> Datos Laborales
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('settings.update') }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="fecha_ingreso" class="form-label">Fecha de Ingreso</label>
|
||||
<input type="date" class="form-control" id="fecha_ingreso"
|
||||
name="fecha_ingreso"
|
||||
value="{{ old('fecha_ingreso', auth()->user()->fecha_ingreso?->format('Y-m-d')) }}">
|
||||
<small class="text-muted">Fecha en que started a trabajar en la empresa</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="razon_social" class="form-label">Razón Social</label>
|
||||
<input type="text" class="form-control" id="razon_social"
|
||||
name="razon_social"
|
||||
value="{{ old('razon_social', auth()->user()->razon_social) }}"
|
||||
placeholder="Empresa donde trabajas">
|
||||
<small class="text-muted">Nombre de la empresa o negocio</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="sueldo_integro_diario" class="form-label">Sueldo Íntegro Diario</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="sueldo_integro_diario"
|
||||
name="sueldo_integro_diario"
|
||||
value="{{ old('sueldo_integro_diario', auth()->user()->sueldo_integro_diario) }}"
|
||||
min="0" step="0.01">
|
||||
</div>
|
||||
<small class="text-muted">Salario diario integrado (para calcular vacaciones)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php($user = auth()->user())
|
||||
@if($user->fecha_ingreso)
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-calendar-check"></i>
|
||||
<strong>Antigüedad:</strong> {{ $user->fecha_ingreso->diffInYears(now()) }} año(s) y {{ $user->fecha_ingreso->diffInMonths(now()) % 12 }} mes(es)
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-save"></i> Guardar Datos Laborales
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuración de Comisión -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-percent"></i> Configuración de Comisión
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('settings.update') }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="commission_percentage" class="form-label">Porcentaje de Comisión (%)</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="commission_percentage"
|
||||
name="commission_percentage"
|
||||
value="{{ old('commission_percentage', auth()->user()->commission_percentage) }}"
|
||||
min="0" max="100" step="0.01" required>
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<small class="text-muted">Porcentaje que recibirás sobre tus ventas</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="monthly_salary" class="form-label">Salario Mensual Base</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" class="form-control" id="monthly_salary"
|
||||
name="monthly_salary"
|
||||
value="{{ old('monthly_salary', auth()->user()->monthly_salary) }}"
|
||||
min="0" step="0.01" required>
|
||||
</div>
|
||||
<small class="text-muted">Salario base mensual sin comisiones</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Nota:</strong> Estos valores se aplicarán a partir del próximo mes.
|
||||
Los meses actuales mantendrán su configuración original.
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Guardar Cambios
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cambiar Contraseña -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-key"></i> Cambiar Contraseña
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('settings.update') }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="current_password" class="form-label">Contraseña Actual</label>
|
||||
<input type="password" class="form-control" id="current_password"
|
||||
name="current_password">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Nueva Contraseña</label>
|
||||
<input type="password" class="form-control" id="password"
|
||||
name="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="password_confirmation" class="form-label">Confirmar Contraseña</label>
|
||||
<input type="password" class="form-control" id="password_confirmation"
|
||||
name="password_confirmation">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="text-muted">Deja los campos de contraseña en blanco si no deseas cambiarlo.</small>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
<i class="bi bi-key-fill"></i> Actualizar Contraseña
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Información del Usuario -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-person"></i> Información del Usuario
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4">Nombre:</dt>
|
||||
<dd class="col-sm-8">{{ auth()->user()->name }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Email:</dt>
|
||||
<dd class="col-sm-8">{{ auth()->user()->email }}</dd>
|
||||
|
||||
@if(auth()->user()->razon_social)
|
||||
<dt class="col-sm-4">Empresa:</dt>
|
||||
<dd class="col-sm-8">{{ auth()->user()->razon_social }}</dd>
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->fecha_ingreso)
|
||||
<dt class="col-sm-4">Ingreso:</dt>
|
||||
<dd class="col-sm-8">{{ auth()->user()->fecha_ingreso->format('d/m/Y') }}</dd>
|
||||
@endif
|
||||
|
||||
<dt class="col-sm-4">Comisión:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-primary">{{ auth()->user()->commission_percentage }}%</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">Salario:</dt>
|
||||
<dd class="col-sm-8">${{ number_format(auth()->user()->monthly_salary, 2) }}</dd>
|
||||
|
||||
<dt class="col-sm-4">Estado:</dt>
|
||||
<dd class="col-sm-8">
|
||||
@if(auth()->user()->is_active)
|
||||
<span class="badge bg-success">Activo</span>
|
||||
@else
|
||||
<span class="badge bg-danger">Inactivo</span>
|
||||
@endif
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ayuda -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-question-circle"></i> Ayuda
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>¿Cómo funciona la comisión?</h6>
|
||||
<p class="text-muted small">
|
||||
Tu salario se calcula sumando el salary base más el {{ auth()->user()->commission_percentage }}%
|
||||
de tus ventas del mes.
|
||||
</p>
|
||||
|
||||
<h6 class="mt-3">¿Qué son las ventas del sistema?</h6>
|
||||
<p class="text-muted small">
|
||||
Las ventas del sistema son las ventas consolidadas automáticamente.
|
||||
Si difieren de tus ventas, aparecerán en el reporte.
|
||||
</p>
|
||||
|
||||
<h6 class="mt-3">¿Puedo cambiar estos valores?</h6>
|
||||
<p class="text-muted small">
|
||||
Sí, pero los cambios se aplicarán al siguiente mes.
|
||||
Los meses actuales mantendrán su configuración.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
78
resources/views/telegram/verify.blade.php
Executable file
78
resources/views/telegram/verify.blade.php
Executable file
@@ -0,0 +1,78 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Vincular Telegram')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-4">Vincular Cuenta de Telegram</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if($telegramAccount->is_verified)
|
||||
<div class="alert alert-success">
|
||||
<h5><i class="bi bi-check-circle"></i> Cuenta Verificada</h5>
|
||||
<p>Tu cuenta de Telegram está vinculada correctamente.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<strong>Chat ID:</strong> {{ $telegramAccount->chat_id }}
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('telegram.unlink') }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('¿Desvincular cuenta de Telegram?')">
|
||||
<i class="bi bi-unlink"></i> Desvincular Cuenta
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="bi bi-info-circle"></i> Vinculación de Telegram</h5>
|
||||
<p>Sigue estos pasos para vincular tu cuenta de Telegram:</p>
|
||||
<ol>
|
||||
<li>Abre Telegram y busca el bot de Nómina Pegaso</li>
|
||||
<li>Envía el código de verificación que aparece abajo</li>
|
||||
<li>Recibirás una confirmación cuando esté vinculado</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-muted">Tu Código de Verificación</h6>
|
||||
<h1 class="display-4 text-primary">{{ $telegramAccount->verification_code }}</h1>
|
||||
<small class="text-muted">Este código expira cuando se genera uno nuevo</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('telegram.regenerate') }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-clockwise"></i> Regenerar Código
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Información</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">Al vincular tu cuenta de Telegram podrás:</p>
|
||||
<ul class="small">
|
||||
<li>Recibir notificaciones de tus ventas</li>
|
||||
<li>Consultar tu estado de comisiones</li>
|
||||
<li>Recibir recordatorios de registro</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
225
resources/views/welcome.blade.php
Executable file
225
resources/views/welcome.blade.php
Executable file
File diff suppressed because one or more lines are too long
8
routes/console.php
Executable file
8
routes/console.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
21
routes/test.php
Normal file
21
routes/test.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Models\User;
|
||||
|
||||
Route::post('/test-save', function (Request $request) {
|
||||
$user = User::find(1);
|
||||
$user->fecha_ingreso = $request->fecha_ingreso;
|
||||
$user->razon_social = $request->razon_social;
|
||||
$user->save();
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Saved!',
|
||||
'data' => [
|
||||
'fecha_ingreso' => $user->fecha_ingreso,
|
||||
'razon_social' => $user->razon_social
|
||||
]
|
||||
]);
|
||||
});
|
||||
71
routes/web.php
Executable file
71
routes/web.php
Executable file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\CalendarController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\ExpenseController;
|
||||
use App\Http\Controllers\MonthController;
|
||||
use App\Http\Controllers\ReportController;
|
||||
use App\Http\Controllers\SaleController;
|
||||
use App\Http\Controllers\SettingsController;
|
||||
use App\Http\Controllers\TelegramController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Rutas públicas - Auth
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::get('/register', [AuthController::class, 'showRegisterForm'])->name('register');
|
||||
Route::post('/register', [AuthController::class, 'register']);
|
||||
});
|
||||
|
||||
// Logout (accessible to authenticated users)
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||
|
||||
// Rutas protegidas
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
// Dashboard
|
||||
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
// Calendario
|
||||
Route::get('/calendar', [CalendarController::class, 'index'])->name('calendar');
|
||||
Route::post('/calendar/day', [CalendarController::class, 'storeDay'])->name('calendar.day.store');
|
||||
|
||||
// Meses
|
||||
Route::resource('months', MonthController::class);
|
||||
Route::post('/months/{month}/close', [MonthController::class, 'close'])->name('months.close');
|
||||
|
||||
// Ventas
|
||||
Route::get('/sales', [SaleController::class, 'index'])->name('sales.index');
|
||||
Route::get('/sales/create', [SaleController::class, 'create'])->name('sales.create');
|
||||
Route::post('/sales', [SaleController::class, 'store'])->name('sales.store');
|
||||
Route::get('/sales/{sale}/edit', [SaleController::class, 'edit'])->name('sales.edit');
|
||||
Route::put('/sales/{sale}', [SaleController::class, 'update'])->name('sales.update');
|
||||
Route::delete('/sales/{sale}', [SaleController::class, 'destroy'])->name('sales.destroy');
|
||||
|
||||
// Gastos
|
||||
Route::get('/expenses', [ExpenseController::class, 'index'])->name('expenses.index');
|
||||
Route::get('/expenses/create', [ExpenseController::class, 'create'])->name('expenses.create');
|
||||
Route::post('/expenses', [ExpenseController::class, 'store'])->name('expenses.store');
|
||||
Route::get('/expenses/{expense}/edit', [ExpenseController::class, 'edit'])->name('expenses.edit');
|
||||
Route::put('/expenses/{expense}', [ExpenseController::class, 'update'])->name('expenses.update');
|
||||
Route::delete('/expenses/{expense}', [ExpenseController::class, 'destroy'])->name('expenses.destroy');
|
||||
|
||||
// Reportes
|
||||
Route::get('/reports/monthly', [ReportController::class, 'monthly'])->name('reports.monthly');
|
||||
Route::get('/reports/biweekly', [ReportController::class, 'biweekly'])->name('reports.biweekly');
|
||||
Route::get('/reports/yearly', [ReportController::class, 'yearly'])->name('reports.yearly');
|
||||
|
||||
// Telegram
|
||||
Route::get('/telegram/verify', [TelegramController::class, 'showVerifyPage'])->name('telegram.verify');
|
||||
Route::post('/telegram/regenerate', [TelegramController::class, 'regenerateCode'])->name('telegram.regenerate');
|
||||
Route::post('/telegram/unlink', [TelegramController::class, 'unlink'])->name('telegram.unlink');
|
||||
|
||||
// Settings
|
||||
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
||||
Route::put('/settings', [SettingsController::class, 'update'])->name('settings.update');
|
||||
});
|
||||
|
||||
// Webhook de Telegram (público, sin auth)
|
||||
Route::post('/telegram/webhook', [TelegramController::class, 'webhook'])->name('telegram.webhook');
|
||||
Route::get('/telegram/setup', [TelegramController::class, 'setupWebhook'])->name('telegram.setup');
|
||||
4
storage/app/.gitignore
vendored
Executable file
4
storage/app/.gitignore
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
2
storage/app/private/.gitignore
vendored
Executable file
2
storage/app/private/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/app/public/.gitignore
vendored
Executable file
2
storage/app/public/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
9
storage/framework/.gitignore
vendored
Executable file
9
storage/framework/.gitignore
vendored
Executable file
@@ -0,0 +1,9 @@
|
||||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
3
storage/framework/cache/.gitignore
vendored
Executable file
3
storage/framework/cache/.gitignore
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
2
storage/framework/cache/data/.gitignore
vendored
Executable file
2
storage/framework/cache/data/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/framework/sessions/.gitignore
vendored
Executable file
2
storage/framework/sessions/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/framework/testing/.gitignore
vendored
Executable file
2
storage/framework/testing/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/framework/views/.gitignore
vendored
Executable file
2
storage/framework/views/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/logs/.gitignore
vendored
Executable file
2
storage/logs/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
19
tests/Feature/ExampleTest.php
Executable file
19
tests/Feature/ExampleTest.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user