forked from lubiana/futtern
Compare commits
4 commits
main
...
tailiwindi
Author | SHA1 | Date | |
---|---|---|---|
4e1ea20fe7 | |||
ab9f1bfeec | |||
b8e9a02a14 | |||
f38c05d97c |
43 changed files with 3038 additions and 1443 deletions
4
.env.dev
4
.env.dev
|
@ -1,4 +0,0 @@
|
|||
|
||||
###> symfony/framework-bundle ###
|
||||
APP_SECRET=11c8937d48993fb3aee1a476413161f5
|
||||
###< symfony/framework-bundle ###
|
|
@ -3,7 +3,7 @@ jobs:
|
|||
ls:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.php.fail/lubiana/container/php:8.4.8-ci
|
||||
image: git.php.fail/lubiana/container/php:8.4.3-ci
|
||||
steps:
|
||||
- name: Manually checkout
|
||||
env:
|
||||
|
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
ls:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.php.fail/lubiana/container/php:8.4.8-ci
|
||||
image: git.php.fail/lubiana/container/php:8.4.3-ci
|
||||
steps:
|
||||
- name: Manually checkout
|
||||
env:
|
||||
|
|
|
@ -4,7 +4,7 @@ jobs:
|
|||
ls:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.php.fail/lubiana/container/php:8.4.8-ci
|
||||
image: git.php.fail/lubiana/container/php:8.4.3-ci
|
||||
steps:
|
||||
- name: Manually checkout
|
||||
env:
|
||||
|
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -18,11 +18,7 @@
|
|||
|
||||
.DS_Store
|
||||
|
||||
###> squizlabs/php_codesniffer ###
|
||||
/.phpcs-cache
|
||||
/phpcs.xml
|
||||
###< squizlabs/php_codesniffer ###
|
||||
|
||||
###> phpstan/phpstan ###
|
||||
phpstan.neon
|
||||
###< phpstan/phpstan ###
|
||||
###> symfony/asset-mapper ###
|
||||
/public/assets/
|
||||
/assets/vendor/
|
||||
###< symfony/asset-mapper ###
|
||||
|
|
156
.junie/guidelines.md
Normal file
156
.junie/guidelines.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Development Guidelines
|
||||
|
||||
This document provides guidelines and instructions for developing and maintaining the Futtern project.
|
||||
|
||||
## Build/Configuration Instructions
|
||||
|
||||
### Environment Setup
|
||||
|
||||
1. **PHP Requirements**: The project requires PHP 8.4 or higher with the following extensions:
|
||||
- ctype
|
||||
- iconv
|
||||
|
||||
2. **Composer**: Install dependencies using Composer:
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
3. **Environment Configuration**: Copy `.env` to `.env.local` and adjust the settings as needed for your local environment.
|
||||
|
||||
### Development Server
|
||||
|
||||
Start the Symfony development server:
|
||||
```bash
|
||||
symfony server:start
|
||||
```
|
||||
|
||||
### Building Assets
|
||||
|
||||
The project uses Tailwind CSS via the symfonycasts/tailwind-bundle:
|
||||
```bash
|
||||
# Install importmap assets
|
||||
symfony console importmap:install
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
The project includes several deployment scripts:
|
||||
|
||||
1. **Prepare for Deployment**:
|
||||
```bash
|
||||
./deploy/prepare-deploy.sh
|
||||
```
|
||||
This script creates a clean copy of the application with only production dependencies.
|
||||
|
||||
2. **Local Deployment**:
|
||||
```bash
|
||||
./deploy/local-deploy.sh
|
||||
```
|
||||
This script deploys the application to a remote server, backing up the database and restarting the service.
|
||||
|
||||
3. **Update After Deployment**:
|
||||
```bash
|
||||
./deploy/update.sh
|
||||
```
|
||||
This script is run on the remote server to clear cache, warm up cache, and run database migrations.
|
||||
|
||||
## Testing Information
|
||||
|
||||
### Testing Framework
|
||||
|
||||
The project uses Pest PHP, a testing framework built on top of PHPUnit, for testing. Tests are organized into:
|
||||
- **Feature Tests**: For testing controllers and API endpoints
|
||||
- **Unit Tests**: For testing individual components
|
||||
|
||||
### Running Tests
|
||||
|
||||
Run all tests:
|
||||
```bash
|
||||
composer test
|
||||
# or
|
||||
./vendor/bin/pest
|
||||
```
|
||||
|
||||
Run specific tests:
|
||||
```bash
|
||||
./vendor/bin/pest tests/Unit/ExampleTest.php
|
||||
```
|
||||
|
||||
Run tests in parallel:
|
||||
```bash
|
||||
./vendor/bin/pest --parallel
|
||||
```
|
||||
|
||||
### Creating Tests
|
||||
|
||||
1. **Unit Tests**: Create files in the `tests/Unit` directory.
|
||||
2. **Feature Tests**:
|
||||
- Controller tests: Create files in `tests/Feature/Controller`
|
||||
- API tests: Create files in `tests/Feature/Api`
|
||||
|
||||
### Example Test
|
||||
|
||||
Here's a simple example of a Pest PHP test:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
test('example test', function (): void {
|
||||
expect(true)->toBeTrue();
|
||||
expect(1 + 1)->toBe(2);
|
||||
expect('hello world')->toContain('world');
|
||||
});
|
||||
|
||||
test('array operations', function (): void {
|
||||
$array = [1, 2, 3];
|
||||
|
||||
expect($array)->toHaveCount(3);
|
||||
expect($array)->toContain(2);
|
||||
expect($array[0])->toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
### Test Base Classes
|
||||
|
||||
- `DbWebTest`: Base class for controller tests
|
||||
- `DbApiTestCase`: Base class for API tests
|
||||
|
||||
## Code Quality and Style
|
||||
|
||||
### Code Style
|
||||
|
||||
The project uses Easy Coding Standard (ECS) for code style checking:
|
||||
```bash
|
||||
composer lint
|
||||
# or
|
||||
./vendor/bin/ecs --fix
|
||||
```
|
||||
|
||||
The code style is defined in `ecs.php` and includes:
|
||||
- Custom rules from `Lubiana\CodeQuality\LubiSetList::ECS`
|
||||
- All classes should be declared as final
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
The project uses Rector for automated code refactoring:
|
||||
```bash
|
||||
./vendor/bin/rector
|
||||
```
|
||||
|
||||
The refactoring rules are defined in `rector.php` and include:
|
||||
- Custom rules from `Lubiana\CodeQuality\LubiSetList::RECTOR`
|
||||
- StaticClosureRector is skipped in the tests directory
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Make changes to the code
|
||||
2. Run tests to ensure functionality: `composer test`
|
||||
3. Fix code style issues: `composer lint`
|
||||
4. Commit and push changes
|
||||
5. Deploy using the deployment scripts
|
||||
|
||||
## Debugging
|
||||
|
||||
- Use the Symfony Web Profiler in development mode
|
||||
- Check logs in `var/log/`
|
||||
- For API debugging, use the API Platform documentation at `/api`
|
3
.symfony.local.yaml
Normal file
3
.symfony.local.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
workers:
|
||||
tailwind:
|
||||
cmd: ['symfony', 'console', 'tailwind:build', '--watch']
|
8
assets/app.js
Normal file
8
assets/app.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* This file will be included onto the page via the importmap() Twig function,
|
||||
* which should already be in your base.html.twig.
|
||||
*/
|
||||
import './styles/app.css';
|
||||
|
7
assets/styles/app.css
Normal file
7
assets/styles/app.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
background-color: skyblue;
|
||||
}
|
|
@ -7,46 +7,47 @@
|
|||
"php": ">=8.4",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"api-platform/doctrine-orm": "*",
|
||||
"api-platform/symfony": "*",
|
||||
"doctrine/dbal": "^4.2.3",
|
||||
"doctrine/doctrine-bundle": "^2.14.1",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.4.2",
|
||||
"doctrine/orm": "^3.4.0",
|
||||
"api-platform/doctrine-orm": "^4.0.0",
|
||||
"api-platform/symfony": "4.1.4",
|
||||
"doctrine/dbal": "^4.1",
|
||||
"doctrine/doctrine-bundle": "^2.12",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.3.1",
|
||||
"doctrine/orm": "^3.2.1",
|
||||
"nelmio/cors-bundle": "^2.5",
|
||||
"phpdocumentor/reflection-docblock": "^5.6.2",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
"phpstan/phpdoc-parser": "^1.33",
|
||||
"psr/clock": "^1.0",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/expression-language": "7.3.*",
|
||||
"symfony/flex": "^2.7.1",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/security-csrf": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/uid": "7.3.*",
|
||||
"symfony/validator": "7.3.*",
|
||||
"symfony/yaml": "7.3.*"
|
||||
"symfony/asset": "7.2.*",
|
||||
"symfony/console": "7.1.*",
|
||||
"symfony/dotenv": "7.1.*",
|
||||
"symfony/expression-language": "7.2.*",
|
||||
"symfony/flex": "^2.4.6",
|
||||
"symfony/form": "7.1.*",
|
||||
"symfony/framework-bundle": "7.1.*",
|
||||
"symfony/property-access": "7.2.*",
|
||||
"symfony/property-info": "7.2.*",
|
||||
"symfony/runtime": "7.1.*",
|
||||
"symfony/security-bundle": "7.2.*",
|
||||
"symfony/security-csrf": "7.1.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/twig-bundle": "7.1.*",
|
||||
"symfony/uid": "7.1.*",
|
||||
"symfony/validator": "7.1.*",
|
||||
"symfony/yaml": "7.1.*",
|
||||
"symfonycasts/tailwind-bundle": "^0.10.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.1",
|
||||
"liip/test-fixtures-bundle": "^3.4",
|
||||
"doctrine/doctrine-fixtures-bundle": "^4.0",
|
||||
"liip/test-fixtures-bundle": "^3.2",
|
||||
"lubiana/code-quality": "^1.7.2",
|
||||
"pestphp/pest": "^3.8.2",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/http-client": "7.3.*",
|
||||
"symfony/maker-bundle": "^1.63",
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*",
|
||||
"symplify/config-transformer": "^12.4.0"
|
||||
"pestphp/pest": "^3.6",
|
||||
"symfony/browser-kit": "7.2.*",
|
||||
"symfony/css-selector": "7.2.*",
|
||||
"symfony/http-client": "7.2.*",
|
||||
"symfony/maker-bundle": "^1.60",
|
||||
"symfony/stopwatch": "7.2.*",
|
||||
"symfony/web-profiler-bundle": "7.2.*",
|
||||
"symplify/config-transformer": "^12.3.4"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
@ -80,13 +81,13 @@
|
|||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*",
|
||||
"symfony/polyfill-php83": "*",
|
||||
"symfony/polyfill-php84": "*"
|
||||
"symfony/polyfill-php83": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||
"importmap:install": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
|
@ -107,7 +108,7 @@
|
|||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.3.*"
|
||||
"require": "7.2.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3170
composer.lock
generated
3170
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,7 @@ use Symfony\Bundle\MakerBundle\MakerBundle;
|
|||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
use Symfony\Bundle\TwigBundle\TwigBundle;
|
||||
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
|
||||
use Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle;
|
||||
|
||||
return [
|
||||
FrameworkBundle::class => [
|
||||
|
@ -42,11 +43,14 @@ return [
|
|||
NelmioCorsBundle::class => [
|
||||
'all' => true,
|
||||
],
|
||||
ApiPlatformBundle::class => [
|
||||
'all' => true,
|
||||
],
|
||||
LiipTestFixturesBundle::class => [
|
||||
'dev' => true,
|
||||
'test' => true,
|
||||
],
|
||||
ApiPlatformBundle::class => [
|
||||
SymfonycastsTailwindBundle::class => [
|
||||
'all' => true,
|
||||
],
|
||||
];
|
||||
|
|
|
@ -4,8 +4,10 @@ use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigura
|
|||
|
||||
return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
$containerConfigurator->extension('api_platform', [
|
||||
'title' => 'Hello API Platform',
|
||||
'title' => 'Futtern API',
|
||||
'version' => '1.0.0',
|
||||
'show_webby' => false,
|
||||
'enable_swagger' => true,
|
||||
'defaults' => [
|
||||
'stateless' => true,
|
||||
'cache_headers' => [
|
||||
|
|
21
config/packages/asset_mapper.php
Normal file
21
config/packages/asset_mapper.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
$containerConfigurator->extension('framework', [
|
||||
'asset_mapper' => [
|
||||
'paths' => [
|
||||
'assets/',
|
||||
],
|
||||
'missing_import_mode' => 'strict',
|
||||
],
|
||||
]);
|
||||
if ($containerConfigurator->env() === 'prod') {
|
||||
$containerConfigurator->extension('framework', [
|
||||
'asset_mapper' => [
|
||||
'missing_import_mode' => 'warn',
|
||||
],
|
||||
]);
|
||||
}
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
$containerConfigurator->extension('framework', [
|
||||
'form' => [
|
||||
'csrf_protection' => [
|
||||
'token_id' => 'submit',
|
||||
],
|
||||
],
|
||||
'csrf_protection' => [
|
||||
'stateless_token_ids' => [
|
||||
'submit',
|
||||
'authenticate',
|
||||
'logout',
|
||||
],
|
||||
],
|
||||
]);
|
||||
};
|
|
@ -3,9 +3,7 @@
|
|||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
$containerConfigurator->extension('framework', [
|
||||
'property_info' => [
|
||||
'with_constructor_extractor' => true,
|
||||
],
|
||||
$containerConfigurator->extension('symfonycasts_tailwind', [
|
||||
'binary_version' => 'v3.4.17',
|
||||
]);
|
||||
};
|
19
importmap.php
Normal file
19
importmap.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Returns the importmap for this application.
|
||||
*
|
||||
* - "path" is a path inside the asset mapper system. Use the
|
||||
* "debug:asset-map" command to see the full list of paths.
|
||||
*
|
||||
* - "entrypoint" (JavaScript only) set to true for any module that will
|
||||
* be used as an "entrypoint" (and passed to the importmap() Twig function).
|
||||
*
|
||||
* The "importmap:require" command can be used to add new entries to this file.
|
||||
*/
|
||||
return [
|
||||
'app' => [
|
||||
'path' => './assets/app.js',
|
||||
'entrypoint' => true,
|
||||
],
|
||||
];
|
20
php-styler.php
Normal file
20
php-styler.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use PhpStyler\Config;
|
||||
use PhpStyler\Files;
|
||||
use PhpStyler\Styler;
|
||||
|
||||
return new Config(
|
||||
styler: new Styler(lineLen: 79),
|
||||
files: new Files(
|
||||
__DIR__ . '/bin',
|
||||
__DIR__ . '/public',
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/config',
|
||||
__DIR__ . '/tests',
|
||||
__DIR__ . '/php-styler.php',
|
||||
__DIR__ . '/ecs.php',
|
||||
__DIR__ . '/rector.php',
|
||||
),
|
||||
cache: __DIR__ . '/.php-styler.cache',
|
||||
);
|
|
@ -3,14 +3,9 @@
|
|||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\FoodOrderRepository;
|
||||
use App\State\OpenOrdersProvider;
|
||||
use App\State\LatestOrderProvider;
|
||||
use App\State\OpenFoodOrderProvider;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
@ -21,26 +16,12 @@ use Symfony\Component\Uid\Ulid;
|
|||
|
||||
use function iterator_to_array;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: 'food_orders/open',
|
||||
description: 'Get only open orders',
|
||||
provider: OpenOrdersProvider::class,
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: 'food_orders/latest',
|
||||
description: 'Get the latest created order',
|
||||
provider: LatestOrderProvider::class,
|
||||
),
|
||||
new GetCollection,
|
||||
new Get,
|
||||
new Post,
|
||||
new Put,
|
||||
new Delete,
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: FoodOrderRepository::class)]
|
||||
#[GetCollection(
|
||||
uriTemplate: '/food_orders/open',
|
||||
provider: OpenFoodOrderProvider::class,
|
||||
)]
|
||||
#[ApiResource]
|
||||
class FoodOrder
|
||||
{
|
||||
#[ORM\Column(nullable: true)]
|
||||
|
|
|
@ -12,8 +12,8 @@ use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
|
|||
use Symfony\Bridge\Doctrine\Types\UlidType;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
#[ApiResource]
|
||||
#[ORM\Entity(repositoryClass: MenuItemRepository::class)]
|
||||
#[ApiResource]
|
||||
class MenuItem
|
||||
{
|
||||
#[ORM\Column(length: 255)]
|
||||
|
|
|
@ -9,8 +9,8 @@ use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator;
|
|||
use Symfony\Bridge\Doctrine\Types\UlidType;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
#[ApiResource]
|
||||
#[ORM\Entity(repositoryClass: OrderItemRepository::class)]
|
||||
#[ApiResource]
|
||||
class OrderItem
|
||||
{
|
||||
#[ORM\Column(length: 255)]
|
||||
|
|
|
@ -62,16 +62,4 @@ final class FoodOrderRepository extends ServiceEntityRepository
|
|||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FoodOrder|null
|
||||
*/
|
||||
public function findLatestOrder(): ?FoodOrder
|
||||
{
|
||||
return $this->createQueryBuilder('alias')
|
||||
->orderBy('alias.id', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\FoodOrder;
|
||||
use App\Repository\FoodOrderRepository;
|
||||
use Override;
|
||||
|
||||
final readonly class LatestOrderProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private FoodOrderRepository $repository
|
||||
) {}
|
||||
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?FoodOrder
|
||||
{
|
||||
return $this->repository->findLatestOrder();
|
||||
}
|
||||
}
|
|
@ -4,21 +4,17 @@ namespace App\State;
|
|||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\FoodOrder;
|
||||
use App\Repository\FoodOrderRepository;
|
||||
use Override;
|
||||
|
||||
final readonly class OpenOrdersProvider implements ProviderInterface
|
||||
final readonly class OpenFoodOrderProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private FoodOrderRepository $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return FoodOrder[]
|
||||
*/
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
return $this->repository->findOpenOrders();
|
||||
}
|
133
symfony.lock
133
symfony.lock
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"api-platform/core": {
|
||||
"version": "4.1",
|
||||
"api-platform/symfony": {
|
||||
"version": "4.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "4.0",
|
||||
"ref": "cb9e6b8ceb9b62f32d41fc8ad72a25d5bd674c6d"
|
||||
"ref": "e9952e9f393c2d048f10a78f272cd35e807d972b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/api_platform.yaml",
|
||||
|
@ -13,22 +13,13 @@
|
|||
"src/ApiResource/.gitignore"
|
||||
]
|
||||
},
|
||||
"doctrine/deprecations": {
|
||||
"version": "1.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
|
||||
}
|
||||
},
|
||||
"doctrine/doctrine-bundle": {
|
||||
"version": "2.14",
|
||||
"version": "2.12",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.13",
|
||||
"ref": "620b57f496f2e599a6015a9fa222c2ee0a32adcb"
|
||||
"version": "2.12",
|
||||
"ref": "7266981c201efbbe02ae53c87f8bb378e3f825ae"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/doctrine.yaml",
|
||||
|
@ -37,7 +28,7 @@
|
|||
]
|
||||
},
|
||||
"doctrine/doctrine-fixtures-bundle": {
|
||||
"version": "4.1",
|
||||
"version": "4.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -49,7 +40,7 @@
|
|||
]
|
||||
},
|
||||
"doctrine/doctrine-migrations-bundle": {
|
||||
"version": "3.4",
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -62,7 +53,7 @@
|
|||
]
|
||||
},
|
||||
"liip/test-fixtures-bundle": {
|
||||
"version": "3.4.0"
|
||||
"version": "3.2.1"
|
||||
},
|
||||
"nelmio/cors-bundle": {
|
||||
"version": "2.5",
|
||||
|
@ -77,46 +68,54 @@
|
|||
]
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "1.12",
|
||||
"version": "1.11",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
|
||||
},
|
||||
"files": [
|
||||
"phpstan.dist.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "11.5",
|
||||
"version": "11.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "11.1",
|
||||
"ref": "c6658a60fc9d594805370eacdf542c3d6b5c0869"
|
||||
"version": "9.6",
|
||||
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"phpunit.xml.dist",
|
||||
"tests/bootstrap.php",
|
||||
"bin/phpunit"
|
||||
"tests/bootstrap.php"
|
||||
]
|
||||
},
|
||||
"squizlabs/php_codesniffer": {
|
||||
"version": "3.13",
|
||||
"version": "3.10",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "3.6",
|
||||
"ref": "1019e5c08d4821cb9b77f4891f8e9c31ff20ac6f"
|
||||
}
|
||||
},
|
||||
"symfony/asset-mapper": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
|
||||
},
|
||||
"files": [
|
||||
"phpcs.xml.dist"
|
||||
"assets/app.js",
|
||||
"assets/styles/app.css",
|
||||
"config/packages/asset_mapper.yaml",
|
||||
"importmap.php"
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -128,37 +127,24 @@
|
|||
]
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "2.7",
|
||||
"version": "2.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.4",
|
||||
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||
"version": "1.0",
|
||||
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
|
||||
},
|
||||
"files": [
|
||||
".env",
|
||||
".env.dev"
|
||||
]
|
||||
},
|
||||
"symfony/form": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.2",
|
||||
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/csrf.yaml"
|
||||
".env"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "5a1497d539f691b96afd45ae397ce5fe30beb4b9"
|
||||
"version": "7.0",
|
||||
"ref": "6356c19b9ae08e7763e4ba2d9ae63043efc75db5"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/cache.yaml",
|
||||
|
@ -168,12 +154,11 @@
|
|||
"config/services.yaml",
|
||||
"public/index.php",
|
||||
"src/Controller/.gitignore",
|
||||
"src/Kernel.php",
|
||||
".editorconfig"
|
||||
"src/Kernel.php"
|
||||
]
|
||||
},
|
||||
"symfony/maker-bundle": {
|
||||
"version": "1.63",
|
||||
"version": "1.60",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -181,20 +166,8 @@
|
|||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||
}
|
||||
},
|
||||
"symfony/property-info": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/property_info.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -207,7 +180,7 @@
|
|||
]
|
||||
},
|
||||
"symfony/security-bundle": {
|
||||
"version": "7.3",
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -220,7 +193,7 @@
|
|||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -233,7 +206,7 @@
|
|||
]
|
||||
},
|
||||
"symfony/uid": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -242,7 +215,7 @@
|
|||
}
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
|
@ -254,16 +227,28 @@
|
|||
]
|
||||
},
|
||||
"symfony/web-profiler-bundle": {
|
||||
"version": "7.3",
|
||||
"version": "7.1",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "7.3",
|
||||
"ref": "5b2b543e13942495c0003f67780cb4448af9e606"
|
||||
"version": "6.1",
|
||||
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/web_profiler.yaml",
|
||||
"config/routes/web_profiler.yaml"
|
||||
]
|
||||
},
|
||||
"symfonycasts/tailwind-bundle": {
|
||||
"version": "0.10",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "0.8",
|
||||
"ref": "4ea7c9488fdce8943520daf3fdc31e93e5b59c64"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/symfonycasts_tailwind.yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
12
tailwind.config.js
Normal file
12
tailwind.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./assets/**/*.js",
|
||||
"./templates/**/*.html.twig",
|
||||
],
|
||||
darkMode: 'media', // Use 'media' to respect the user's system preference
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -1,4 +1,24 @@
|
|||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<button class="btn">{{ button_label|default('Save') }}</button>
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
|
||||
{% for field in form %}
|
||||
{% if field.vars.name != '_token' %}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900 dark:text-gray-100'}}) }}
|
||||
<div>
|
||||
{{ form_widget(field, {'attr': {'class': 'block w-full rounded-md border-0 py-1.5 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:ring-2 focus:ring-inset focus:ring-indigo-600 dark:focus:ring-indigo-500 sm:text-sm sm:leading-6'}}) }}
|
||||
</div>
|
||||
{% if field.vars.errors|length > 0 %}
|
||||
<div class="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{{ form_errors(field) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.vars.help is defined and field.vars.help %}
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ field.vars.help }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="mt-6 flex items-center justify-end gap-x-6">
|
||||
<a href="{{ app.request.headers.get('referer', '/') }}" class="text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100">Cancel</a>
|
||||
<button type="submit" class="rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500">{{ button_label|default('Save') }}</button>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
|
|
@ -1,46 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html class="h-full bg-gray-100 dark:bg-gray-900">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="theme-color" content="#0000ff" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#222222" media="(prefers-color-scheme: dark)">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml"
|
||||
href="{{ favicon }}" />
|
||||
{% set currentDate = "now"|date("d") %}
|
||||
{% if currentDate % 4 == 0 %}
|
||||
<link rel="stylesheet" href="/static/css/new.min.css">
|
||||
{% elseif currentDate % 4 == 1 %}
|
||||
<link rel="stylesheet" href="/static/css/simple.min.css">
|
||||
{% elseif currentDate % 4 == 2 %}
|
||||
<link rel="stylesheet" href="/static/css/water.min.css">
|
||||
{% else %}
|
||||
<link rel="stylesheet" href="/static/css/fieber.css">
|
||||
{% endif %}
|
||||
<style>
|
||||
label{
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<title>{% block title %}Welcome!{% endblock %} - Futtern</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ favicon }}" />
|
||||
<script src="/static/js/htmx.min.js"></script>
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
|
||||
{% endblock %}
|
||||
{% block javascripts %}
|
||||
{{ importmap() }}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<p>Hello {{ app.request.cookies.get('username', 'nobody') }} - <a href="{{ path('username') }}">change name</a></p>
|
||||
<nav>
|
||||
<a href="{{ path('app_food_order_index') }}">Orders</a> /
|
||||
<a href="{{ path('app_food_vendor_index') }}">Vendors</a> /
|
||||
<a
|
||||
href="https://git.hannover.ccc.de/lubiana/futtern/issues/new"
|
||||
target="_blank"
|
||||
>Create Issue</a> /
|
||||
<a href="/api">API</a>
|
||||
<body class="h-full dark:text-gray-100">
|
||||
<div class="min-h-full">
|
||||
<nav class="bg-indigo-600 dark:bg-indigo-800">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-white text-xl font-bold">Futtern</span>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="ml-10 flex items-baseline space-x-4">
|
||||
<a href="{{ path('app_food_order_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">Orders</a>
|
||||
<a href="{{ path('app_food_vendor_index') }}" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">Vendors</a>
|
||||
<a href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">Create Issue</a>
|
||||
<a href="/api" class="text-white hover:bg-indigo-500 hover:bg-opacity-75 dark:hover:bg-indigo-700 rounded-md px-3 py-2 text-sm font-medium">API</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="ml-4 flex items-center md:ml-6">
|
||||
<div class="text-white">
|
||||
Hello {{ app.request.cookies.get('username', 'nobody') }} -
|
||||
<a href="{{ path('username') }}" class="text-indigo-200 hover:text-white">change name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-2 flex md:hidden">
|
||||
<!-- Mobile menu button -->
|
||||
<button type="button" class="relative inline-flex items-center justify-center rounded-md bg-indigo-600 dark:bg-indigo-800 p-2 text-indigo-200 hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-indigo-600 dark:focus:ring-offset-indigo-800" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="absolute -inset-0.5"></span>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Menu open: "hidden", Menu closed: "block" -->
|
||||
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu, show/hide based on menu state. -->
|
||||
<div class="md:hidden" id="mobile-menu">
|
||||
<div class="space-y-1 px-2 pb-3 pt-2 sm:px-3">
|
||||
<a href="{{ path('app_food_order_index') }}" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Orders</a>
|
||||
<a href="{{ path('app_food_vendor_index') }}" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Vendors</a>
|
||||
<a href="https://git.hannover.ccc.de/lubiana/futtern/issues/new" target="_blank" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Create Issue</a>
|
||||
<a href="/api" class="text-white hover:bg-indigo-500 dark:hover:bg-indigo-700 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">API</a>
|
||||
</div>
|
||||
<div class="border-t border-indigo-700 pb-3 pt-4">
|
||||
<div class="flex items-center px-5">
|
||||
<div class="text-base font-medium text-white">
|
||||
Hello {{ app.request.cookies.get('username', 'nobody') }} -
|
||||
<a href="{{ path('username') }}" class="text-indigo-200 hover:text-white">change name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<header class="bg-white dark:bg-gray-800 shadow">
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">{% block header %}{% endblock %}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
@ -1,45 +1,69 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}FoodOrder index{% endblock %}
|
||||
{% block title %}Food Orders{% endblock %}
|
||||
|
||||
{% block header %}Food Orders{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>FoodOrder index</h1>
|
||||
<div>
|
||||
<button
|
||||
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage your food orders and create new ones.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<button
|
||||
hx-get="{{ path('app_food_order_new') }}"
|
||||
hx-trigger="click"
|
||||
hx-target="closest div"
|
||||
>Create new</button>
|
||||
hx-target="#new-order-form"
|
||||
class="block rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500"
|
||||
>Create new order</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CreatedBy</th>
|
||||
<th>Vendor</th>
|
||||
<th>CreatedAt</th>
|
||||
<th>ClosedAt</th>
|
||||
<th>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for food_order in food_orders %}
|
||||
{{ include('food_order/table_row.html.twig') }}
|
||||
{% endfor %}
|
||||
{% if food_orders|length < 10 %}
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
check the <a href="{{ path('app_food_order_archive') }}">archive</a>
|
||||
for older orders
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<div id="new-order-form" class="mb-8"></div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">Created By</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Vendor</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Created At</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Closed At</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
|
||||
{% for food_order in food_orders %}
|
||||
{{ include('food_order/table_row.html.twig') }}
|
||||
{% endfor %}
|
||||
{% if food_orders|length < 10 %}
|
||||
<tr>
|
||||
<td colspan="5" class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-500 dark:text-gray-400 sm:pl-6">
|
||||
Check the <a href="{{ path('app_food_order_archive') }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">archive</a>
|
||||
for older orders
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-center gap-x-6">
|
||||
{% if prev_page > 0 %}
|
||||
<a href="{{ path('app_food_order_archive', {'page': prev_page}) }}" class="rounded-md bg-white dark:bg-gray-700 px-3.5 py-2.5 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">Previous Page</a>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if prev_page > 0 %}
|
||||
<a href="{{ path('app_food_order_archive', {'page': prev_page}) }}">previous page</a> |
|
||||
{% endif %}
|
||||
{% if next_page > current_page %}
|
||||
<a href="{{ path('app_food_order_archive', {'page': next_page}) }}">next page</a>
|
||||
{% endif %}
|
||||
{% if next_page > current_page %}
|
||||
<a href="{{ path('app_food_order_archive', {'page': next_page}) }}" class="rounded-md bg-white dark:bg-gray-700 px-3.5 py-2.5 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">Next Page</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,2 +1,11 @@
|
|||
{{ include('_form.html.twig') }}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">Create New Order</h3>
|
||||
<div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
|
||||
<p>Fill out the form below to create a new food order.</p>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
{{ include('_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,71 +1,101 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}FoodOrder{% endblock %}
|
||||
{% block title %}Order Details{% endblock %}
|
||||
|
||||
{% block header %}Order Details{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>FoodOrder</h1>
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg mb-8">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Order Information</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">Details about the food order from {{ food_order.foodVendor.name }}.</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700">
|
||||
<dl>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Vendor</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.foodVendor.name }}</dd>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Vendor Phone</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.foodVendor.phone }}</dd>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Created By</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.createdBy }}</dd>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Created At</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</dd>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-300">Closed At</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 sm:col-span-2 sm:mt-0">{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : 'Not closed yet' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Vendor</th>
|
||||
<td>{{ food_order.foodVendor.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Vendorphone</th>
|
||||
<td>{{ food_order.foodVendor.phone }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By</th>
|
||||
<td>{{ food_order.createdBy }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>CreatedAt</th>
|
||||
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>ClosedAt</th>
|
||||
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<a class="button" href="{{ path('app_food_order_index') }}">back to list</a>
|
||||
{% if(food_order.isClosed) %}
|
||||
<a class="button" href="{{ path('app_food_order_open', {'id': food_order.id}) }}">reopen</a>
|
||||
{% else %}
|
||||
<a class="button" href="{{ path('app_food_order_close', {'id': food_order.id}) }}">close</a>
|
||||
{% endif %}
|
||||
|
||||
<h2>Items</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Index</th>
|
||||
<th>username</th>
|
||||
<th>name</th>
|
||||
<th>extras</th>
|
||||
<th>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in food_order.orderItemsSortedByName %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ item.createdBy }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.extras }}</td>
|
||||
<td>
|
||||
{% if(food_order.isClosed) %}
|
||||
{% else %}
|
||||
<a href="{{ path('app_order_item_edit', {'id': item.id}) }}">edit</a>
|
||||
<a href="{{ path('app_order_item_copy', {'id': item.id}) }}">copy</a>
|
||||
<a href="{{ path('app_order_item_delete', {'id': item.id}) }}">remove</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a class="button" href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}">New Item</a>
|
||||
<div class="flex space-x-4 mb-8">
|
||||
<a href="{{ path('app_food_order_index') }}" class="inline-flex items-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
Back to list
|
||||
</a>
|
||||
{% if(food_order.isClosed) %}
|
||||
<a href="{{ path('app_food_order_open', {'id': food_order.id}) }}" class="inline-flex items-center rounded-md bg-green-600 dark:bg-green-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 dark:hover:bg-green-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600 dark:focus-visible:outline-green-500">
|
||||
Reopen Order
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_food_order_close', {'id': food_order.id}) }}" class="inline-flex items-center rounded-md bg-red-600 dark:bg-red-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 dark:hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:focus-visible:outline-red-500">
|
||||
Close Order
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg mb-8">
|
||||
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Order Items</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">Items included in this order.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ path('app_order_item_new', {'foodOrder': food_order.id}) }}" class="inline-flex items-center rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500">
|
||||
Add New Item
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10">
|
||||
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">#</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Username</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Item Name</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Extras</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
|
||||
{% for item in food_order.orderItemsSortedByName %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">{{ loop.index }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ item.createdBy }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ item.name }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ item.extras }}</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
{% if(food_order.isClosed) %}
|
||||
<span class="text-gray-400 dark:text-gray-500">Order closed</span>
|
||||
{% else %}
|
||||
<a href="{{ path('app_order_item_edit', {'id': item.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2">Edit</a>
|
||||
<a href="{{ path('app_order_item_copy', {'id': item.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2">Copy</a>
|
||||
<a href="{{ path('app_order_item_delete', {'id': item.id}) }}" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Remove</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<tr>
|
||||
<td>{{ food_order.createdBy }}</td>
|
||||
<td>{{ food_order.foodVendor.name }}</td>
|
||||
<td>{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
|
||||
<td>{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_food_order_show', {'id': food_order.id}) }}">show</a>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">{{ food_order.createdBy }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ food_order.foodVendor.name }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ food_order.createdAt ? food_order.createdAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ food_order.closedAt ? food_order.closedAt|date('Y-m-d H:i:s', 'Europe/Berlin') : '' }}</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<a href="{{ path('app_food_order_show', {'id': food_order.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">View<span class="sr-only">, order from {{ food_order.createdBy }}</span></a>
|
||||
</td>
|
||||
</tr>
|
|
@ -1,4 +1,24 @@
|
|||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<button class="btn">{{ button_label|default('Save') }}</button>
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
|
||||
{% for field in form %}
|
||||
{% if field.vars.name != '_token' %}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(field, null, {'label_attr': {'class': 'block text-sm font-medium leading-6 text-gray-900'}}) }}
|
||||
<div>
|
||||
{{ form_widget(field, {'attr': {'class': 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6'}}) }}
|
||||
</div>
|
||||
{% if field.vars.errors|length > 0 %}
|
||||
<div class="mt-1 text-sm text-red-600">
|
||||
{{ form_errors(field) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.vars.help is defined and field.vars.help %}
|
||||
<p class="mt-1 text-sm text-gray-500">{{ field.vars.help }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="mt-6 flex items-center justify-end gap-x-6">
|
||||
<a href="{{ app.request.headers.get('referer', '/') }}" class="text-sm font-semibold leading-6 text-gray-900">Cancel</a>
|
||||
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">{{ button_label|default('Save') }}</button>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
|
|
@ -1,33 +1,54 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}FoodVendor index{% endblock %}
|
||||
{% block title %}Food Vendors{% endblock %}
|
||||
|
||||
{% block header %}Food Vendors{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>FoodVendor index</h1>
|
||||
<div class="sm:flex sm:items-center sm:justify-between mb-6">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage your food vendors and create new ones.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:ml-4 sm:mt-0">
|
||||
<a href="{{ path('app_food_vendor_new') }}" class="block rounded-md bg-indigo-600 dark:bg-indigo-700 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500">
|
||||
Create new vendor
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for food_vendor in food_vendors %}
|
||||
<tr>
|
||||
<td>{{ food_vendor.name }}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}">show</a>
|
||||
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}">edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3">no records found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="{{ path('app_food_vendor_new') }}">Create new</a>
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">Name</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
|
||||
{% for food_vendor in food_vendors %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">{{ food_vendor.name }}</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<a href="{{ path('app_food_vendor_show', {'id': food_vendor.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-2">View</a>
|
||||
<a href="{{ path('app_food_vendor_edit', {'id': food_vendor.id}) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2" class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-500 dark:text-gray-400 sm:pl-6">No vendors found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,8 +2,15 @@
|
|||
|
||||
{% block title %}Tell me your name{% endblock %}
|
||||
|
||||
{% block header %}Tell me your name{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Tell me your name</h1>
|
||||
<p>By submitting the form, you agree that your username will be stored as a cookie.</p>
|
||||
{{ include('_form.html.twig') }}
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="max-w-xl text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
<p>By submitting the form, you agree that your username will be stored as a cookie.</p>
|
||||
</div>
|
||||
{{ include('_form.html.twig') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
26
tests/DbApiTestCase.php
Normal file
26
tests/DbApiTestCase.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
|
||||
use ApiPlatform\Symfony\Bundle\Test\Client;
|
||||
use App\DataFixtures\AppFixtures;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;
|
||||
use Override;
|
||||
|
||||
abstract class DbApiTestCase extends ApiTestCase
|
||||
{
|
||||
protected EntityManagerInterface $manager;
|
||||
protected Client $client;
|
||||
|
||||
#[Override]
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->client = static::createClient();
|
||||
$this->manager = static::getContainer()->get('doctrine')->getManager();
|
||||
$toolKit = self::getContainer()->get(DatabaseToolCollection::class)->get();
|
||||
$toolKit->loadFixtures([AppFixtures::class]);
|
||||
}
|
||||
}
|
48
tests/Feature/Api/ApiSmokeTest.php
Normal file
48
tests/Feature/Api/ApiSmokeTest.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
test('orders', function (): void {
|
||||
$response = $this->client->request('GET', '/api/food_orders');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
|
||||
$this->assertJsonContains([
|
||||
'@context' => '/api/contexts/FoodOrder',
|
||||
'@id' => '/api/food_orders',
|
||||
'@type' => 'Collection',
|
||||
'totalItems' => 1,
|
||||
]);
|
||||
$array = $response->toArray();
|
||||
expect($array['member'][0]['orderItems'])->toHaveCount(10);
|
||||
});
|
||||
|
||||
test('open orders', function (): void {
|
||||
$response = $this->client->request('GET', '/api/food_orders/open');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
|
||||
$this->assertJsonContains([
|
||||
'@context' => '/api/contexts/FoodOrder',
|
||||
'@id' => '/api/food_orders/open',
|
||||
'@type' => 'Collection',
|
||||
]);
|
||||
|
||||
// Get the response content and verify that all returned orders are open
|
||||
$array = $response->toArray();
|
||||
|
||||
// If we have no items, we should still have a successful response
|
||||
if ($array['totalItems'] === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For each order in the response, check that it is not closed
|
||||
foreach ($array['member'] as $order) {
|
||||
// An order is considered open if either:
|
||||
// 1. closedAt is null or
|
||||
// 2. closedAt is in the future
|
||||
$closedAt = isset($order['closedAt']) ? new DateTimeImmutable($order['closedAt']) : null;
|
||||
$now = new DateTimeImmutable;
|
||||
|
||||
// Assert that the order is open (not closed)
|
||||
$isOpen = (! $closedAt instanceof DateTimeImmutable || $closedAt > $now);
|
||||
expect($isOpen)
|
||||
->toBeTrue('Order should be open but is closed');
|
||||
}
|
||||
});
|
|
@ -44,7 +44,7 @@ describe(FoodOrderController::class, function (): void {
|
|||
|
||||
$crawler = $this->client->request('GET', "{$this->path}list");
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$this->assertPageTitleContains('FoodOrder index');
|
||||
$this->assertPageTitleContains('Food Orders');
|
||||
$this->assertCount(
|
||||
1,
|
||||
$crawler->filter('td')
|
||||
|
@ -99,17 +99,29 @@ describe(FoodOrderController::class, function (): void {
|
|||
|
||||
$crawler = $this->client->request('GET', "{$this->path}{$order->getId()}");
|
||||
$this->assertResponseIsSuccessful();
|
||||
$tdContent = $crawler->filter(
|
||||
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(3)'
|
||||
)->text();
|
||||
|
||||
// Find all tables and get the last one (order items table)
|
||||
$tables = $crawler->filter('table');
|
||||
$lastTable = $tables->eq($tables->count() - 1);
|
||||
|
||||
// Get the item names from the table rows
|
||||
$rows = $lastTable->filter('tbody tr');
|
||||
$tdContent = $rows->eq(0)
|
||||
->filter('td')
|
||||
->eq(2)
|
||||
->text();
|
||||
$this->assertEquals('A', $tdContent);
|
||||
$tdContent = $crawler->filter(
|
||||
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(2) > td:nth-child(3)'
|
||||
)->text();
|
||||
|
||||
$tdContent = $rows->eq(1)
|
||||
->filter('td')
|
||||
->eq(2)
|
||||
->text();
|
||||
$this->assertEquals('B', $tdContent);
|
||||
$tdContent = $crawler->filter(
|
||||
'table.table:nth-child(6) > tbody:nth-child(2) > tr:nth-child(3) > td:nth-child(3)'
|
||||
)->text();
|
||||
|
||||
$tdContent = $rows->eq(2)
|
||||
->filter('td')
|
||||
->eq(2)
|
||||
->text();
|
||||
$this->assertEquals('C', $tdContent);
|
||||
});
|
||||
|
||||
|
@ -124,12 +136,12 @@ describe(FoodOrderController::class, function (): void {
|
|||
$this->manager->flush();
|
||||
$crawler = $this->client->request('GET', "{$this->path}list");
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$this->assertPageTitleContains('FoodOrder index');
|
||||
$this->assertPageTitleContains('Food Orders');
|
||||
$this->assertElementContainsCount(
|
||||
$crawler,
|
||||
'td',
|
||||
1,
|
||||
'older orders'
|
||||
'for older orders'
|
||||
);
|
||||
$this->assertElementContainsCount(
|
||||
$crawler,
|
||||
|
@ -139,7 +151,7 @@ describe(FoodOrderController::class, function (): void {
|
|||
);
|
||||
});
|
||||
|
||||
test('paginatedFirstPage', function (int $page, int $prevPage, int $nextPage, int $items = 10): void {
|
||||
test('paginatedFirstPage', function (int $page, int $prevPage, int $nextPage, int $items = 20): void {
|
||||
foreach (range(1, 35) as $i) {
|
||||
$order = new FoodOrder($this->generateOldUlid());
|
||||
$order->setFoodVendor($this->vendor);
|
||||
|
@ -150,7 +162,7 @@ describe(FoodOrderController::class, function (): void {
|
|||
$this->manager->flush();
|
||||
$crawler = $this->client->request('GET', "{$this->path}list/archive/{$page}");
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$this->assertPageTitleContains('FoodOrder index');
|
||||
$this->assertPageTitleContains('Food Orders');
|
||||
$this->assertElementContainsCount(
|
||||
$crawler,
|
||||
'td',
|
||||
|
@ -160,14 +172,14 @@ describe(FoodOrderController::class, function (): void {
|
|||
if ($prevPage > 0) {
|
||||
$prevPage = $prevPage === 1 ? '' : "/{$prevPage}";
|
||||
$node = $crawler->filter('a')
|
||||
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'previous page')
|
||||
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'Previous Page')
|
||||
->first();
|
||||
$target = $node->attr('href');
|
||||
$this->assertTrue(str_ends_with((string) $target, $prevPage));
|
||||
}
|
||||
if ($prevPage > 3) {
|
||||
$node = $crawler->filter('a')
|
||||
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'next page')
|
||||
->reduce(static fn(Crawler $node, $i): bool => $node->text() === 'Next Page')
|
||||
->first();
|
||||
$target = $node->attr('href');
|
||||
$this->assertTrue(str_ends_with((string) $target, "/{$nextPage}"));
|
||||
|
@ -178,7 +190,7 @@ describe(FoodOrderController::class, function (): void {
|
|||
[1, 0, 2],
|
||||
[2, 1, 3],
|
||||
[3, 2, 4],
|
||||
[4, 3, 0, 5],
|
||||
[4, 3, 0, 10],
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -231,30 +243,6 @@ describe(FoodOrderController::class, function (): void {
|
|||
$this->assertTrue($openOrder->isClosed());
|
||||
});
|
||||
|
||||
test('api_latest_order', function (): void {
|
||||
// Create an older order
|
||||
$olderOrder = new FoodOrder($this->generateOldUlid());
|
||||
$olderOrder->setFoodVendor($this->vendor);
|
||||
$this->manager->persist($olderOrder);
|
||||
|
||||
// Create the latest order
|
||||
$latestOrder = new FoodOrder;
|
||||
$latestOrder->setFoodVendor($this->vendor);
|
||||
$this->manager->persist($latestOrder);
|
||||
|
||||
$this->manager->flush();
|
||||
|
||||
// Test the API endpoint
|
||||
$this->client->request('GET', '/api/food_orders/latest');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$this->assertIsArray($response);
|
||||
$this->assertArrayHasKey('@id', $response);
|
||||
$this->assertStringContainsString($latestOrder->getId()->__toString(), $response['@id']);
|
||||
});
|
||||
|
||||
})
|
||||
->covers(
|
||||
FoodOrderController::class,
|
||||
|
|
|
@ -29,7 +29,7 @@ describe(FoodVendorController::class, function (): void {
|
|||
$this->client->request('GET', $this->path);
|
||||
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$this->assertPageTitleContains('FoodVendor index');
|
||||
$this->assertPageTitleContains('Food Vendors');
|
||||
});
|
||||
|
||||
test('new', function (): void {
|
||||
|
|
|
@ -136,10 +136,9 @@ describe(MenuItemController::class, function (): void {
|
|||
$this->assertTrue($menuItem->isDeleted());
|
||||
|
||||
$crawler = $this->client->request('GET', '/order/item/new/' . $order->getId());
|
||||
$count = $crawler->filter('body > main:nth-child(2) > div:nth-child(5)')
|
||||
->children()
|
||||
$count = $crawler->filter('form')
|
||||
->count();
|
||||
$this->assertSame(2, $count);
|
||||
$this->assertSame(1, $count);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
|
|
|
@ -60,8 +60,7 @@ describe(OrderItemController::class, function (): void {
|
|||
sprintf('%snew/%s', $this->path, $this->order->getId())
|
||||
);
|
||||
|
||||
$children = $crawler->filter('body > main:nth-child(2) > div:nth-child(5)')
|
||||
->children();
|
||||
$children = $crawler->filter('form');
|
||||
|
||||
$this->assertCount(1, $children);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
use App\Tests\DbApiTestCase;
|
||||
use App\Tests\DbWebTest;
|
||||
|
||||
/*
|
||||
|
@ -15,6 +16,8 @@ use App\Tests\DbWebTest;
|
|||
|
||||
pest()
|
||||
->extends(DbWebTest::class)->in('Feature/Controller/*.php');
|
||||
pest()
|
||||
->extends(DbApiTestCase::class)->in('Feature/Api/*.php');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
21
tests/Unit/ExampleTest.php
Normal file
21
tests/Unit/ExampleTest.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
test('example test', function (): void {
|
||||
// This is a simple example test
|
||||
expect(true)
|
||||
->toBeTrue();
|
||||
expect(1 + 1)
|
||||
->toBe(2);
|
||||
expect('hello world')
|
||||
->toContain('world');
|
||||
});
|
||||
|
||||
test('array operations', function (): void {
|
||||
$array = [1, 2, 3];
|
||||
|
||||
expect($array)
|
||||
->toHaveCount(3);
|
||||
expect($array)
|
||||
->toContain(2);
|
||||
expect($array[0])->toBe(1);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue