Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions admin/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,16 @@ When adding a new entity, build the files in this order, matching the existing f
- Tables use `public static function table(Table $table): Table` with `->columns([])`, `->filters([])`, `->recordActions([...])`, `->toolbarActions([...])`.
- Actions come from the `Filament\Actions\` namespace (`EditAction`, `CreateAction`, `DeleteAction`, `BulkActionGroup`, `DeleteBulkAction`).
- Import individual components (`Filament\Forms\Components\TextInput`, `Filament\Tables\Columns\TextColumn`), not the parent `Forms`/`Tables` namespaces.
- 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).
- 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')`.
- Rich text uses `AmidEsfahani\FilamentTinyEditor\TinyEditor`.
- Select fields backed by an enum use `->options(SomeEnum::options())` and `->default(SomeEnum::CASE->value)`.
- Table text columns that can be long (headings, relation labels) use `->limit(30)->wrap()`.
- Enum-backed table columns render via `->getStateUsing(fn ($record) => $record->field->label())` and `->color(fn ($record) => $record->field->color())`.
- 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.
- Control navigation order within a group with `protected static ?int $navigationSort = 1;` (lower = higher in the list).
- Add an explanatory subheading to a list page with `protected ?string $subheading = 'Description here.';` on the `ListRecords` page class.
- 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.

## Models

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

## Migrations

- **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.
- Anonymous class style: `return new class extends Migration`.
- 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.
- For a second FK to the same table, use a named column: `$table->foreignId('user_creator_id')->nullable()->constrained('users')->nullOnDelete()` (see `coupons`).
Expand Down Expand Up @@ -255,5 +260,6 @@ When adding a new entity, build the files in this order, matching the existing f

- The implementation status and priority order live in `IMPLEMENTATION.md`. When an entity is finished or the plan changes, update it.
- The full schema reference is `ShoFlow db doc.md`. Treat it as the source of truth for table columns and relationships.
- 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.
- Keep this "ShopFlow Admin Conventions" section updated whenever a new reusable pattern is introduced.

37 changes: 37 additions & 0 deletions admin/CACHE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# ShopFlow Cache Register

Tracks cache keys that have been identified but are not yet implemented.
When a cache is implemented, move it to the **Implemented** section and record the key, driver, TTL, and where it is invalidated.

Legend: `[ ]` not started, `[x]` implemented.

> **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.

---

## Pending (identified, not implemented)

| # | Cache key / pattern | What it caches | Suggested TTL | Invalidated when |
|---|---------------------|----------------|---------------|-----------------|
| 1 | `categories.tree` | Full nested category tree | 1 hour | Category saved / deleted |
| 2 | `banners.{position}` | Published banners for a given position | 30 min | Banner saved / deleted |
| 3 | `menus.{slug}` | Rendered menu tree for a given slug | 1 hour | Menu or MenuItem saved / deleted |
| 4 | `attributes.group.{group_id}` | Attributes belonging to a group | 1 hour | Attribute saved / deleted |
| 5 | `products.slug.{slug}` | Single product record (with images, attributes, brand, category) | 30 min | Product saved / deleted |
| 6 | `products.category.{category_id}` | Published products list for a category page | 15 min | Product saved / deleted |
| 7 | `varieties.product.{product_id}` | All varieties for a product (price, inventory, status) | 15 min | Variety saved / deleted |

---

## Implemented

_None yet._

---

## Rules

- Cache keys use dot notation and include any dynamic segment as `{placeholder}`.
- Tag caches by entity name so bulk-invalidation is possible (e.g. `Cache::tags(['banners'])->flush()`).
- Invalidation logic goes in the model's `booted()` method or a dedicated observer, never in controllers or resources.
- Document every new key here before or when implementing it.
16 changes: 13 additions & 3 deletions admin/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,40 @@ Catalog layer and platform basics.
- [x] Discounts (auto-applied price rules per variety)
- [x] Coupons (+ `coupon_product`, `coupon_variety`, `category_coupon` scoping pivots)
- [x] Images (polymorphic, used via uploads - no standalone resource by design)
- [x] Banners (polymorphic images via `images` table)
- [~] Addresses (model only, no resource yet)

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

Cross-cutting improvements landed:
- Navigation groups reorganized: Catalog / Promotions / Attribute / Content / Address.
- `CACHE.md` added to track identified-but-not-implemented cache keys.

## Phase 0 - Finish current branch (`implement_variety`)

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

- [x] Fix `VarietyResource` table: `product.heading` column (was `product.title` with `->numeric()` on a string)
- [ ] Variety extensions (`warehouse_id`, `guarantee_name_id`, `attribute_id`, `variety_attribute`, `variety_serials`, `variety_details`) - deferred to Phase 3, they need Warehouses / Guarantees first
- [ ] 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
- [x] `attribute_id` FK on varieties - links each variety to one attribute; auto-populates `attribute_value` and `color` from the attribute on save
- [ ] Variety extensions (`warehouse_id`, `guarantee_name_id`, `variety_serials`, `variety_details`) - deferred to Phase 3, they need Warehouses / Guarantees first
- [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

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

Builds on Products/Varieties; prerequisite for Orders.

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

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

Depend mostly on Images only.

- [ ] Banners
- [x] Banners
- [ ] Sliders + Slides
- [ ] Menus + Menu Items
- [ ] Pages
Expand Down
16 changes: 8 additions & 8 deletions admin/ShoFlow db doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,10 @@ Used to store banners.
* `position` specifies the advertisement location, which is an arbitrary name to retrieve the corresponding record from the database.
* `heading` specifies the banner item title or the alt text of the image.
* `url` specifies the item link, which redirects when the image or title is clicked.
* `image_id1` specifies the item's image.
* `image_id2` specifies the item's image.
* `image_id3` specifies the item's image.
* `sort` specifies the item order.
* `status` specifies the publication and draft status of the banner.
* `status` stores the publication status of the banner, with values 10 for deleted, 20 for published, and 30 for draft.

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.

# Brand\_Category

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

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

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

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

* `product_id`: Indicates which product this variety belongs to. Foreign key to `products`; cascades on product delete.
* `attribute_value`: Free-text value of the selected variation (e.g., "Red", "8GB"). Currently not linked to the `attributes` table.
* `color`: Optional color value for the variation.
* `product_id`: Indicates which product this variety belongs to. Foreign key to `products`; cascades on product delete.
* `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.
* `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.
* `color`: Hex or name color for the variation, auto-populated from `attribute.color` when `attribute_id` is set.
* `price`
* `sale_price`
* `inventory`: Number of ShopFlow inventory, the default value is 0\.
Expand Down
34 changes: 34 additions & 0 deletions admin/app/Enums/BannerStatusEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace App\Enums;

use App\Traits\HasOptions;

enum BannerStatusEnum: int
{
use HasOptions;

case DELETED = 10;
case PUBLISHED = 20;
case DRAFT = 30;

public function label(): string
{
return match ($this) {
self::DELETED => 'Deleted',
self::PUBLISHED => 'Published',
self::DRAFT => 'Draft',
};
}

public function color(): string
{
return match ($this) {
self::DELETED => 'danger',
self::PUBLISHED => 'success',
self::DRAFT => 'warning',
};
}
}
2 changes: 2 additions & 0 deletions admin/app/Filament/Resources/AncestorResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class AncestorResource extends Resource

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

protected static ?int $navigationSort = 1;

protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';

public static function form(Schema $schema): Schema
Expand Down
10 changes: 10 additions & 0 deletions admin/app/Filament/Resources/AttributeGroupCategoryResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class AttributeGroupCategoryResource extends Resource

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

protected static ?int $navigationSort = 3;

protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';

public static function form(Schema $schema): Schema
Expand All @@ -33,13 +35,21 @@ public static function form(Schema $schema): Schema
->components([
Select::make('attribute_group_id')
->relationship('attributeGroup', 'name')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('The attribute group to attach to the category (e.g. "Color", "Size").')
->required(),
Select::make('category_id')
->relationship('category', 'title')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('The category this attribute group will be available for.')
->required(),
Toggle::make('as_filter')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('When enabled, this group appears as a filter panel on the category\'s product listing page.')
->required(),
Toggle::make('required')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('When enabled, admins must select at least one attribute from this group before saving a product in this category.')
->required(),
]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ListAttributeGroupCategories extends ListRecords
{
protected static string $resource = AttributeGroupCategoryResource::class;

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.';

protected function getHeaderActions(): array
{
return [
Expand Down
6 changes: 6 additions & 0 deletions admin/app/Filament/Resources/AttributeGroupResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class AttributeGroupResource extends Resource

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

protected static ?int $navigationSort = 2;

protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';

public static function form(Schema $schema): Schema
Expand All @@ -32,11 +34,15 @@ public static function form(Schema $schema): Schema
->components([
Select::make('ancestor_id')
->relationship('ancestor', 'name')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('Groups attribute groups under a common ancestor (e.g. "Physical", "Technical"). Create ancestors first.')
->required(),
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('label')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('The label shown to the customer on the frontend (e.g. "Choose a color"). If empty, the name is used.')
->maxLength(255),
TextInput::make('order')
->numeric(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ListAttributeGroups extends ListRecords
{
protected static string $resource = AttributeGroupResource::class;

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.';

protected function getHeaderActions(): array
{
return [
Expand Down
8 changes: 8 additions & 0 deletions admin/app/Filament/Resources/AttributeResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class AttributeResource extends Resource

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

protected static ?int $navigationSort = 4;

protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';

public static function form(Schema $schema): Schema
Expand All @@ -32,11 +34,17 @@ public static function form(Schema $schema): Schema
->components([
Select::make('attribute_group_id')
->relationship('attributeGroup', 'name')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('The group this attribute belongs to (e.g. "Color", "Size"). Create attribute groups first.')
->required(),
TextInput::make('value')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('The display value of this attribute (e.g. "Red", "XL"). Shown to customers and used as the variety label.')
->required()
->maxLength(255),
TextInput::make('color')
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('Optional hex or color name (e.g. #ff0000). Auto-copied to the variety\'s color when this attribute is selected.')
->nullable()
->maxLength(31),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ListAttributes extends ListRecords
{
protected static string $resource = AttributeResource::class;

protected ?string $subheading = 'Attributes are the individual values inside an attribute group (e.g. "Red", "Blue", "Green" inside "Color"). Each attribute can have an optional color swatch. When linked to a variety via attribute_id, the variety\'s value and color are auto-filled from the attribute.';

protected function getHeaderActions(): array
{
return [
Expand Down
Loading
Loading