mep
This commit is contained in:
parent
e958163a4a
commit
b8a5a1ff58
79 changed files with 15113 additions and 0 deletions
41
src/Controller/DashboardController.php
Normal file
41
src/Controller/DashboardController.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Service\ConfigurationService;
|
||||
use App\Service\DrinkTypeService;
|
||||
use App\Service\InventoryService;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Twig $view,
|
||||
private readonly InventoryService $inventoryService,
|
||||
private readonly DrinkTypeService $drinkTypeService,
|
||||
private readonly ConfigurationService $configService
|
||||
) {}
|
||||
|
||||
public function index(Response $response): Response
|
||||
{
|
||||
// Get low stock items for the low-stock-alerts component
|
||||
$lowStockItems = $this->inventoryService->getLowStockDrinkTypes();
|
||||
|
||||
// Get all drink types for the quick-update-form component
|
||||
$drinkTypes = $this->drinkTypeService->getAllDrinkTypes();
|
||||
|
||||
// Get configuration settings
|
||||
$showLowStockAlerts = (bool) $this->configService->getConfigValue('show_low_stock_alerts', '1');
|
||||
$showQuickUpdateForm = (bool) $this->configService->getConfigValue('show_quick_update_form', '1');
|
||||
|
||||
return $this->view->render($response, 'dashboard/index.twig', [
|
||||
'lowStockItems' => $lowStockItems,
|
||||
'drinkTypes' => $drinkTypes,
|
||||
'showLowStockAlerts' => $showLowStockAlerts,
|
||||
'showQuickUpdateForm' => $showQuickUpdateForm
|
||||
]);
|
||||
}
|
||||
}
|
151
src/Controller/DrinkTypeController.php
Normal file
151
src/Controller/DrinkTypeController.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use App\Service\DrinkTypeService;
|
||||
use App\Service\InventoryService;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class DrinkTypeController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DrinkTypeService $drinkTypeService,
|
||||
private readonly InventoryService $inventoryService,
|
||||
private readonly Twig $view
|
||||
) {}
|
||||
|
||||
public function index(Response $response): Response
|
||||
{
|
||||
$drinkTypes = $this->drinkTypeService->getAllDrinkTypes();
|
||||
|
||||
// Get current stock levels for all drink types
|
||||
$drinkTypesWithStock = [];
|
||||
foreach ($drinkTypes as $drinkType) {
|
||||
$currentStock = $this->inventoryService->getCurrentStockLevel($drinkType);
|
||||
$drinkTypesWithStock[] = [
|
||||
'id' => $drinkType->getId(),
|
||||
'name' => $drinkType->getName(),
|
||||
'description' => $drinkType->getDescription(),
|
||||
'desiredStock' => $drinkType->getDesiredStock(),
|
||||
'currentStock' => $currentStock,
|
||||
'createdAt' => $drinkType->getCreatedAt(),
|
||||
'updatedAt' => $drinkType->getUpdatedAt(),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->view->render($response, 'drink-types/index.twig', [
|
||||
'drinkTypes' => $drinkTypesWithStock,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Response $response, int $id): Response
|
||||
{
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($id);
|
||||
|
||||
if (!$drinkType instanceof DrinkType) {
|
||||
// Redirect to drink types list with error message
|
||||
// In a real application, you might want to use flash messages
|
||||
return $response->withHeader('Location', '/drink-types')->withStatus(302);
|
||||
}
|
||||
|
||||
// Get current stock information for the drink type
|
||||
$currentStock = $this->inventoryService->getCurrentStockLevel($drinkType);
|
||||
|
||||
// Get inventory records for the drink type
|
||||
$inventoryRecords = $this->inventoryService->getInventoryRecordsByDrinkType($drinkType);
|
||||
|
||||
return $this->view->render($response, 'drink-types/show.twig', [
|
||||
'drinkType' => $drinkType,
|
||||
'currentStock' => $currentStock,
|
||||
'inventoryRecords' => $inventoryRecords,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Response $response): Response
|
||||
{
|
||||
return $this->view->render($response, 'drink-types/create.twig');
|
||||
}
|
||||
|
||||
public function store(Request $request, Response $response): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$name = $data['name'] ?? '';
|
||||
$description = $data['description'] ?? null;
|
||||
$desiredStock = isset($data['desired_stock']) ? (int) $data['desired_stock'] : 0;
|
||||
|
||||
try {
|
||||
$this->drinkTypeService->createDrinkType($name, $description, $desiredStock);
|
||||
|
||||
// Redirect to drink types list
|
||||
return $response->withHeader('Location', '/drink-types')->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the form again with error message
|
||||
return $this->view->render($response, 'drink-types/create.twig', [
|
||||
'error' => $e->getMessage(),
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($id);
|
||||
|
||||
if (!$drinkType instanceof DrinkType) {
|
||||
// Redirect to drink types list
|
||||
return $response->withHeader('Location', '/drink-types')->withStatus(302);
|
||||
}
|
||||
|
||||
return $this->view->render($response, 'drink-types/edit.twig', [
|
||||
'drinkType' => $drinkType,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($id);
|
||||
|
||||
if (!$drinkType instanceof DrinkType) {
|
||||
// Redirect to drink types list
|
||||
return $response->withHeader('Location', '/drink-types')->withStatus(302);
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$name = $data['name'] ?? null;
|
||||
$description = $data['description'] ?? null;
|
||||
$desiredStock = isset($data['desired_stock']) ? (int) $data['desired_stock'] : null;
|
||||
|
||||
try {
|
||||
$this->drinkTypeService->updateDrinkType($drinkType, $name, $description, $desiredStock);
|
||||
|
||||
// Redirect to drink type details
|
||||
return $response->withHeader('Location', '/drink-types/' . $id)->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the form again with error message
|
||||
return $this->view->render($response, 'drink-types/edit.twig', [
|
||||
'drinkType' => $drinkType,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($id);
|
||||
|
||||
if ($drinkType instanceof DrinkType) {
|
||||
$this->drinkTypeService->deleteDrinkType($drinkType);
|
||||
}
|
||||
|
||||
// Redirect to drink types list
|
||||
return $response->withHeader('Location', '/drink-types')->withStatus(302);
|
||||
}
|
||||
}
|
96
src/Controller/InventoryController.php
Normal file
96
src/Controller/InventoryController.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use App\Service\DrinkTypeService;
|
||||
use App\Service\InventoryService;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class InventoryController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InventoryService $inventoryService,
|
||||
private readonly DrinkTypeService $drinkTypeService,
|
||||
private readonly Twig $view
|
||||
) {}
|
||||
|
||||
public function index(Response $response): Response
|
||||
{
|
||||
$stockLevels = $this->inventoryService->getAllDrinkTypesWithStockLevels();
|
||||
|
||||
return $this->view->render($response, 'inventory/index.twig', [
|
||||
'stockLevels' => $stockLevels,
|
||||
]);
|
||||
}
|
||||
|
||||
public function showUpdateForm(Response $response): Response
|
||||
{
|
||||
$drinkTypes = $this->drinkTypeService->getAllDrinkTypes();
|
||||
|
||||
return $this->view->render($response, 'inventory/update.twig', [
|
||||
'drinkTypes' => $drinkTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Response $response): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$drinkTypeId = isset($data['drink_type_id']) ? (int) $data['drink_type_id'] : 0;
|
||||
$quantity = isset($data['quantity']) ? (int) $data['quantity'] : 0;
|
||||
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($drinkTypeId);
|
||||
|
||||
if (!$drinkType instanceof DrinkType) {
|
||||
// Redirect to inventory page with error message
|
||||
return $response->withHeader('Location', '/inventory')->withStatus(302);
|
||||
}
|
||||
|
||||
$this->inventoryService->updateStockLevel($drinkType, $quantity);
|
||||
|
||||
// Redirect to inventory page
|
||||
return $response->withHeader('Location', '/inventory')->withStatus(302);
|
||||
}
|
||||
|
||||
public function history(Response $response, ?int $id = null): Response
|
||||
{
|
||||
$drinkTypeId = $id;
|
||||
|
||||
if ($drinkTypeId !== null && $drinkTypeId !== 0) {
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($drinkTypeId);
|
||||
|
||||
if (!$drinkType instanceof DrinkType) {
|
||||
// Redirect to inventory page
|
||||
return $response->withHeader('Location', '/inventory')->withStatus(302);
|
||||
}
|
||||
|
||||
$inventoryRecords = $this->inventoryService->getInventoryRecordsByDrinkType($drinkType);
|
||||
|
||||
return $this->view->render($response, 'inventory/history.twig', [
|
||||
'drinkType' => $drinkType,
|
||||
'inventoryRecords' => $inventoryRecords,
|
||||
]);
|
||||
}
|
||||
|
||||
// If no drink type ID is provided, show history for all drink types
|
||||
$inventoryRecords = $this->inventoryService->getAllInventoryRecords();
|
||||
|
||||
return $this->view->render($response, 'inventory/history.twig', [
|
||||
'inventoryRecords' => $inventoryRecords,
|
||||
]);
|
||||
}
|
||||
|
||||
public function lowStock(Response $response): Response
|
||||
{
|
||||
$lowStockItems = $this->inventoryService->getLowStockDrinkTypes();
|
||||
|
||||
return $this->view->render($response, 'inventory/low-stock.twig', [
|
||||
'lowStockItems' => $lowStockItems,
|
||||
]);
|
||||
}
|
||||
}
|
223
src/Controller/OrderController.php
Normal file
223
src/Controller/OrderController.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use App\Entity\Order;
|
||||
use App\Service\DrinkTypeService;
|
||||
use App\Service\OrderService;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class OrderController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderService $orderService,
|
||||
private readonly DrinkTypeService $drinkTypeService,
|
||||
private readonly Twig $view
|
||||
) {}
|
||||
|
||||
public function index(Response $response): Response
|
||||
{
|
||||
$orders = $this->orderService->getAllOrders();
|
||||
|
||||
return $this->view->render($response, 'orders/index.twig', [
|
||||
'orders' => $orders,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Response $response, int $id): Response
|
||||
{
|
||||
$order = $this->orderService->getOrderById($id);
|
||||
|
||||
if (!$order instanceof Order) {
|
||||
// Redirect to orders list
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
}
|
||||
|
||||
return $this->view->render($response, 'orders/show.twig', [
|
||||
'order' => $order,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Response $response): Response
|
||||
{
|
||||
$drinkTypes = $this->drinkTypeService->getAllDrinkTypes();
|
||||
|
||||
return $this->view->render($response, 'orders/create.twig', [
|
||||
'drinkTypes' => $drinkTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, Response $response): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$items = [];
|
||||
|
||||
// Process form data to create order items
|
||||
foreach ($data['items'] ?? [] as $item) {
|
||||
if (!empty($item['drink_type_id']) && !empty($item['quantity'])) {
|
||||
$items[] = [
|
||||
'drinkTypeId' => (int) $item['drink_type_id'],
|
||||
'quantity' => (int) $item['quantity'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$order = $this->orderService->createOrder($items);
|
||||
|
||||
// Redirect to order details
|
||||
return $response->withHeader('Location', '/orders/' . $order->getId())->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the form again with error message
|
||||
return $this->view->render($response, 'orders/create.twig', [
|
||||
'error' => $e->getMessage(),
|
||||
'drinkTypes' => $this->drinkTypeService->getAllDrinkTypes(),
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function createFromStock(Response $response): Response
|
||||
{
|
||||
try {
|
||||
$order = $this->orderService->createOrderFromStockLevels();
|
||||
|
||||
// Redirect to order details
|
||||
return $response->withHeader('Location', '/orders/' . $order->getId())->withStatus(302);
|
||||
} catch (InvalidArgumentException) {
|
||||
// Redirect to orders list with error message
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$order = $this->orderService->getOrderById($id);
|
||||
|
||||
if (!$order instanceof Order) {
|
||||
// Redirect to orders list
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
$status = $data['status'] ?? '';
|
||||
|
||||
try {
|
||||
$this->orderService->updateOrderStatus($order, $status);
|
||||
|
||||
// Redirect to order details
|
||||
return $response->withHeader('Location', '/orders/' . $id)->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the order details with error message
|
||||
return $this->view->render($response, 'orders/show.twig', [
|
||||
'order' => $order,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function addItem(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$order = $this->orderService->getOrderById($id);
|
||||
|
||||
if (!$order instanceof Order) {
|
||||
// Redirect to orders list
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
$drinkTypeId = isset($data['drink_type_id']) ? (int) $data['drink_type_id'] : 0;
|
||||
$quantity = isset($data['quantity']) ? (int) $data['quantity'] : 0;
|
||||
|
||||
$drinkType = $this->drinkTypeService->getDrinkTypeById($drinkTypeId);
|
||||
|
||||
if (!$drinkType instanceof DrinkType) {
|
||||
// Render the order details with error message
|
||||
return $this->view->render($response, 'orders/show.twig', [
|
||||
'order' => $order,
|
||||
'error' => 'Invalid drink type',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->orderService->addOrderItem($order, $drinkType, $quantity);
|
||||
|
||||
// Redirect to order details
|
||||
return $response->withHeader('Location', '/orders/' . $id)->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the order details with error message
|
||||
return $this->view->render($response, 'orders/show.twig', [
|
||||
'order' => $order,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeItem(Response $response, int $id, int $itemId): Response
|
||||
{
|
||||
$orderId = $id;
|
||||
|
||||
$order = $this->orderService->getOrderById($orderId);
|
||||
|
||||
if (!$order instanceof Order) {
|
||||
// Redirect to orders list
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
}
|
||||
|
||||
$orderItem = null;
|
||||
foreach ($order->getOrderItems() as $item) {
|
||||
if ($item->getId() === $itemId) {
|
||||
$orderItem = $item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$orderItem) {
|
||||
// Redirect to order details
|
||||
return $response->withHeader('Location', '/orders/' . $orderId)->withStatus(302);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->orderService->removeOrderItem($order, $orderItem);
|
||||
|
||||
// Redirect to order details
|
||||
return $response->withHeader('Location', '/orders/' . $orderId)->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the order details with error message
|
||||
return $this->view->render($response, 'orders/show.twig', [
|
||||
'order' => $order,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Response $response, int $id): Response
|
||||
{
|
||||
$order = $this->orderService->getOrderById($id);
|
||||
|
||||
if (!$order instanceof Order) {
|
||||
// Redirect to orders list
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->orderService->deleteOrder($order);
|
||||
|
||||
// Redirect to orders list
|
||||
return $response->withHeader('Location', '/orders')->withStatus(302);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// Render the order details with error message
|
||||
return $this->view->render($response, 'orders/show.twig', [
|
||||
'order' => $order,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
74
src/Controller/SettingsController.php
Normal file
74
src/Controller/SettingsController.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\SystemConfig;
|
||||
use App\Service\ConfigurationService;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
|
||||
class SettingsController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConfigurationService $configurationService,
|
||||
private readonly Twig $view
|
||||
) {}
|
||||
|
||||
public function index(Response $response): Response
|
||||
{
|
||||
|
||||
$settings = [];
|
||||
foreach($this->configurationService->getAllConfigs() as $setting) {
|
||||
$settings[$setting->getKey()] = $setting->getValue();
|
||||
}
|
||||
return $this->view->render($response, 'settings/index.twig', [
|
||||
'configs' => $settings,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Response $response): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
// Process each configuration value from the form
|
||||
foreach ($data['configs'] ?? [] as $key => $value) {
|
||||
try {
|
||||
// Get existing config or create a new one
|
||||
$config = $this->configurationService->getConfigByKey($key);
|
||||
|
||||
if ($config instanceof SystemConfig) {
|
||||
$this->configurationService->updateConfig($config, null, $value);
|
||||
} else {
|
||||
$this->configurationService->createConfig($key, $value);
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// If there's an error, render the form again with error message
|
||||
return $this->view->render($response, 'settings/index.twig', [
|
||||
'configs' => $this->configurationService->getAllConfigs(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to settings page with success message
|
||||
return $response->withHeader('Location', '/settings')->withStatus(302);
|
||||
}
|
||||
|
||||
public function resetToDefaults(Response $response): Response
|
||||
{
|
||||
// Delete all existing configurations
|
||||
foreach ($this->configurationService->getAllConfigs() as $config) {
|
||||
$this->configurationService->deleteConfig($config);
|
||||
}
|
||||
|
||||
// Initialize default configurations
|
||||
$this->configurationService->initializeDefaultConfigs();
|
||||
|
||||
// Redirect back to settings page
|
||||
return $response->withHeader('Location', '/settings')->withStatus(302);
|
||||
}
|
||||
}
|
115
src/Entity/DrinkType.php
Normal file
115
src/Entity/DrinkType.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use DateTimeImmutable;
|
||||
use App\Repository\DrinkTypeRepository;
|
||||
|
||||
#[ORM\Entity(repositoryClass: DrinkTypeRepository::class)]
|
||||
#[ORM\Table(name: 'drink_type')]
|
||||
class DrinkType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private readonly DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'drinkType', targetEntity: InventoryRecord::class)]
|
||||
private Collection $inventoryRecords;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'drinkType', targetEntity: OrderItem::class)]
|
||||
private Collection $orderItems;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: 'string', length: 255, unique: true)]
|
||||
private string $name,
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $description = null,
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $desiredStock = 0,
|
||||
?DateTimeImmutable $createdAt = null,
|
||||
?DateTimeImmutable $updatedAt = null
|
||||
) {
|
||||
$this->createdAt = $createdAt ?? new DateTimeImmutable();
|
||||
$this->updatedAt = $updatedAt ?? new DateTimeImmutable();
|
||||
$this->inventoryRecords = new ArrayCollection();
|
||||
$this->orderItems = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDesiredStock(): int
|
||||
{
|
||||
return $this->desiredStock;
|
||||
}
|
||||
|
||||
public function setDesiredStock(int $desiredStock): self
|
||||
{
|
||||
$this->desiredStock = $desiredStock;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getInventoryRecords(): Collection
|
||||
{
|
||||
return $this->inventoryRecords;
|
||||
}
|
||||
|
||||
public function getOrderItems(): Collection
|
||||
{
|
||||
return $this->orderItems;
|
||||
}
|
||||
|
||||
private function updateTimestamp(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
99
src/Entity/InventoryRecord.php
Normal file
99
src/Entity/InventoryRecord.php
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use DateTimeImmutable;
|
||||
use App\Repository\InventoryRecordRepository;
|
||||
|
||||
#[ORM\Entity(repositoryClass: InventoryRecordRepository::class)]
|
||||
#[ORM\Table(name: 'inventory_record')]
|
||||
class InventoryRecord
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $timestamp;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private readonly DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: DrinkType::class, inversedBy: 'inventoryRecords')]
|
||||
#[ORM\JoinColumn(name: 'drink_type_id', referencedColumnName: 'id', nullable: false)]
|
||||
private DrinkType $drinkType,
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $quantity,
|
||||
?DateTimeImmutable $timestamp = null,
|
||||
?DateTimeImmutable $createdAt = null,
|
||||
?DateTimeImmutable $updatedAt = null
|
||||
) {
|
||||
$this->timestamp = $timestamp ?? new DateTimeImmutable();
|
||||
$this->createdAt = $createdAt ?? new DateTimeImmutable();
|
||||
$this->updatedAt = $updatedAt ?? new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getDrinkType(): DrinkType
|
||||
{
|
||||
return $this->drinkType;
|
||||
}
|
||||
|
||||
public function setDrinkType(DrinkType $drinkType): self
|
||||
{
|
||||
$this->drinkType = $drinkType;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuantity(): int
|
||||
{
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function setQuantity(int $quantity): self
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimestamp(): DateTimeImmutable
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function setTimestamp(DateTimeImmutable $timestamp): self
|
||||
{
|
||||
$this->timestamp = $timestamp;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function updateTimestamp(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
111
src/Entity/Order.php
Normal file
111
src/Entity/Order.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use DateTimeImmutable;
|
||||
use App\Repository\OrderRepository;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OrderRepository::class)]
|
||||
#[ORM\Table(name: '`order`')] // 'order' is a reserved keyword in SQL, so we escape it
|
||||
class Order
|
||||
{
|
||||
public const STATUS_NEW = 'new';
|
||||
public const STATUS_IN_WORK = 'in_work';
|
||||
public const STATUS_ORDERED = 'ordered';
|
||||
public const STATUS_FULFILLED = 'fulfilled';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private readonly DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'order', targetEntity: OrderItem::class, cascade: ['persist', 'remove'])]
|
||||
private Collection $orderItems;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $status = self::STATUS_NEW,
|
||||
?DateTimeImmutable $createdAt = null,
|
||||
?DateTimeImmutable $updatedAt = null
|
||||
) {
|
||||
$this->createdAt = $createdAt ?? new DateTimeImmutable();
|
||||
$this->updatedAt = $updatedAt ?? new DateTimeImmutable();
|
||||
$this->orderItems = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(string $status): self
|
||||
{
|
||||
if (!in_array($status, [self::STATUS_NEW, self::STATUS_IN_WORK, self::STATUS_ORDERED, self::STATUS_FULFILLED], true)) {
|
||||
throw new InvalidArgumentException('Invalid status');
|
||||
}
|
||||
|
||||
$this->status = $status;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, OrderItem>
|
||||
*/
|
||||
public function getOrderItems(): Collection
|
||||
{
|
||||
return $this->orderItems;
|
||||
}
|
||||
|
||||
public function addOrderItem(OrderItem $orderItem): self
|
||||
{
|
||||
if (!$this->orderItems->contains($orderItem)) {
|
||||
$this->orderItems[] = $orderItem;
|
||||
$orderItem->setOrder($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeOrderItem(OrderItem $orderItem): self
|
||||
{
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($this->orderItems->removeElement($orderItem) && $orderItem->getOrder() === $this) {
|
||||
$orderItem->setOrder(null);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function updateTimestamp(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
114
src/Entity/OrderItem.php
Normal file
114
src/Entity/OrderItem.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use DateTimeImmutable;
|
||||
use App\Repository\OrderItemRepository;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OrderItemRepository::class)]
|
||||
#[ORM\Table(name: 'order_item')]
|
||||
class OrderItem
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private readonly DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: DrinkType::class, inversedBy: 'orderItems')]
|
||||
#[ORM\JoinColumn(name: 'drink_type_id', referencedColumnName: 'id', nullable: false)]
|
||||
private DrinkType $drinkType,
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $quantity,
|
||||
#[ORM\ManyToOne(targetEntity: Order::class, inversedBy: 'orderItems')]
|
||||
#[ORM\JoinColumn(name: 'order_id', referencedColumnName: 'id', nullable: false)]
|
||||
private ?Order $order = null,
|
||||
?DateTimeImmutable $createdAt = null,
|
||||
?DateTimeImmutable $updatedAt = null
|
||||
) {
|
||||
$this->createdAt = $createdAt ?? new DateTimeImmutable();
|
||||
$this->updatedAt = $updatedAt ?? new DateTimeImmutable();
|
||||
|
||||
// Establish bidirectional relationship
|
||||
if ($this->order instanceof Order) {
|
||||
$this->order->addOrderItem($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOrder(): ?Order
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function setOrder(?Order $order): self
|
||||
{
|
||||
// Remove from old order if exists
|
||||
if ($this->order instanceof Order && $this->order !== $order) {
|
||||
$this->order->removeOrderItem($this);
|
||||
}
|
||||
|
||||
// Set new order
|
||||
$this->order = $order;
|
||||
|
||||
// Add to new order if not null
|
||||
if ($order instanceof Order && !$order->getOrderItems()->contains($this)) {
|
||||
$order->addOrderItem($this);
|
||||
}
|
||||
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDrinkType(): DrinkType
|
||||
{
|
||||
return $this->drinkType;
|
||||
}
|
||||
|
||||
public function setDrinkType(DrinkType $drinkType): self
|
||||
{
|
||||
$this->drinkType = $drinkType;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQuantity(): int
|
||||
{
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function setQuantity(int $quantity): self
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function updateTimestamp(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
81
src/Entity/SystemConfig.php
Normal file
81
src/Entity/SystemConfig.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use DateTimeImmutable;
|
||||
use App\Repository\SystemConfigRepository;
|
||||
|
||||
#[ORM\Entity(repositoryClass: SystemConfigRepository::class)]
|
||||
#[ORM\Table(name: 'system_config')]
|
||||
class SystemConfig
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private readonly DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private DateTimeImmutable $updatedAt;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: 'string', length: 255, unique: true)]
|
||||
private string $key,
|
||||
#[ORM\Column(type: 'text')]
|
||||
private string $value,
|
||||
?DateTimeImmutable $createdAt = null,
|
||||
?DateTimeImmutable $updatedAt = null
|
||||
) {
|
||||
$this->createdAt = $createdAt ?? new DateTimeImmutable();
|
||||
$this->updatedAt = $updatedAt ?? new DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function setKey(string $key): self
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string $value): self
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->updateTimestamp();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function updateTimestamp(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
27
src/Enum/SystemSettingKey.php
Normal file
27
src/Enum/SystemSettingKey.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* Enum for system setting keys
|
||||
*/
|
||||
enum SystemSettingKey: string
|
||||
{
|
||||
/**
|
||||
* @deprecated Use STOCK_ADJUSTMENT_LOOKBACK_ORDERS instead
|
||||
*/
|
||||
case STOCK_ADJUSTMENT_LOOKBACK = 'stock_adjustment_lookback';
|
||||
case STOCK_ADJUSTMENT_LOOKBACK_ORDERS = 'stock_adjustment_lookback_orders';
|
||||
case STOCK_ADJUSTMENT_MAGNITUDE = 'stock_adjustment_magnitude';
|
||||
case STOCK_ADJUSTMENT_THRESHOLD = 'stock_adjustment_threshold';
|
||||
case LOW_STOCK_THRESHOLD = 'low_stock_threshold';
|
||||
case CRITICAL_STOCK_THRESHOLD = 'critical_stock_threshold';
|
||||
case DEFAULT_DESIRED_STOCK = 'default_desired_stock';
|
||||
case SYSTEM_NAME = 'system_name';
|
||||
case ENABLE_AUTO_ADJUSTMENT = 'enable_auto_adjustment';
|
||||
case SHOW_LOW_STOCK_ALERTS = 'show_low_stock_alerts';
|
||||
case SHOW_QUICK_UPDATE_FORM = 'show_quick_update_form';
|
||||
case ITEMS_PER_PAGE = 'items_per_page';
|
||||
}
|
70
src/Middleware/ErrorHandlerMiddleware.php
Normal file
70
src/Middleware/ErrorHandlerMiddleware.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Slim\Exception\HttpException;
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Views\Twig;
|
||||
use Throwable;
|
||||
|
||||
class ErrorHandlerMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(private readonly Logger $logger, private readonly Twig $twig, private readonly bool $displayErrorDetails = false) {}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
try {
|
||||
return $handler->handle($request);
|
||||
} catch (HttpException $e) {
|
||||
return $this->handleException($e, $request, $e->getCode());
|
||||
} catch (Throwable $e) {
|
||||
return $this->handleException($e, $request);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleException(Throwable $e, ServerRequestInterface $request, int $statusCode = 500): ResponseInterface
|
||||
{
|
||||
$this->logger->error($e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'request' => [
|
||||
'method' => $request->getMethod(),
|
||||
'uri' => (string) $request->getUri(),
|
||||
],
|
||||
]);
|
||||
|
||||
$response = new Response($statusCode);
|
||||
|
||||
// Only show error details in development
|
||||
$error = [
|
||||
'message' => $this->displayErrorDetails ? $e->getMessage() : 'An error occurred',
|
||||
];
|
||||
|
||||
if ($this->displayErrorDetails) {
|
||||
$error['trace'] = $e->getTraceAsString();
|
||||
$error['file'] = $e->getFile();
|
||||
$error['line'] = $e->getLine();
|
||||
}
|
||||
|
||||
// Try to render an error page, fall back to JSON if Twig fails
|
||||
try {
|
||||
return $this->twig->render($response, 'error.twig', [
|
||||
'error' => $error,
|
||||
'status_code' => $statusCode,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error('Failed to render error template', ['exception' => $e]);
|
||||
|
||||
$response = $response->withHeader('Content-Type', 'application/json');
|
||||
$response->getBody()->write(json_encode(['error' => $error]));
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
33
src/Repository/AbstractRepository.php
Normal file
33
src/Repository/AbstractRepository.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
/**
|
||||
* @template E of object
|
||||
* @extends EntityRepository<E>
|
||||
*/
|
||||
abstract class AbstractRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @param E $entity
|
||||
* @return void
|
||||
*/
|
||||
public function save(object $entity): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param E $entity
|
||||
*/
|
||||
public function remove(object $entity): void
|
||||
{
|
||||
$this->getEntityManager()->remove($entity);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
20
src/Repository/DrinkTypeRepository.php
Normal file
20
src/Repository/DrinkTypeRepository.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractRepository<DrinkType>
|
||||
*/
|
||||
class DrinkTypeRepository extends AbstractRepository
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager, $entityManager->getClassMetadata(DrinkType::class));
|
||||
}
|
||||
}
|
61
src/Repository/InventoryRecordRepository.php
Normal file
61
src/Repository/InventoryRecordRepository.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use App\Entity\InventoryRecord;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* @extends AbstractRepository<InventoryRecord>
|
||||
*/
|
||||
class InventoryRecordRepository extends AbstractRepository
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager, $entityManager->getClassMetadata(InventoryRecord::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, InventoryRecord>
|
||||
*/
|
||||
public function findByDrinkType(DrinkType $drinkType): array
|
||||
{
|
||||
return $this->findBy(['drinkType' => $drinkType], ['timestamp' => 'DESC']);
|
||||
}
|
||||
|
||||
public function findLatestByDrinkType(DrinkType $drinkType): ?InventoryRecord
|
||||
{
|
||||
$records = $this->findBy(
|
||||
['drinkType' => $drinkType],
|
||||
['timestamp' => 'DESC'],
|
||||
1
|
||||
);
|
||||
|
||||
return $records[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, InventoryRecord>
|
||||
*/
|
||||
public function findByTimestampRange(DateTimeImmutable $start, DateTimeImmutable $end): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('ir')
|
||||
->from(InventoryRecord::class, 'ir')
|
||||
->where('ir.timestamp >= :start')
|
||||
->andWhere('ir.timestamp <= :end')
|
||||
->setParameter('start', $start)
|
||||
->setParameter('end', $end)
|
||||
->orderBy('ir.timestamp', 'DESC');
|
||||
|
||||
/** @var array<int, InventoryRecord> $result */
|
||||
$result = $qb->getQuery()->getResult();
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
46
src/Repository/OrderItemRepository.php
Normal file
46
src/Repository/OrderItemRepository.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use App\Entity\Order;
|
||||
use App\Entity\OrderItem;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractRepository<OrderItem>
|
||||
*/
|
||||
class OrderItemRepository extends AbstractRepository
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager, $entityManager->getClassMetadata(OrderItem::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, OrderItem>
|
||||
*/
|
||||
public function findByOrder(Order $order): array
|
||||
{
|
||||
return $this->findBy(['order' => $order]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, OrderItem>
|
||||
*/
|
||||
public function findByDrinkType(DrinkType $drinkType): array
|
||||
{
|
||||
return $this->findBy(['drinkType' => $drinkType]);
|
||||
}
|
||||
|
||||
public function findByOrderAndDrinkType(Order $order, DrinkType $drinkType): ?OrderItem
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'order' => $order,
|
||||
'drinkType' => $drinkType,
|
||||
]);
|
||||
}
|
||||
}
|
74
src/Repository/OrderRepository.php
Normal file
74
src/Repository/OrderRepository.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Order;
|
||||
use App\Entity\DrinkType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* @extends AbstractRepository<Order>
|
||||
*/
|
||||
class OrderRepository extends AbstractRepository
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager, $entityManager->getClassMetadata(Order::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Order>
|
||||
*/
|
||||
public function findByStatus(string $status): array
|
||||
{
|
||||
return $this->findBy(['status' => $status], ['createdAt' => 'DESC']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Order>
|
||||
*/
|
||||
public function findByDateRange(DateTimeImmutable $start, DateTimeImmutable $end): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('o')
|
||||
->from(Order::class, 'o')
|
||||
->where('o.createdAt >= :start')
|
||||
->andWhere('o.createdAt <= :end')
|
||||
->setParameter('start', $start)
|
||||
->setParameter('end', $end)
|
||||
->orderBy('o.createdAt', 'DESC');
|
||||
|
||||
/** @var array<int, Order> $result */
|
||||
$result = $qb->getQuery()->getResult();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the last N orders that contain a specific drink type
|
||||
*
|
||||
* @param DrinkType $drinkType The drink type to search for
|
||||
* @param int $limit The maximum number of orders to return
|
||||
* @return array<int, Order> The last N orders containing the drink type, ordered by creation date (newest first)
|
||||
*/
|
||||
public function findLastOrdersForDrinkType(DrinkType $drinkType, int $limit = 5): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->select('o')
|
||||
->from(Order::class, 'o')
|
||||
->join('o.orderItems', 'oi')
|
||||
->where('oi.drinkType = :drinkType')
|
||||
->andWhere('o.status = :status') // Only consider fulfilled orders
|
||||
->setParameter('drinkType', $drinkType)
|
||||
->setParameter('status', Order::STATUS_FULFILLED)
|
||||
->orderBy('o.createdAt', 'DESC')
|
||||
->setMaxResults($limit);
|
||||
|
||||
/** @var array<int, Order> $result */
|
||||
$result = $qb->getQuery()->getResult();
|
||||
return $result;
|
||||
}
|
||||
}
|
43
src/Repository/SystemConfigRepository.php
Normal file
43
src/Repository/SystemConfigRepository.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\SystemConfig;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractRepository<SystemConfig>
|
||||
*/
|
||||
class SystemConfigRepository extends AbstractRepository
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager, $entityManager->getClassMetadata(SystemConfig::class));
|
||||
}
|
||||
|
||||
public function findByKey(string $key): ?SystemConfig
|
||||
{
|
||||
return $this->findOneBy(['key' => $key]);
|
||||
}
|
||||
|
||||
public function getValue(string $key, string $default = ''): string
|
||||
{
|
||||
$config = $this->findByKey($key);
|
||||
return $config instanceof SystemConfig ? $config->getValue() : $default;
|
||||
}
|
||||
|
||||
public function setValue(string $key, string $value): void
|
||||
{
|
||||
$config = $this->findByKey($key);
|
||||
|
||||
if ($config instanceof SystemConfig) {
|
||||
$config->setValue($value);
|
||||
} else {
|
||||
$config = new SystemConfig($key, $value);
|
||||
}
|
||||
$this->save($config);
|
||||
}
|
||||
}
|
197
src/Service/ConfigurationService.php
Normal file
197
src/Service/ConfigurationService.php
Normal file
|
@ -0,0 +1,197 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use App\Entity\SystemConfig;
|
||||
use App\Repository\SystemConfigRepository;
|
||||
use App\Enum\SystemSettingKey;
|
||||
|
||||
class ConfigurationService
|
||||
{
|
||||
// For backward compatibility
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK instead
|
||||
*/
|
||||
public const KEY_STOCK_ADJUSTMENT_LOOKBACK = 'stock_adjustment_lookback';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK_ORDERS instead
|
||||
*/
|
||||
public const KEY_STOCK_ADJUSTMENT_LOOKBACK_ORDERS = 'stock_adjustment_lookback_orders';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::STOCK_ADJUSTMENT_MAGNITUDE instead
|
||||
*/
|
||||
public const KEY_STOCK_ADJUSTMENT_MAGNITUDE = 'stock_adjustment_magnitude';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::STOCK_ADJUSTMENT_THRESHOLD instead
|
||||
*/
|
||||
public const KEY_STOCK_ADJUSTMENT_THRESHOLD = 'stock_adjustment_threshold';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::LOW_STOCK_THRESHOLD instead
|
||||
*/
|
||||
public const KEY_LOW_STOCK_THRESHOLD = 'low_stock_threshold';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::CRITICAL_STOCK_THRESHOLD instead
|
||||
*/
|
||||
public const KEY_CRITICAL_STOCK_THRESHOLD = 'critical_stock_threshold';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::DEFAULT_DESIRED_STOCK instead
|
||||
*/
|
||||
public const KEY_DEFAULT_DESIRED_STOCK = 'default_desired_stock';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::SYSTEM_NAME instead
|
||||
*/
|
||||
public const KEY_SYSTEM_NAME = 'system_name';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::ENABLE_AUTO_ADJUSTMENT instead
|
||||
*/
|
||||
public const KEY_ENABLE_AUTO_ADJUSTMENT = 'enable_auto_adjustment';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::SHOW_LOW_STOCK_ALERTS instead
|
||||
*/
|
||||
public const KEY_SHOW_LOW_STOCK_ALERTS = 'show_low_stock_alerts';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::SHOW_QUICK_UPDATE_FORM instead
|
||||
*/
|
||||
public const KEY_SHOW_QUICK_UPDATE_FORM = 'show_quick_update_form';
|
||||
/**
|
||||
* @deprecated Use SystemSettingKey::ITEMS_PER_PAGE instead
|
||||
*/
|
||||
public const KEY_ITEMS_PER_PAGE = 'items_per_page';
|
||||
|
||||
public function __construct(
|
||||
private readonly SystemConfigRepository $systemConfigRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all configuration entries
|
||||
*
|
||||
* @return SystemConfig[]
|
||||
*/
|
||||
public function getAllConfigs(): array
|
||||
{
|
||||
return $this->systemConfigRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a configuration value by key
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $default Default value if the key doesn't exist
|
||||
* @return string
|
||||
*/
|
||||
public function getConfigValue(string $key, string $default = ''): string
|
||||
{
|
||||
return $this->systemConfigRepository->getValue($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a configuration value
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setConfigValue(string $key, string $value): void
|
||||
{
|
||||
$this->systemConfigRepository->setValue($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a configuration entity by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return SystemConfig|null
|
||||
*/
|
||||
public function getConfigByKey(string $key): ?SystemConfig
|
||||
{
|
||||
return $this->systemConfigRepository->findByKey($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new configuration entry
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return SystemConfig
|
||||
* @throws InvalidArgumentException If a configuration with the same key already exists
|
||||
*/
|
||||
public function createConfig(string $key, string $value): SystemConfig
|
||||
{
|
||||
if ($this->systemConfigRepository->findByKey($key) instanceof SystemConfig) {
|
||||
throw new InvalidArgumentException("A configuration with the key '$key' already exists");
|
||||
}
|
||||
|
||||
$config = new SystemConfig($key, $value);
|
||||
$this->systemConfigRepository->save($config);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing configuration entry
|
||||
*
|
||||
* @param SystemConfig $config
|
||||
* @param string|null $key
|
||||
* @param string|null $value
|
||||
* @return SystemConfig
|
||||
* @throws InvalidArgumentException If a configuration with the same key already exists
|
||||
*/
|
||||
public function updateConfig(
|
||||
SystemConfig $config,
|
||||
?string $key = null,
|
||||
?string $value = null
|
||||
): SystemConfig {
|
||||
// Update key if provided
|
||||
if ($key !== null && $key !== $config->getKey()) {
|
||||
// Check if a configuration with the same key already exists
|
||||
if ($this->systemConfigRepository->findByKey($key) instanceof SystemConfig) {
|
||||
throw new InvalidArgumentException("A configuration with the key '$key' already exists");
|
||||
}
|
||||
$config->setKey($key);
|
||||
}
|
||||
|
||||
// Update value if provided
|
||||
if ($value !== null) {
|
||||
$config->setValue($value);
|
||||
}
|
||||
|
||||
$this->systemConfigRepository->save($config);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a configuration entry
|
||||
*
|
||||
* @param SystemConfig $config
|
||||
* @return void
|
||||
*/
|
||||
public function deleteConfig(SystemConfig $config): void
|
||||
{
|
||||
$this->systemConfigRepository->remove($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default configuration values if they don't exist
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initializeDefaultConfigs(): void
|
||||
{
|
||||
$defaults = [
|
||||
SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK->value => '30', // Deprecated
|
||||
SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK_ORDERS->value => '5',
|
||||
SystemSettingKey::STOCK_ADJUSTMENT_MAGNITUDE->value => '0.2',
|
||||
SystemSettingKey::STOCK_ADJUSTMENT_THRESHOLD->value => '0.1',
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (!$this->getConfigByKey($key) instanceof SystemConfig) {
|
||||
$this->createConfig($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
128
src/Service/DrinkTypeService.php
Normal file
128
src/Service/DrinkTypeService.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use App\Entity\DrinkType;
|
||||
use App\Repository\DrinkTypeRepository;
|
||||
use App\Service\ConfigurationService;
|
||||
|
||||
class DrinkTypeService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DrinkTypeRepository $drinkTypeRepository,
|
||||
private readonly ConfigurationService $configService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all drink types
|
||||
*
|
||||
* @return DrinkType[]
|
||||
*/
|
||||
public function getAllDrinkTypes(): array
|
||||
{
|
||||
return $this->drinkTypeRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a drink type by ID
|
||||
*
|
||||
* @param int $id
|
||||
* @return DrinkType|null
|
||||
*/
|
||||
public function getDrinkTypeById(int $id): ?DrinkType
|
||||
{
|
||||
return $this->drinkTypeRepository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a drink type by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return DrinkType|null
|
||||
*/
|
||||
public function getDrinkTypeByName(string $name): ?DrinkType
|
||||
{
|
||||
return $this->drinkTypeRepository->findOneBy(['name' => $name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new drink type
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $description
|
||||
* @param int|null $desiredStock
|
||||
* @return DrinkType
|
||||
* @throws InvalidArgumentException If a drink type with the same name already exists
|
||||
*/
|
||||
public function createDrinkType(string $name, ?string $description = null, ?int $desiredStock = null): DrinkType
|
||||
{
|
||||
// Check if a drink type with the same name already exists
|
||||
if ($this->drinkTypeRepository->findOneBy(['name' => $name]) !== null) {
|
||||
throw new InvalidArgumentException("A drink type with the name '$name' already exists");
|
||||
}
|
||||
|
||||
// If no desired stock is provided, use the default from configuration
|
||||
if ($desiredStock === null) {
|
||||
$desiredStock = (int) $this->configService->getConfigValue('default_desired_stock', '10');
|
||||
}
|
||||
|
||||
$drinkType = new DrinkType($name, $description, $desiredStock);
|
||||
$this->drinkTypeRepository->save($drinkType);
|
||||
|
||||
return $drinkType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @param string|null $name
|
||||
* @param string|null $description
|
||||
* @param int|null $desiredStock
|
||||
* @return DrinkType
|
||||
* @throws InvalidArgumentException If a drink type with the same name already exists
|
||||
*/
|
||||
public function updateDrinkType(
|
||||
DrinkType $drinkType,
|
||||
?string $name = null,
|
||||
?string $description = null,
|
||||
?int $desiredStock = null
|
||||
): DrinkType {
|
||||
// Update name if provided
|
||||
if ($name !== null && $name !== $drinkType->getName()) {
|
||||
// Check if a drink type with the same name already exists
|
||||
if ($this->drinkTypeRepository->findOneBy(['name' => $name]) !== null) {
|
||||
throw new InvalidArgumentException("A drink type with the name '$name' already exists");
|
||||
}
|
||||
$drinkType->setName($name);
|
||||
}
|
||||
|
||||
// Update description if provided
|
||||
if ($description !== null) {
|
||||
$drinkType->setDescription($description);
|
||||
}
|
||||
|
||||
// Update desired stock if provided
|
||||
if ($desiredStock !== null) {
|
||||
$drinkType->setDesiredStock($desiredStock);
|
||||
}
|
||||
|
||||
$this->drinkTypeRepository->save($drinkType);
|
||||
|
||||
return $drinkType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @return void
|
||||
*/
|
||||
public function deleteDrinkType(DrinkType $drinkType): void
|
||||
{
|
||||
$this->drinkTypeRepository->remove($drinkType);
|
||||
}
|
||||
}
|
204
src/Service/InventoryService.php
Normal file
204
src/Service/InventoryService.php
Normal file
|
@ -0,0 +1,204 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\DrinkType;
|
||||
use App\Entity\InventoryRecord;
|
||||
use App\Repository\DrinkTypeRepository;
|
||||
use App\Repository\InventoryRecordRepository;
|
||||
use App\Service\ConfigurationService;
|
||||
use App\Enum\SystemSettingKey;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class InventoryService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InventoryRecordRepository $inventoryRecordRepository,
|
||||
private readonly DrinkTypeRepository $drinkTypeRepository,
|
||||
private readonly ConfigurationService $configService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all inventory records
|
||||
*
|
||||
* @return InventoryRecord[]
|
||||
*/
|
||||
public function getAllInventoryRecords(): array
|
||||
{
|
||||
return $this->inventoryRecordRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventory records for a specific drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @return InventoryRecord[]
|
||||
*/
|
||||
public function getInventoryRecordsByDrinkType(DrinkType $drinkType): array
|
||||
{
|
||||
return $this->inventoryRecordRepository->findByDrinkType($drinkType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest inventory record for a specific drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @return InventoryRecord|null
|
||||
*/
|
||||
public function getLatestInventoryRecord(DrinkType $drinkType): ?InventoryRecord
|
||||
{
|
||||
return $this->inventoryRecordRepository->findLatestByDrinkType($drinkType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventory records within a specific time range
|
||||
*
|
||||
* @param DateTimeImmutable $start
|
||||
* @param DateTimeImmutable $end
|
||||
* @return InventoryRecord[]
|
||||
*/
|
||||
public function getInventoryRecordsByTimeRange(DateTimeImmutable $start, DateTimeImmutable $end): array
|
||||
{
|
||||
return $this->inventoryRecordRepository->findByTimestampRange($start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current stock level for a specific drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @return int
|
||||
*/
|
||||
public function getCurrentStockLevel(DrinkType $drinkType): int
|
||||
{
|
||||
$latestRecord = $this->getLatestInventoryRecord($drinkType);
|
||||
return $latestRecord instanceof InventoryRecord ? $latestRecord->getQuantity() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the difference between current and desired stock for a specific drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @return int Positive value means excess stock, negative value means shortage
|
||||
*/
|
||||
public function getStockDifference(DrinkType $drinkType): int
|
||||
{
|
||||
$currentStock = $this->getCurrentStockLevel($drinkType);
|
||||
$desiredStock = $drinkType->getDesiredStock();
|
||||
|
||||
return $currentStock - $desiredStock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the stock level for a specific drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @param int $quantity
|
||||
* @param DateTimeImmutable|null $timestamp
|
||||
* @return InventoryRecord
|
||||
*/
|
||||
public function updateStockLevel(
|
||||
DrinkType $drinkType,
|
||||
int $quantity,
|
||||
?DateTimeImmutable $timestamp = null
|
||||
): InventoryRecord {
|
||||
$inventoryRecord = new InventoryRecord(
|
||||
$drinkType,
|
||||
$quantity,
|
||||
$timestamp
|
||||
);
|
||||
|
||||
$this->inventoryRecordRepository->save($inventoryRecord);
|
||||
|
||||
return $inventoryRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all drink types with their current stock levels
|
||||
*
|
||||
* @return array<int, array{drinkType: DrinkType, currentStock: int, desiredStock: int, difference: int}>
|
||||
*/
|
||||
public function getAllDrinkTypesWithStockLevels(): array
|
||||
{
|
||||
$drinkTypes = $this->drinkTypeRepository->findAll();
|
||||
$result = [];
|
||||
|
||||
foreach ($drinkTypes as $drinkType) {
|
||||
$currentStock = $this->getCurrentStockLevel($drinkType);
|
||||
$desiredStock = $drinkType->getDesiredStock();
|
||||
$difference = $currentStock - $desiredStock;
|
||||
|
||||
$result[] = [
|
||||
'drinkType' => $drinkType,
|
||||
'currentStock' => $currentStock,
|
||||
'desiredStock' => $desiredStock,
|
||||
'difference' => $difference,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all drink types with low stock (current stock below low stock threshold)
|
||||
*
|
||||
* @return array<int, array{drinkType: DrinkType, currentStock: int, desiredStock: int, shortage: int}>
|
||||
*/
|
||||
public function getLowStockDrinkTypes(): array
|
||||
{
|
||||
$lowStockThreshold = (float) $this->configService->getConfigValue(SystemSettingKey::LOW_STOCK_THRESHOLD->value, '50') / 100;
|
||||
$drinkTypes = $this->drinkTypeRepository->findAll();
|
||||
$result = [];
|
||||
|
||||
foreach ($drinkTypes as $drinkType) {
|
||||
$currentStock = $this->getCurrentStockLevel($drinkType);
|
||||
$desiredStock = $drinkType->getDesiredStock();
|
||||
|
||||
// Check if current stock is below the threshold percentage of desired stock
|
||||
if ($desiredStock > 0 && $currentStock < ($desiredStock * $lowStockThreshold)) {
|
||||
$shortage = $desiredStock - $currentStock;
|
||||
|
||||
$result[] = [
|
||||
'drinkType' => $drinkType,
|
||||
'currentStock' => $currentStock,
|
||||
'desiredStock' => $desiredStock,
|
||||
'shortage' => $shortage,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all drink types with critical stock (current stock below critical stock threshold)
|
||||
*
|
||||
* @return array<int, array{drinkType: DrinkType, currentStock: int, desiredStock: int, shortage: int}>
|
||||
*/
|
||||
public function getCriticalStockDrinkTypes(): array
|
||||
{
|
||||
$criticalStockThreshold = (float) $this->configService->getConfigValue(SystemSettingKey::CRITICAL_STOCK_THRESHOLD->value, '25') / 100;
|
||||
$drinkTypes = $this->drinkTypeRepository->findAll();
|
||||
$result = [];
|
||||
|
||||
foreach ($drinkTypes as $drinkType) {
|
||||
$currentStock = $this->getCurrentStockLevel($drinkType);
|
||||
$desiredStock = $drinkType->getDesiredStock();
|
||||
|
||||
// Check if current stock is below the threshold percentage of desired stock
|
||||
if ($desiredStock > 0 && $currentStock < ($desiredStock * $criticalStockThreshold)) {
|
||||
$shortage = $desiredStock - $currentStock;
|
||||
|
||||
$result[] = [
|
||||
'drinkType' => $drinkType,
|
||||
'currentStock' => $currentStock,
|
||||
'desiredStock' => $desiredStock,
|
||||
'shortage' => $shortage,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
217
src/Service/OrderService.php
Normal file
217
src/Service/OrderService.php
Normal file
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use App\Entity\DrinkType;
|
||||
use App\Entity\Order;
|
||||
use App\Entity\OrderItem;
|
||||
use App\Repository\DrinkTypeRepository;
|
||||
use App\Repository\OrderItemRepository;
|
||||
use App\Repository\OrderRepository;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class OrderService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderRepository $orderRepository,
|
||||
private readonly OrderItemRepository $orderItemRepository,
|
||||
private readonly DrinkTypeRepository $drinkTypeRepository,
|
||||
private readonly InventoryService $inventoryService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all orders
|
||||
*
|
||||
* @return Order[]
|
||||
*/
|
||||
public function getAllOrders(): array
|
||||
{
|
||||
return $this->orderRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an order by ID
|
||||
*
|
||||
* @param int $id
|
||||
* @return Order|null
|
||||
*/
|
||||
public function getOrderById(int $id): ?Order
|
||||
{
|
||||
return $this->orderRepository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orders by status
|
||||
*
|
||||
* @param string $status
|
||||
* @return Order[]
|
||||
*/
|
||||
public function getOrdersByStatus(string $status): array
|
||||
{
|
||||
return $this->orderRepository->findByStatus($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orders within a specific date range
|
||||
*
|
||||
* @param DateTimeImmutable $start
|
||||
* @param DateTimeImmutable $end
|
||||
* @return Order[]
|
||||
*/
|
||||
public function getOrdersByDateRange(DateTimeImmutable $start, DateTimeImmutable $end): array
|
||||
{
|
||||
return $this->orderRepository->findByDateRange($start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new order
|
||||
*
|
||||
* @param array<int, array{drinkTypeId: int, quantity: int}> $items
|
||||
* @return Order
|
||||
* @throws InvalidArgumentException If a drink type ID is invalid
|
||||
*/
|
||||
public function createOrder(array $items): Order
|
||||
{
|
||||
$order = new Order();
|
||||
$this->orderRepository->save($order);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$drinkType = $this->drinkTypeRepository->find($item['drinkTypeId']);
|
||||
if ($drinkType === null) {
|
||||
throw new InvalidArgumentException("Invalid drink type ID: {$item['drinkTypeId']}");
|
||||
}
|
||||
|
||||
$orderItem = new OrderItem($drinkType, $item['quantity'], $order);
|
||||
$this->orderItemRepository->save($orderItem);
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an order based on current stock levels
|
||||
*
|
||||
* @return Order
|
||||
*/
|
||||
public function createOrderFromStockLevels(): Order
|
||||
{
|
||||
$lowStockItems = $this->inventoryService->getAllDrinkTypesWithStockLevels();
|
||||
$orderItems = [];
|
||||
|
||||
foreach ($lowStockItems as $item) {
|
||||
if ($item['currentStock'] < $item['desiredStock']) {
|
||||
$orderItems[] = [
|
||||
'drinkTypeId' => $item['drinkType']->getId(),
|
||||
'quantity' => $item['desiredStock'] - $item['currentStock'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createOrder($orderItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an order's status
|
||||
*
|
||||
* @param Order $order
|
||||
* @param string $status
|
||||
* @return Order
|
||||
* @throws InvalidArgumentException If the status is invalid
|
||||
*/
|
||||
public function updateOrderStatus(Order $order, string $status): Order
|
||||
{
|
||||
$order->setStatus($status);
|
||||
$this->orderRepository->save($order);
|
||||
|
||||
// If the order is fulfilled, update the inventory
|
||||
if ($status === Order::STATUS_FULFILLED) {
|
||||
$this->updateInventoryFromOrder($order);
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update inventory based on a fulfilled order
|
||||
*
|
||||
* @param Order $order
|
||||
* @return void
|
||||
*/
|
||||
private function updateInventoryFromOrder(Order $order): void
|
||||
{
|
||||
foreach ($order->getOrderItems() as $orderItem) {
|
||||
$drinkType = $orderItem->getDrinkType();
|
||||
$currentStock = $this->inventoryService->getCurrentStockLevel($drinkType);
|
||||
$newStock = $currentStock + $orderItem->getQuantity();
|
||||
|
||||
$this->inventoryService->updateStockLevel($drinkType, $newStock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to an order
|
||||
*
|
||||
* @param Order $order
|
||||
* @param DrinkType $drinkType
|
||||
* @param int $quantity
|
||||
* @return OrderItem
|
||||
* @throws InvalidArgumentException If the order is not in 'new' or 'in_work' status
|
||||
*/
|
||||
public function addOrderItem(Order $order, DrinkType $drinkType, int $quantity): OrderItem
|
||||
{
|
||||
if (!in_array($order->getStatus(), [Order::STATUS_NEW, Order::STATUS_IN_WORK], true)) {
|
||||
throw new InvalidArgumentException("Cannot add items to an order with status '{$order->getStatus()}'");
|
||||
}
|
||||
|
||||
// Check if the order already has an item for this drink type
|
||||
$existingItem = $this->orderItemRepository->findByOrderAndDrinkType($order, $drinkType);
|
||||
|
||||
if ($existingItem instanceof OrderItem) {
|
||||
// Update the existing item
|
||||
$existingItem->setQuantity($existingItem->getQuantity() + $quantity);
|
||||
$this->orderItemRepository->save($existingItem);
|
||||
return $existingItem;
|
||||
}
|
||||
// Create a new item
|
||||
$orderItem = new OrderItem($drinkType, $quantity, $order);
|
||||
$this->orderItemRepository->save($orderItem);
|
||||
return $orderItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from an order
|
||||
*
|
||||
* @param Order $order
|
||||
* @param OrderItem $orderItem
|
||||
* @return void
|
||||
* @throws InvalidArgumentException If the order is not in 'new' or 'in_work' status
|
||||
*/
|
||||
public function removeOrderItem(Order $order, OrderItem $orderItem): void
|
||||
{
|
||||
if (!in_array($order->getStatus(), [Order::STATUS_NEW, Order::STATUS_IN_WORK], true)) {
|
||||
throw new InvalidArgumentException("Cannot remove items from an order with status '{$order->getStatus()}'");
|
||||
}
|
||||
|
||||
$order->removeOrderItem($orderItem);
|
||||
$this->orderItemRepository->remove($orderItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an order
|
||||
*
|
||||
* @param Order $order
|
||||
* @return void
|
||||
* @throws InvalidArgumentException If the order is not in 'new' status
|
||||
*/
|
||||
public function deleteOrder(Order $order): void
|
||||
{
|
||||
if ($order->getStatus() !== Order::STATUS_NEW) {
|
||||
throw new InvalidArgumentException("Cannot delete an order with status '{$order->getStatus()}'");
|
||||
}
|
||||
|
||||
$this->orderRepository->remove($order);
|
||||
}
|
||||
}
|
262
src/Service/StockAdjustmentService.php
Normal file
262
src/Service/StockAdjustmentService.php
Normal file
|
@ -0,0 +1,262 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\InventoryRecord;
|
||||
use App\Entity\DrinkType;
|
||||
use App\Repository\DrinkTypeRepository;
|
||||
use App\Repository\InventoryRecordRepository;
|
||||
use App\Repository\OrderRepository;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class StockAdjustmentService
|
||||
{
|
||||
private const int DEFAULT_LOOKBACK_ORDERS = 5;
|
||||
private const float DEFAULT_ADJUSTMENT_MAGNITUDE = 0.2; // 20%
|
||||
private const float DEFAULT_ADJUSTMENT_THRESHOLD = 0.1; // 10%
|
||||
|
||||
public function __construct(
|
||||
private readonly DrinkTypeRepository $drinkTypeRepository,
|
||||
private readonly InventoryRecordRepository $inventoryRecordRepository,
|
||||
private readonly OrderRepository $orderRepository,
|
||||
private readonly ConfigurationService $configService,
|
||||
private readonly InventoryService $inventoryService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Analyze consumption patterns and adjust desired stock levels
|
||||
*
|
||||
* @return array<int, array{drinkType: DrinkType, oldDesiredStock: int, newDesiredStock: int, adjustmentReason: string}>
|
||||
*/
|
||||
public function adjustStockLevels(): array
|
||||
{
|
||||
$lookbackOrders = (int) $this->configService->getConfigValue(
|
||||
'stock_adjustment_lookback_orders',
|
||||
(string) self::DEFAULT_LOOKBACK_ORDERS
|
||||
);
|
||||
|
||||
$adjustmentMagnitude = (float) $this->configService->getConfigValue(
|
||||
'stock_adjustment_magnitude',
|
||||
(string) self::DEFAULT_ADJUSTMENT_MAGNITUDE
|
||||
);
|
||||
|
||||
$adjustmentThreshold = (float) $this->configService->getConfigValue(
|
||||
'stock_adjustment_threshold',
|
||||
(string) self::DEFAULT_ADJUSTMENT_THRESHOLD
|
||||
);
|
||||
|
||||
$drinkTypes = $this->drinkTypeRepository->findAll();
|
||||
$result = [];
|
||||
|
||||
foreach ($drinkTypes as $drinkType) {
|
||||
$consumptionRate = $this->calculateConsumptionRateFromOrders($drinkType, $lookbackOrders);
|
||||
|
||||
if ($consumptionRate === null) {
|
||||
continue; // Skip if we don't have enough data
|
||||
}
|
||||
|
||||
$currentDesiredStock = $drinkType->getDesiredStock();
|
||||
// For the suggested stock, we use the consumption rate directly as it's already weighted
|
||||
$suggestedStock = (int) ceil($consumptionRate);
|
||||
|
||||
// Only adjust if the difference is significant
|
||||
$difference = abs($suggestedStock - $currentDesiredStock) / max(1, $currentDesiredStock);
|
||||
|
||||
if ($difference >= $adjustmentThreshold) {
|
||||
// Calculate the new desired stock with a gradual adjustment
|
||||
$adjustment = (int) ceil(($suggestedStock - $currentDesiredStock) * $adjustmentMagnitude);
|
||||
$newDesiredStock = $currentDesiredStock + $adjustment;
|
||||
|
||||
// Ensure we don't go below 1
|
||||
$newDesiredStock = max(1, $newDesiredStock);
|
||||
|
||||
// Update the drink type
|
||||
$drinkType->setDesiredStock($newDesiredStock);
|
||||
$this->drinkTypeRepository->save($drinkType);
|
||||
|
||||
$result[] = [
|
||||
'drinkType' => $drinkType,
|
||||
'oldDesiredStock' => $currentDesiredStock,
|
||||
'newDesiredStock' => $newDesiredStock,
|
||||
'adjustmentReason' => $suggestedStock > $currentDesiredStock
|
||||
? 'Increased consumption rate'
|
||||
: 'Decreased consumption rate',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the weighted consumption rate based on the last N orders for a drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @param int $lookbackOrders Number of past orders to consider
|
||||
* @return float|null Weighted consumption rate, or null if not enough data
|
||||
*/
|
||||
private function calculateConsumptionRateFromOrders(
|
||||
DrinkType $drinkType,
|
||||
int $lookbackOrders
|
||||
): ?float {
|
||||
// Get the last N orders for this drink type
|
||||
$orders = $this->orderRepository->findLastOrdersForDrinkType($drinkType, $lookbackOrders);
|
||||
|
||||
// Need at least one order to calculate consumption
|
||||
if (count($orders) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate weighted consumption
|
||||
$totalWeightedConsumption = 0;
|
||||
$totalWeight = 0;
|
||||
|
||||
// Define weights - most recent order has highest weight
|
||||
// For example, with 5 orders: weights are 5, 4, 3, 2, 1
|
||||
$weights = range($lookbackOrders, 1);
|
||||
|
||||
foreach ($orders as $index => $order) {
|
||||
// Get the order items for this drink type
|
||||
$orderItems = $order->getOrderItems();
|
||||
$quantity = 0;
|
||||
|
||||
// Sum up quantities for this drink type
|
||||
foreach ($orderItems as $item) {
|
||||
if ($item->getDrinkType()->getId() === $drinkType->getId()) {
|
||||
$quantity += $item->getQuantity();
|
||||
}
|
||||
}
|
||||
|
||||
// Apply weight to this order's consumption
|
||||
$weight = $weights[$index] ?? 1; // Fallback to weight 1 if index is out of bounds
|
||||
$totalWeightedConsumption += $quantity * $weight;
|
||||
$totalWeight += $weight;
|
||||
}
|
||||
|
||||
// If total weight is 0, return null (shouldn't happen, but just in case)
|
||||
if ($totalWeight === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate weighted average consumption
|
||||
return $totalWeightedConsumption / $totalWeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the average daily consumption rate for a drink type
|
||||
*
|
||||
* @deprecated Use calculateConsumptionRateFromOrders instead
|
||||
* @param DrinkType $drinkType
|
||||
* @param DateTimeImmutable $startDate
|
||||
* @param DateTimeImmutable $endDate
|
||||
* @return float|null Average daily consumption rate, or null if not enough data
|
||||
*/
|
||||
private function calculateConsumptionRate(
|
||||
DrinkType $drinkType,
|
||||
DateTimeImmutable $startDate,
|
||||
DateTimeImmutable $endDate
|
||||
): ?float {
|
||||
// Get inventory records for the period
|
||||
$inventoryRecords = $this->inventoryRecordRepository->findByDrinkType($drinkType);
|
||||
|
||||
// Filter records within the date range
|
||||
$inventoryRecords = array_filter($inventoryRecords, function ($record) use ($startDate, $endDate): bool {
|
||||
$timestamp = $record->getTimestamp();
|
||||
return $timestamp >= $startDate && $timestamp <= $endDate;
|
||||
});
|
||||
|
||||
// Sort by timestamp
|
||||
usort($inventoryRecords, fn($a, $b): int => $a->getTimestamp() <=> $b->getTimestamp());
|
||||
|
||||
// Need at least two records to calculate consumption
|
||||
if (count($inventoryRecords) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate total consumption
|
||||
$totalConsumption = 0;
|
||||
$previousRecord = null;
|
||||
|
||||
foreach ($inventoryRecords as $record) {
|
||||
if ($previousRecord instanceof InventoryRecord) {
|
||||
$previousQuantity = $previousRecord->getQuantity();
|
||||
$currentQuantity = $record->getQuantity();
|
||||
$daysDifference = $record->getTimestamp()->diff($previousRecord->getTimestamp())->days;
|
||||
|
||||
// If days difference is 0, use 1 to avoid division by zero
|
||||
$daysDifference = max(1, $daysDifference);
|
||||
|
||||
// If current quantity is less than previous, consumption occurred
|
||||
if ($currentQuantity < $previousQuantity) {
|
||||
$consumption = $previousQuantity - $currentQuantity;
|
||||
$totalConsumption += $consumption;
|
||||
}
|
||||
}
|
||||
|
||||
$previousRecord = $record;
|
||||
}
|
||||
|
||||
// Calculate days between first and last record
|
||||
$firstRecord = reset($inventoryRecords);
|
||||
$lastRecord = end($inventoryRecords);
|
||||
$totalDays = $lastRecord->getTimestamp()->diff($firstRecord->getTimestamp())->days;
|
||||
|
||||
// If total days is 0, use 1 to avoid division by zero
|
||||
$totalDays = max(1, $totalDays);
|
||||
|
||||
// Calculate average daily consumption
|
||||
return $totalConsumption / $totalDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the consumption history for a drink type
|
||||
*
|
||||
* @param DrinkType $drinkType
|
||||
* @param DateTimeImmutable $startDate
|
||||
* @param DateTimeImmutable $endDate
|
||||
* @return array<int, array{date: DateTimeImmutable, consumption: int}>
|
||||
*/
|
||||
public function getConsumptionHistory(
|
||||
DrinkType $drinkType,
|
||||
DateTimeImmutable $startDate,
|
||||
DateTimeImmutable $endDate
|
||||
): array {
|
||||
// Get inventory records for the period
|
||||
$inventoryRecords = $this->inventoryRecordRepository->findByDrinkType($drinkType);
|
||||
|
||||
// Filter records within the date range
|
||||
$inventoryRecords = array_filter($inventoryRecords, function ($record) use ($startDate, $endDate): bool {
|
||||
$timestamp = $record->getTimestamp();
|
||||
return $timestamp >= $startDate && $timestamp <= $endDate;
|
||||
});
|
||||
|
||||
// Sort by timestamp
|
||||
usort($inventoryRecords, fn($a, $b): int => $a->getTimestamp() <=> $b->getTimestamp());
|
||||
|
||||
// Calculate consumption between each record
|
||||
$consumptionHistory = [];
|
||||
$previousRecord = null;
|
||||
|
||||
foreach ($inventoryRecords as $record) {
|
||||
if ($previousRecord !== null) {
|
||||
$previousQuantity = $previousRecord->getQuantity();
|
||||
$currentQuantity = $record->getQuantity();
|
||||
|
||||
// If current quantity is less than previous, consumption occurred
|
||||
if ($currentQuantity < $previousQuantity) {
|
||||
$consumption = $previousQuantity - $currentQuantity;
|
||||
$consumptionHistory[] = [
|
||||
'date' => $record->getTimestamp(),
|
||||
'consumption' => $consumption,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$previousRecord = $record;
|
||||
}
|
||||
|
||||
return $consumptionHistory;
|
||||
}
|
||||
}
|
14
src/Settings.php
Normal file
14
src/Settings.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final readonly class Settings
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isTestMode = false,
|
||||
public bool $isDevMode = false,
|
||||
public string $cacheDir = __DIR__ . '/../var/cache'
|
||||
) {}
|
||||
}
|
38
src/bootstrap.php
Normal file
38
src/bootstrap.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Middleware\ErrorHandlerMiddleware;
|
||||
use Monolog\Logger;
|
||||
use Slim\App;
|
||||
use Slim\Views\Twig;
|
||||
use DI\Bridge\Slim\Bridge;
|
||||
|
||||
return (function (): App {
|
||||
// Create container
|
||||
$container = (require __DIR__ . '/../config/container.php')();
|
||||
/** @var \App\Settings $settings */
|
||||
$settings = $container->get(\App\Settings::class);
|
||||
|
||||
// create cache dirs
|
||||
foreach (['twig', 'doctrine/proxy'] as $dir) {
|
||||
if (!is_dir($settings->cacheDir . '/' . $dir)) {
|
||||
mkdir($settings->cacheDir . '/' . $dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Create app with container
|
||||
$app = Bridge::create($container);
|
||||
|
||||
// Add middleware
|
||||
$app->add(new ErrorHandlerMiddleware(
|
||||
$container->get(Logger::class),
|
||||
$container->get(Twig::class),
|
||||
true // Set to false in production
|
||||
));
|
||||
|
||||
// Add routes
|
||||
(require __DIR__ . '/routes.php')($app);
|
||||
|
||||
return $app;
|
||||
})();
|
48
src/routes.php
Normal file
48
src/routes.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Controller\DashboardController;
|
||||
use App\Controller\DrinkTypeController;
|
||||
use App\Controller\InventoryController;
|
||||
use App\Controller\OrderController;
|
||||
use App\Controller\SettingsController;
|
||||
use Slim\App;
|
||||
|
||||
return function (App $app): void {
|
||||
// Dashboard routes
|
||||
$app->get('/', [DashboardController::class, 'index']);
|
||||
|
||||
// Drink Type routes
|
||||
$app->get('/drink-types', [DrinkTypeController::class, 'index']);
|
||||
$app->get('/drink-types/create', [DrinkTypeController::class, 'create']);
|
||||
$app->post('/drink-types', [DrinkTypeController::class, 'store']);
|
||||
$app->get('/drink-types/{id:[0-9]+}', [DrinkTypeController::class, 'show']);
|
||||
$app->get('/drink-types/{id:[0-9]+}/edit', [DrinkTypeController::class, 'edit']);
|
||||
$app->post('/drink-types/{id:[0-9]+}', [DrinkTypeController::class, 'update']);
|
||||
$app->post('/drink-types/{id:[0-9]+}/delete', [DrinkTypeController::class, 'delete']);
|
||||
|
||||
// Inventory routes
|
||||
$app->get('/inventory', [InventoryController::class, 'index']);
|
||||
$app->get('/inventory/update', [InventoryController::class, 'showUpdateForm']);
|
||||
$app->post('/inventory/update', [InventoryController::class, 'update']);
|
||||
$app->get('/inventory/history', [InventoryController::class, 'history']);
|
||||
$app->get('/inventory/history/{id:[0-9]+}', [InventoryController::class, 'history']);
|
||||
$app->get('/inventory/low-stock', [InventoryController::class, 'lowStock']);
|
||||
|
||||
// Order routes
|
||||
$app->get('/orders', [OrderController::class, 'index']);
|
||||
$app->get('/orders/create', [OrderController::class, 'create']);
|
||||
$app->post('/orders', [OrderController::class, 'store']);
|
||||
$app->get('/orders/create-from-stock', [OrderController::class, 'createFromStock']);
|
||||
$app->get('/orders/{id:[0-9]+}', [OrderController::class, 'show']);
|
||||
$app->post('/orders/{id:[0-9]+}/status', [OrderController::class, 'updateStatus']);
|
||||
$app->post('/orders/{id:[0-9]+}/items', [OrderController::class, 'addItem']);
|
||||
$app->post('/orders/{id:[0-9]+}/items/{itemId:[0-9]+}/remove', [OrderController::class, 'removeItem']);
|
||||
$app->post('/orders/{id:[0-9]+}/delete', [OrderController::class, 'delete']);
|
||||
|
||||
// Settings routes
|
||||
$app->get('/settings', [SettingsController::class, 'index']);
|
||||
$app->post('/settings', [SettingsController::class, 'update']);
|
||||
$app->post('/settings/reset', [SettingsController::class, 'resetToDefaults']);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue