updateeeeee

This commit is contained in:
lubiana 2025-06-10 20:14:19 +02:00
parent 0c758749e0
commit ca9819a436
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
12 changed files with 214 additions and 156 deletions

View file

@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250609175837 extends AbstractMigration
final class Version20250610173233 extends AbstractMigration
{
public function getDescription(): string
{
@ -23,20 +23,11 @@ final class Version20250609175837 extends AbstractMigration
$this->addSql(<<<'SQL'
CREATE TABLE drink_type (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, desired_stock INTEGER NOT NULL)
, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, wanted_stock INTEGER NOT NULL, current_stock INTEGER NOT NULL)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_841484B15E237E06 ON drink_type (name)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE inventory_record (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, drink_type_id INTEGER NOT NULL, quantity INTEGER NOT NULL, timestamp DATETIME NOT NULL --(DC2Type:datetime_immutable)
, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_9BE8033AE7E8D8A1 FOREIGN KEY (drink_type_id) REFERENCES drink_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9BE8033AE7E8D8A1 ON inventory_record (drink_type_id)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE "order" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
@ -73,9 +64,6 @@ final class Version20250609175837 extends AbstractMigration
$this->addSql(<<<'SQL'
DROP TABLE drink_type
SQL);
$this->addSql(<<<'SQL'
DROP TABLE inventory_record
SQL);
$this->addSql(<<<'SQL'
DROP TABLE "order"
SQL);

File diff suppressed because one or more lines are too long

View file

@ -29,45 +29,145 @@
}
}
/* Light Mode (Default) */
/*
* =================================================================================================
* 💅 GAY, SLAY & FABULOUS THEME 💅
*
* A Bootstrap override that's unapologetically queer, vibrant, and readable.
* Because your website deserves to be as iconic as you are.
*
* Designed with love, glitter, and a whole lot of sass.
* =================================================================================================
*/
:root {
--bs-primary: #ff47a3; /* Hot Pink */
--bs-secondary: #7367f0; /* Lavender */
--bs-success: #53e69d; /* Minty Green */
--bs-info: #51caff; /* Sky Blue */
--bs-warning: #ffd53e; /* Sunshine Yellow */
--bs-danger: #ff5666; /* Coral Red */
--bs-light: #fff7fa; /* Almost-white Pink */
--bs-dark: #3b233d; /* Deep Purple */
/* 🌈✨ The Main Attraction: A Vibrant & Proud Palette ✨🌈 */
--bs-red: #FF3860; /* 🍒 Cherry Pop Realness */
--bs-orange: #FF9F43; /* 🍊 Tangerine Dream Queen */
--bs-yellow: #FFE156; /* 🌟 Superstar Spotlight */
--bs-green: #34C759; /* 🌿 Growth & chosen family */
--bs-teal: #20c997; /* 🧜‍♀️ Mermaid's Grotto */
--bs-cyan: #4ED6FF; /* 🧊 Iced Genderfluid */
--bs-blue: #5AC8FA; /* 🦋 Blue Bi You */
--bs-indigo: #6C63FF; /* 🦄 Unicorn Energy */
--bs-purple: #AF52DE; /* 💜 Violets for Visibility */
--bs-pink: #FF6F91; /* 💖 Queerberry Kiss */
/* Quirk it up further */
--bs-body-bg: #fff7fa;
--bs-body-color: #3b233d;
--bs-link-color: #ff47a3;
--bs-link-hover-color: #7367f0;
--bs-border-color: #ffb8e1;
/* 💅 Neutrals That Are Anything But Neutral 💅 */
--bs-black: #1A202C; /* 🕶️ Midnight Manifesto */
--bs-white: #F8F7FF; /* ☁️ Cloud Nine Comfort */
--bs-gray: #A0AEC0; /* 🌫️ Foggy Festival Morning */
--bs-gray-dark: #2D3748; /* 🖤 Emo Afterparty */
--bs-gray-100: #F7FAFC;
--bs-gray-200: #EDF2F7;
--bs-gray-300: #E2E8F0;
--bs-gray-400: #CBD5E1;
--bs-gray-500: #A0AEC0;
--bs-gray-600: #718096;
--bs-gray-700: #4A5568;
--bs-gray-800: #2D3748;
--bs-gray-900: #1A202C;
/* 💖 Semantic Roles: Give Every Color a Job 💖 */
--bs-primary: var(--bs-indigo); /* Main Character Energy */
--bs-secondary: var(--bs-pink); /* Sassy Sidekick */
--bs-success: var(--bs-green); /* Yas, Queen! (Success) */
--bs-info: var(--bs-cyan); /* The Tea (Information) */
--bs-warning: var(--bs-orange); /* Hun, Be Careful (Warning) */
--bs-danger: var(--bs-red); /* Not Today, Satan (Danger) */
--bs-light: var(--bs-gray-100); /* Daytime Disco */
--bs-dark: var(--bs-gray-900); /* Velvet Rope */
/* 📖 Fonts with Personality 📖 */
/* Recommendation: Import "Quicksand" and "Fira Mono" from Google Fonts for the full effect! */
--bs-font-sans-serif: "Quicksand", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: "Fira Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* ✨ The Gradient That Slays ✨ */
--bs-gradient: linear-gradient(93deg, var(--bs-indigo), var(--bs-pink) 50%, var(--bs-orange));
/* 🎨 Core Styles for a Flawless Look 🎨 */
--bs-body-color: var(--bs-gray-800);
--bs-body-bg: var(--bs-white);
--bs-emphasis-color: var(--bs-black);
--bs-secondary-color: rgba(45, 55, 72, 0.75);
--bs-secondary-bg: var(--bs-gray-200);
--bs-tertiary-color: rgba(45, 55, 72, 0.5);
--bs-tertiary-bg: var(--bs-gray-100);
--bs-heading-color: inherit;
--bs-link-color: var(--bs-primary);
--bs-link-hover-color: var(--bs-purple);
--bs-code-color: var(--bs-pink);
--bs-highlight-bg: #fff9c4; /* A soft, buttery highlight */
--bs-border-color: var(--bs-gray-300);
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.5rem; /* A little softer, a little friendlier */
--bs-border-radius-lg: 0.75rem;
--bs-border-radius-sm: 0.25rem;
--bs-box-shadow: 0 0.25rem 1.5rem rgba(45, 55, 72, 0.1);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(45, 55, 72, 0.07);
--bs-box-shadow-lg: 0 1rem 3rem rgba(45, 55, 72, 0.15);
}
/* Dark Mode (Bootstrap's preferred way) */
@media (prefers-color-scheme: dark) {
:root {
--bs-primary: #ff9cd7; /* Pastel Pink */
--bs-secondary: #b39afd; /* Light Lavender */
--bs-success: #8fffd9; /* Lighter Mint */
--bs-info: #90e7ff; /* Pastel Sky Blue */
--bs-warning: #fff4b1; /* Pale Yellow */
--bs-danger: #ffb1b8; /* Soft Coral */
--bs-light: #2a102d; /* Deep Purple-Black */
--bs-dark: #fff7fa; /* Reverse for dark backgrounds */
/*
* =================================================================================================
* 🌙 DARK MODE: VOGUE AFTER MIDNIGHT 🌙
*
* Because fabulousness doesn't sleep. This is for the night owls, the dreamers,
* and everyone who knows the party really starts after dark.
* =================================================================================================
*/
--bs-body-bg: #2a102d;
--bs-body-color: #fff7fa;
--bs-link-color: #ff9cd7;
--bs-link-hover-color: #b39afd;
--bs-border-color: #6e3a6e;
}
}
[data-bs-theme=dark] {
color-scheme: dark;
/* 💅 Dark Mode Neutrals: Still Chic 💅 */
--bs-body-color: var(--bs-gray-300);
--bs-body-bg: var(--bs-gray-900);
--bs-emphasis-color: var(--bs-white);
--bs-secondary-color: rgba(237, 242, 247, 0.75);
--bs-secondary-bg: var(--bs-gray-800);
--bs-tertiary-color: rgba(237, 242, 247, 0.5);
--bs-tertiary-bg: #222938; /* A slightly bluer dark gray */
/* 💖 Semantic Roles: Nightlife Edition 💖 */
/* We use the same vibrant colors but adjust text emphasis and backgrounds for contrast */
--bs-primary-text-emphasis: #A3A0FF;
--bs-secondary-text-emphasis: #FFA8B9;
--bs-success-text-emphasis: #79F3B3;
--bs-info-text-emphasis: #99E5FF;
--bs-warning-text-emphasis: #FFC98B;
--bs-danger-text-emphasis: #FF7B96;
--bs-light-text-emphasis: #F7FAFC;
--bs-dark-text-emphasis: #E2E8F0;
--bs-primary-bg-subtle: #2A2859;
--bs-secondary-bg-subtle: #592432;
--bs-success-bg-subtle: #144A29;
--bs-info-bg-subtle: #194A59;
--bs-warning-bg-subtle: #593918;
--bs-danger-bg-subtle: #591422;
--bs-light-bg-subtle: var(--bs-gray-800);
--bs-dark-bg-subtle: var(--bs-gray-900);
--bs-primary-border-subtle: #4D49A6;
--bs-secondary-border-subtle: #A6435C;
--bs-success-border-subtle: #248047;
--bs-info-border-subtle: #2D8DA6;
--bs-warning-border-subtle: #A66629;
--bs-danger-border-subtle: #A6273D;
--bs-light-border-subtle: var(--bs-gray-700);
--bs-dark-border-subtle: var(--bs-gray-800);
/* 🎨 Core Styles for the Afterparty 🎨 */
--bs-heading-color: inherit;
--bs-link-color: var(--bs-blue);
--bs-link-hover-color: var(--bs-cyan);
--bs-code-color: var(--bs-secondary);
--bs-highlight-bg: #403d07;
--bs-border-color: var(--bs-gray-700);
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
}
/* Top Navigation Bar Styles */
main {
margin-top: 20px;

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,11 @@ namespace App\Controller;
use App\Entity\DrinkType;
use App\Form\DrinkTypeForm;
use App\Form\DrinkTypeFormCurrentStockForm;
use App\Repository\DrinkTypeRepository;
use App\Repository\PropertyChangeLogRepository;
use App\Service\DrinkType\GetStockHistory;
use App\Service\DrinkType\GetWantedHistory;
use App\Service\InventoryService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -18,10 +22,10 @@ use Symfony\Component\Routing\Attribute\Route;
final class DrinkTypeController extends AbstractController
{
#[Route(name: 'app_drink_type_index', methods: ['GET'])]
public function index(InventoryService $inventoryService): Response
public function index(DrinkTypeRepository $drinkTypeRepository): Response
{
return $this->render('drink_type/index.html.twig', [
'drink_stocks' => $inventoryService->getAllDrinkTypesWithStockLevels(true),
'drink_types' => $drinkTypeRepository->findAll(),
]);
}
@ -46,28 +50,20 @@ final class DrinkTypeController extends AbstractController
}
#[Route(path: '/{id}', name: 'app_drink_type_show', methods: ['GET'])]
public function show(DrinkType $drinkType, PropertyChangeLogRepository $propertyChangeLogRepository): Response
public function show(
DrinkType $drinkType,
GetStockHistory $getStockHistory,
GetWantedHistory $getWantedHistory,
): Response
{
// Get orders that contain this drink type
$orderItems = $drinkType->getOrderItems();
// Get inventory history for this drink type
$inventoryRecords = $drinkType->getInventoryRecords();
// Get desired stock history from PropertyChangeLog
$desiredStockHistory = $propertyChangeLogRepository->findBy([
'entityClass' => DrinkType::class,
'propertyName' => 'desiredStock',
'entityId' => $drinkType->getId(),
], [
'changeDate' => 'DESC',
]);
return $this->render('drink_type/show.html.twig', [
'drink_type' => $drinkType,
'order_items' => $orderItems,
'inventory_records' => $inventoryRecords,
'desired_stock_history' => $desiredStockHistory,
'stock_history' => $getStockHistory($drinkType),
'wanted_history' => $getWantedHistory($drinkType),
]);
}
@ -89,6 +85,24 @@ final class DrinkTypeController extends AbstractController
]);
}
#[Route(path: '/{id}/update-stock', name: 'app_drink_type_update_stock', methods: ['GET', 'POST'])]
public function updateStock(Request $request, DrinkType $drinkType, EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(DrinkTypeFormCurrentStockForm::class, $drinkType);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush();
return $this->redirectToRoute('app_index', [], Response::HTTP_SEE_OTHER);
}
return $this->render('drink_type/edit.html.twig', [
'drink_type' => $drinkType,
'form' => $form,
]);
}
#[Route(path: '/{id}', name: 'app_drink_type_delete', methods: ['POST'])]
public function delete(Request $request, DrinkType $drinkType, EntityManagerInterface $entityManager): Response
{

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller;
use App\Repository\DrinkTypeRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@ -11,8 +12,11 @@ use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/', name: 'app_index')]
final class Index extends AbstractController
{
public function __invoke(): Response
public function __invoke(DrinkTypeRepository $drinkTypeRepository): Response
{
return new Response('<h1>Hello World!</h1>');
return $this->render('index.html.twig', [
'drinkTypes' => $drinkTypeRepository->findWanted(),
]);
}
}

View file

@ -14,7 +14,11 @@ class DrinkTypeForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name')->add('description')->add('desiredStock', NumberType::class);
$builder
->add('name')
->add('description')
->add('currentStock', NumberType::class)
->add('wantedStock', NumberType::class);
}
public function configureOptions(OptionsResolver $resolver): void

