Skip to content

Commit 17e4db0

Browse files
authored
Merge pull request #30 from bahman026/implement_banners
Implement banners
2 parents e622bc1 + a085c95 commit 17e4db0

54 files changed

Lines changed: 50334 additions & 5090 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

admin/AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,16 @@ When adding a new entity, build the files in this order, matching the existing f
194194
- Tables use `public static function table(Table $table): Table` with `->columns([])`, `->filters([])`, `->recordActions([...])`, `->toolbarActions([...])`.
195195
- Actions come from the `Filament\Actions\` namespace (`EditAction`, `CreateAction`, `DeleteAction`, `BulkActionGroup`, `DeleteBulkAction`).
196196
- Import individual components (`Filament\Forms\Components\TextInput`, `Filament\Tables\Columns\TextColumn`), not the parent `Forms`/`Tables` namespaces.
197+
- For reactive `->options()` or `->live()` closures that receive `Get $get`, import `Filament\Schemas\Components\Utilities\Get` (NOT `Filament\Forms\Get` - that will throw a type error at runtime).
197198
- Page classes set `protected static string $resource = {Name}Resource::class;`. List pages expose `CreateAction::make()` in `getHeaderActions()`. Create and Edit pages redirect with `getRedirectUrl(): string` returning `$this->getResource()::getUrl('index')`.
198199
- Rich text uses `AmidEsfahani\FilamentTinyEditor\TinyEditor`.
199200
- Select fields backed by an enum use `->options(SomeEnum::options())` and `->default(SomeEnum::CASE->value)`.
200201
- Table text columns that can be long (headings, relation labels) use `->limit(30)->wrap()`.
201202
- Enum-backed table columns render via `->getStateUsing(fn ($record) => $record->field->label())` and `->color(fn ($record) => $record->field->color())`.
202203
- Manage many-to-many pivots with a relationship multi-select: `Select::make('products')->relationship('products', 'heading')->multiple()->searchable()->preload()` (see `CouponResource`). No separate resource for pure scoping pivots.
204+
- Control navigation order within a group with `protected static ?int $navigationSort = 1;` (lower = higher in the list).
205+
- Add an explanatory subheading to a list page with `protected ?string $subheading = 'Description here.';` on the `ListRecords` page class.
206+
- Add a tooltip to a form field with `->hintIcon('heroicon-o-information-circle')->hintIconTooltip('Explanation...')`. Use this instead of always-visible `->hint()` when the text is long.
203207

204208
## Models
205209

@@ -218,6 +222,7 @@ When adding a new entity, build the files in this order, matching the existing f
218222

219223
## Migrations
220224

225+
- **Development rule**: NEVER create a new migration to add a column to a table that already has a migration in this branch. Update the existing `create_*` migration directly to keep history clean. After editing, run `php artisan migrate:fresh` inside the container to re-apply everything from scratch. Only create additive migrations when the table already exists in production.
221226
- Anonymous class style: `return new class extends Migration`.
222227
- Use `$table->foreignIdFor(Model::class)` for foreign keys (add `->nullable()` when optional). Chain `->constrained()->cascadeOnDelete()` / `->nullOnDelete()` / `->restrictOnDelete()` to add the real FK constraint with its delete rule.
223228
- For a second FK to the same table, use a named column: `$table->foreignId('user_creator_id')->nullable()->constrained('users')->nullOnDelete()` (see `coupons`).
@@ -255,5 +260,6 @@ When adding a new entity, build the files in this order, matching the existing f
255260

256261
- The implementation status and priority order live in `IMPLEMENTATION.md`. When an entity is finished or the plan changes, update it.
257262
- The full schema reference is `ShoFlow db doc.md`. Treat it as the source of truth for table columns and relationships.
263+
- Cache keys that have been identified but not yet implemented are tracked in `CACHE.md`. When adding a model whose data is likely to be cached (products, categories, banners, menus, etc.), check `CACHE.md` and add or update the relevant rows.
258264
- Keep this "ShopFlow Admin Conventions" section updated whenever a new reusable pattern is introduced.
259265

admin/CACHE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# ShopFlow Cache Register
2+
3+
Tracks cache keys that have been identified but are not yet implemented.
4+
When a cache is implemented, move it to the **Implemented** section and record the key, driver, TTL, and where it is invalidated.
5+
6+
Legend: `[ ]` not started, `[x]` implemented.
7+
8+
> **Note:** `variety_counts` on `products` is NOT a cache. It is a denormalized DB column kept in sync by `Variety::booted()` (saved/deleted events calling `syncProductVarietyCount()`). No cache entry needed for it.
9+
10+
---
11+
12+
## Pending (identified, not implemented)
13+
14+
| # | Cache key / pattern | What it caches | Suggested TTL | Invalidated when |
15+
|---|---------------------|----------------|---------------|-----------------|
16+
| 1 | `categories.tree` | Full nested category tree | 1 hour | Category saved / deleted |
17+
| 2 | `banners.{position}` | Published banners for a given position | 30 min | Banner saved / deleted |
18+
| 3 | `menus.{slug}` | Rendered menu tree for a given slug | 1 hour | Menu or MenuItem saved / deleted |
19+
| 4 | `attributes.group.{group_id}` | Attributes belonging to a group | 1 hour | Attribute saved / deleted |
20+
| 5 | `products.slug.{slug}` | Single product record (with images, attributes, brand, category) | 30 min | Product saved / deleted |
21+
| 6 | `products.category.{category_id}` | Published products list for a category page | 15 min | Product saved / deleted |
22+
| 7 | `varieties.product.{product_id}` | All varieties for a product (price, inventory, status) | 15 min | Variety saved / deleted |
23+
24+
---
25+
26+
## Implemented
27+
28+
_None yet._
29+
30+
---
31+
32+
## Rules
33+
34+
- Cache keys use dot notation and include any dynamic segment as `{placeholder}`.
35+
- Tag caches by entity name so bulk-invalidation is possible (e.g. `Cache::tags(['banners'])->flush()`).
36+
- Invalidation logic goes in the model's `booted()` method or a dedicated observer, never in controllers or resources.
37+
- Document every new key here before or when implementing it.

admin/IMPLEMENTATION.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,40 @@ Catalog layer and platform basics.
2424
- [x] Discounts (auto-applied price rules per variety)
2525
- [x] Coupons (+ `coupon_product`, `coupon_variety`, `category_coupon` scoping pivots)
2626
- [x] Images (polymorphic, used via uploads - no standalone resource by design)
27+
- [x] Banners (polymorphic images via `images` table)
2728
- [~] Addresses (model only, no resource yet)
2829

2930
Sample data for manual admin testing lives in `TestSeeder` (`php artisan db:seed --class=TestSeeder`); `DatabaseSeeder` holds only necessary data.
3031

32+
Cross-cutting improvements landed:
33+
- Navigation groups reorganized: Catalog / Promotions / Attribute / Content / Address.
34+
- `CACHE.md` added to track identified-but-not-implemented cache keys.
35+
3136
## Phase 0 - Finish current branch (`implement_variety`)
3237

3338
Done. Variety has model, migration, factory, resource (+ pages), tests, and `variety_counts` auto-sync.
3439

3540
- [x] Fix `VarietyResource` table: `product.heading` column (was `product.title` with `->numeric()` on a string)
36-
- [ ] Variety extensions (`warehouse_id`, `guarantee_name_id`, `attribute_id`, `variety_attribute`, `variety_serials`, `variety_details`) - deferred to Phase 3, they need Warehouses / Guarantees first
37-
- [ ] Attribute `as_filter` / `required` flags into the product/category flow (open `// todo` in the attribute-group-category migration) - moved to its own branch; it is an attribute/category feature, not variety
41+
- [x] `attribute_id` FK on varieties - links each variety to one attribute; auto-populates `attribute_value` and `color` from the attribute on save
42+
- [ ] Variety extensions (`warehouse_id`, `guarantee_name_id`, `variety_serials`, `variety_details`) - deferred to Phase 3, they need Warehouses / Guarantees first
43+
- [x] Attribute `required` flag enforced in `ProductResource`: if a category has required attribute groups, saving a product without them shows a danger notification and skips the sync
3844

3945
## Phase 1 - Pricing & promotions (recommended next)
4046

4147
Builds on Products/Varieties; prerequisite for Orders.
4248

4349
- [x] Discounts (auto-applied price rules per variety)
4450
- [x] Coupons (+ `coupon_product`, `coupon_variety`, `category_coupon` scoping pivots)
51+
- [ ] Cron jobs for Discounts & Coupons:
52+
- Auto-expire discounts when `ended_at` is passed (set status or filter in queries)
53+
- Auto-expire coupons when `expired_at` is passed
54+
- Optionally: reset `sold` / `total_used` counters on a schedule if needed
4555

4656
## Phase 2 - CMS / content (independent quick wins)
4757

4858
Depend mostly on Images only.
4959

50-
- [ ] Banners
60+
- [x] Banners
5161
- [ ] Sliders + Slides
5262
- [ ] Menus + Menu Items
5363
- [ ] Pages

admin/ShoFlow db doc.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,10 @@ Used to store banners.
9494
* `position` specifies the advertisement location, which is an arbitrary name to retrieve the corresponding record from the database.
9595
* `heading` specifies the banner item title or the alt text of the image.
9696
* `url` specifies the item link, which redirects when the image or title is clicked.
97-
* `image_id1` specifies the item's image.
98-
* `image_id2` specifies the item's image.
99-
* `image_id3` specifies the item's image.
10097
* `sort` specifies the item order.
101-
* `status` specifies the publication and draft status of the banner.
98+
* `status` stores the publication status of the banner, with values 10 for deleted, 20 for published, and 30 for draft.
99+
100+
Images are attached through the polymorphic `images` table (`imageable_type` / `imageable_id`), so a banner can have one or more images instead of fixed `image_id` columns. The featured image is the one with `is_featured` set to true.
102101

103102
# Brand\_Category
104103

@@ -374,7 +373,7 @@ In short: discounts are automatic, per-variety, condition-based price rules. The
374373

375374
* Stores images.
376375
* `path`: Stores the relative path of the images, the absolute path can be specified using the `static_asset` function relative to the CDN.
377-
* `imageable_type:` used for polymorphic relation Attributes Brand Categories
376+
* `imageable_type:` used for polymorphic relation Attributes, Brand, Categories, Products, Banners
378377
* `imageable_id:` used for polymorphic relation
379378
* `created_at`: Specifies the record creation date.
380379

@@ -818,9 +817,10 @@ For storing user permissions and removing role-based permissions.
818817

819818
Contains product variations entered by the seller on the site. This table creates a record depending on the selected variation and price.
820819

821-
* `product_id`: Indicates which product this variety belongs to. Foreign key to `products`; cascades on product delete.
822-
* `attribute_value`: Free-text value of the selected variation (e.g., "Red", "8GB"). Currently not linked to the `attributes` table.
823-
* `color`: Optional color value for the variation.
820+
* `product_id`: Indicates which product this variety belongs to. Foreign key to `products`; cascades on product delete.
821+
* `attribute_id`: Links to the `attributes` table - the specific attribute that defines this variety (e.g. "Red", "XL"). Nullable FK; set to null when the attribute is deleted. When set, `attribute_value` and `color` are auto-populated from the linked attribute via model saving event.
822+
* `attribute_value`: Display label for the variation, auto-populated from `attribute.value` when `attribute_id` is set. Can also be set manually when no attribute is linked.
823+
* `color`: Hex or name color for the variation, auto-populated from `attribute.color` when `attribute_id` is set.
824824
* `price`
825825
* `sale_price`
826826
* `inventory`: Number of ShopFlow inventory, the default value is 0\.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Enums;
6+
7+
use App\Traits\HasOptions;
8+
9+
enum BannerStatusEnum: int
10+
{
11+
use HasOptions;
12+
13+
case DELETED = 10;
14+
case PUBLISHED = 20;
15+
case DRAFT = 30;
16+
17+
public function label(): string
18+
{
19+
return match ($this) {
20+
self::DELETED => 'Deleted',
21+
self::PUBLISHED => 'Published',
22+
self::DRAFT => 'Draft',
23+
};
24+
}
25+
26+
public function color(): string
27+
{
28+
return match ($this) {
29+
self::DELETED => 'danger',
30+
self::PUBLISHED => 'success',
31+
self::DRAFT => 'warning',
32+
};
33+
}
34+
}

admin/app/Filament/Resources/AncestorResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class AncestorResource extends Resource
2323

2424
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
2525

26+
protected static ?int $navigationSort = 1;
27+
2628
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
2729

2830
public static function form(Schema $schema): Schema

admin/app/Filament/Resources/AttributeGroupCategoryResource.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class AttributeGroupCategoryResource extends Resource
2525

2626
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
2727

28+
protected static ?int $navigationSort = 3;
29+
2830
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
2931

3032
public static function form(Schema $schema): Schema
@@ -33,13 +35,21 @@ public static function form(Schema $schema): Schema
3335
->components([
3436
Select::make('attribute_group_id')
3537
->relationship('attributeGroup', 'name')
38+
->hintIcon('heroicon-o-information-circle')
39+
->hintIconTooltip('The attribute group to attach to the category (e.g. "Color", "Size").')
3640
->required(),
3741
Select::make('category_id')
3842
->relationship('category', 'title')
43+
->hintIcon('heroicon-o-information-circle')
44+
->hintIconTooltip('The category this attribute group will be available for.')
3945
->required(),
4046
Toggle::make('as_filter')
47+
->hintIcon('heroicon-o-information-circle')
48+
->hintIconTooltip('When enabled, this group appears as a filter panel on the category\'s product listing page.')
4149
->required(),
4250
Toggle::make('required')
51+
->hintIcon('heroicon-o-information-circle')
52+
->hintIconTooltip('When enabled, admins must select at least one attribute from this group before saving a product in this category.')
4353
->required(),
4454
]);
4555
}

admin/app/Filament/Resources/AttributeGroupCategoryResource/Pages/ListAttributeGroupCategories.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class ListAttributeGroupCategories extends ListRecords
1212
{
1313
protected static string $resource = AttributeGroupCategoryResource::class;
1414

15+
protected ?string $subheading = 'Links an attribute group to a category. This tells the system which attributes are relevant for products in that category. Use "As Filter" to show the group as a filter panel on the category page, and "Required" to force admins to pick an attribute from this group when creating a product in that category.';
16+
1517
protected function getHeaderActions(): array
1618
{
1719
return [

admin/app/Filament/Resources/AttributeGroupResource.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class AttributeGroupResource extends Resource
2424

2525
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
2626

27+
protected static ?int $navigationSort = 2;
28+
2729
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
2830

2931
public static function form(Schema $schema): Schema
@@ -32,11 +34,15 @@ public static function form(Schema $schema): Schema
3234
->components([
3335
Select::make('ancestor_id')
3436
->relationship('ancestor', 'name')
37+
->hintIcon('heroicon-o-information-circle')
38+
->hintIconTooltip('Groups attribute groups under a common ancestor (e.g. "Physical", "Technical"). Create ancestors first.')
3539
->required(),
3640
TextInput::make('name')
3741
->required()
3842
->maxLength(255),
3943
TextInput::make('label')
44+
->hintIcon('heroicon-o-information-circle')
45+
->hintIconTooltip('The label shown to the customer on the frontend (e.g. "Choose a color"). If empty, the name is used.')
4046
->maxLength(255),
4147
TextInput::make('order')
4248
->numeric(),

admin/app/Filament/Resources/AttributeGroupResource/Pages/ListAttributeGroups.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class ListAttributeGroups extends ListRecords
1212
{
1313
protected static string $resource = AttributeGroupResource::class;
1414

15+
protected ?string $subheading = 'An attribute group is a set of attributes that share a common property (e.g. "Color" contains Red, Blue, Green). Each group belongs to an ancestor and is linked to categories via Attribute Group Categories. Products then use a group to define their variety differentiator.';
16+
1517
protected function getHeaderActions(): array
1618
{
1719
return [

0 commit comments

Comments
 (0)