View file

@ -5,34 +5,23 @@ declare(strict_types=1);
namespace App\Form;
use App\Entity\DrinkType;
use App\Entity\InventoryRecord;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class InventoryRecordForm extends AbstractType
class DrinkTypeFormCurrentStockForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('quantity', NumberType::class)
->add('drinkType', EntityType::class, [
'class' => DrinkType::class,
'choice_label' => 'id',
'attr' => [
'style' => 'display: none;',
],
'label' => false,
])
;
->add('currentStock', NumberType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => InventoryRecord::class,
'data_class' => DrinkType::class,
]);
}
}

View file

@ -10,19 +10,18 @@
<tr>
<th>Name</th>
<th>Description</th>
<th>DesiredStock</th>
<th>DrinkStock</th>
<th>Wanted Stock</th>
<th>Current Stock</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for drink_stock in drink_stocks %}
{% set drink_type = drink_stock.record.drinkType %}
{% for drink_type in drink_types %}
<tr>
<td>{{ drink_type.name }}</td>
<td>{{ drink_type.description }}</td>
<td>{{ drink_type.desiredStock }}</td>
<td>{{ drink_stock.record.quantity }}</td>
<td>{{ drink_type.wantedStock }}</td>
<td>{{ drink_type.currentStock }}</td>
<td>
<a href="{{ path('app_drink_type_show', {'id': drink_type.id}) }}">show</a>
<a href="{{ path('app_drink_type_edit', {'id': drink_type.id}) }}">edit</a>

View file

@ -36,8 +36,12 @@
<td>{{ drink_type.description }}</td>
</tr>
<tr>
<th>DesiredStock</th>
<td>{{ drink_type.desiredStock }}</td>
<th>Current Stock</th>
<td>{{ drink_type.currentStock }}</td>
</tr>
<tr>
<th>Wanted Stock</th>
<td>{{ drink_type.wantedStock }}</td>
</tr>
</tbody>
</table>
@ -87,7 +91,7 @@
<h5 class="card-title">Inventory History</h5>
</div>
<div class="card-body">
{% if inventory_records|length > 0 %}
{% if stock_history|length > 0 %}
<table class="table table-striped">
<thead>
<tr>
@ -96,10 +100,10 @@
</tr>
</thead>
<tbody>
{% for record in inventory_records %}
{% for record in stock_history %}
<tr>
<td>{{ record.createdAt|date('Y-m-d H:i:s') }}</td>
<td>{{ record.quantity }}</td>
<td>{{ record.changeDate|date('Y-m-d H:i:s') }}</td>
<td>{{ record.newValue }}</td>
</tr>
{% endfor %}
</tbody>
@ -119,7 +123,7 @@
<h5 class="card-title">Desired Stock History</h5>
</div>
<div class="card-body">
{% if desired_stock_history|length > 0 %}
{% if wanted_history|length > 0 %}
<table class="table table-striped">
<thead>
<tr>
@ -128,7 +132,7 @@
</tr>
</thead>
<tbody>
{% for log in desired_stock_history %}
{% for log in wanted_history %}
<tr>
<td>{{ log.changeDate|date('Y-m-d H:i:s') }}</td>
<td>{{ log.newValue }}</td>

View file

@ -5,35 +5,6 @@
{% block body %}
<div class="container">
<h1>Drink Inventory</h1>
{% if low is not same as([]) %}
<div class="card">
<div class="card-header">
<h5 class="card-title">Low Stock Alert</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Drink Name</th>
<th>Current Stock</th>
<th>Desired Stock</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for lowStock in low %}
<tr>
<td><a href="{{ path('app_drink_type_show', {'id': lowStock.record.drinkType.id}) }}">{{ lowStock.record.drinkType.name }}</a></td>
<td>{{ lowStock.record.quantity }}</td>
<td>{{ lowStock.record.drinkType.desiredStock }}</td>
<td>{{ lowStock.stock.value|capitalize }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<h5 class="card-title">Drink Inventory Overview</h5>
@ -43,36 +14,22 @@
<thead>
<tr>
<th>Drink Name</th>
<th>Description</th>
<th>Current Stock</th>
<th>Desired Stock</th>
<th>Status</th>
<th>Wanted Stock</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for drinkStock in drinkStocks %}
{% set rowClass = '' %}
{% if drinkStock.stock.value == 'critical' %}
{% set rowClass = 'table-danger' %}
{% elseif drinkStock.stock.value == 'low' %}
{% set rowClass = 'table-warning' %}
{% elseif drinkStock.stock.value == 'high' %}
{% set rowClass = 'table-success' %}
{% endif %}
<tr class="{{ rowClass }}">
<td><a href="{{ path('app_drink_type_show', {'id': drinkStock.record.drinkType.id}) }}">{{ drinkStock.record.drinkType.name }}</a></td>
<td>{{ drinkStock.record.quantity }}</td>
<td>{{ drinkStock.record.drinkType.desiredStock }}</td>
<td>{{ drinkStock.stock.value|capitalize }}</td>
{% for drinkType in drinkTypes %}
<tr>
<td>
<a href="{{ path('app_drink_type_show', {'id': drinkType.id}) }}">{{ drinkType.name }}</a>
</td>
<td>{{ drinkType.description }}</td>
<td>{{ drinkType.currentStock }}</td>
<td>{{ drinkType.wantedStock }}</td>
<td>
<a href="{{ path('app_inventory_record_modal', {drinkType: drinkStock.record.drinkType.id}) }}"
class="btn btn-primary"
hx-get="{{ path('app_inventory_record_modal', {drinkType: drinkStock.record.drinkType.id}) }}"
hx-target="#htmxModalBody"
hx-trigger="click"
data-bs-toggle="modal"
data-bs-target="#htmxModal"
data-drink-name="{{ drinkStock.record.drinkType.name }}">Update Stock</a>
</td>
</tr>
{% endfor %}
@ -81,7 +38,6 @@
<div class="mt-3">
<a href="{{ path('app_drink_type_index') }}" class="btn btn-primary">View All Drinks</a>
</div>
</div>
</div>
</div>

View file

@ -14,5 +14,5 @@ test('Hello World', function (): void {
// Validate a successful response and some content
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello World');
$this->assertSelectorTextContains('h1', 'Drink Inventory');
});