diff --git a/README.md b/README.md index b2b40a64..4ff078e2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The PHP application currently handles most functionality, while the Go applicati ### Quick Start ``` shell -git clone git@code.springupsoftware.com:cmc/cmc-sales.git +git clone git@code.springupsoftware.com:springup/cmc-sales.git cd cmc-sales # Easy way - use the setup script @@ -54,6 +54,28 @@ gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc Both applications share the same database, allowing for gradual migration. +### Database Migrations + +Database schema changes are managed using [Goose](https://github.com/pressly/goose) migrations in the `go/sql/migrations/` directory. + +**Creating a new migration:** +```bash +cd go +make migrate-create name=add_new_column_to_table +``` + +**Running migrations:** +```bash +cd go +make migrate # Apply all pending migrations +make migrate-status # Check migration status +make migrate-down # Rollback last migration +``` + +**Migration files** use the Goose format with `-- +goose Up` and `-- +goose Down` sections. See `go/sql/migrations/` for examples. + +**Configuration:** Database connection settings are in `go/goose.env` (create from `goose.env.example`). + ### Requirements - **Go Application**: Requires Go 1.23+ (for latest sqlc) diff --git a/go/sql/migrations/004_add_archived_to_products.sql b/go/sql/migrations/004_add_archived_to_products.sql new file mode 100644 index 00000000..af8cacab --- /dev/null +++ b/go/sql/migrations/004_add_archived_to_products.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- Add archived field to products table +ALTER TABLE products ADD COLUMN archived TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Product is archived and hidden from main listing'; + +-- +goose Down +-- Remove archived column from products +ALTER TABLE products DROP COLUMN archived; diff --git a/php/app/controllers/products_controller.php b/php/app/controllers/products_controller.php index 441e7334..9f57290b 100755 --- a/php/app/controllers/products_controller.php +++ b/php/app/controllers/products_controller.php @@ -19,10 +19,21 @@ class ProductsController extends AppController { $this->Session->setFlash(__('Invalid Principle ID', true)); $this->redirect(array('action'=>'index')); } - $this->set('products', $this->Product->find('all', array('conditions'=>array('Product.principle_id'=>$id),'order'=>'Product.title ASC'))); + $this->set('products', $this->Product->find('all', array('conditions'=>array('Product.principle_id'=>$id, 'Product.archived'=>0),'order'=>'Product.title ASC'))); $this->set('principle', $this->Product->Principle->findById($id)); } + function view_archived_principle($id = null) { + if(!$id) { + $this->Session->setFlash(__('Invalid Principle ID', true)); + $this->redirect(array('action'=>'index')); + } + $this->set('products', $this->Product->find('all', array('conditions'=>array('Product.principle_id'=>$id, 'Product.archived'=>1),'order'=>'Product.title ASC'))); + $this->set('principle', $this->Product->Principle->findById($id)); + $currentuser = $this->getCurrentUser(); + $this->set('is_admin', $currentuser['User']['access_level'] == 'admin'); + } + function view($id = null) { if (!$id) { $this->Session->setFlash(__('Invalid Product.', true)); @@ -122,9 +133,73 @@ class ProductsController extends AppController { $this->Session->setFlash(__('Invalid id for Product', true)); $this->redirect(array('action'=>'index')); } + + // Check if user is admin + $currentuser = $this->getCurrentUser(); + if($currentuser['User']['access_level'] != 'admin') { + $this->Session->setFlash(__('Only administrators can delete products', true)); + $this->redirect(array('action'=>'index')); + return; + } + + $product = $this->Product->findById($id); + if (!$product) { + $this->Session->setFlash(__('Invalid Product', true)); + $this->redirect(array('action'=>'index')); + return; + } + if ($this->Product->del($id)) { $this->Session->setFlash(__('Product deleted', true)); + $this->redirect(array('action'=>'view_archived_principle', $product['Product']['principle_id'])); + } + } + + function archive($id = null) { + if (!$id) { + $this->Session->setFlash(__('Invalid id for Product', true)); $this->redirect(array('action'=>'index')); + return; + } + + $product = $this->Product->findById($id); + if (!$product) { + $this->Session->setFlash(__('Invalid Product', true)); + $this->redirect(array('action'=>'index')); + return; + } + + $this->Product->id = $id; + if ($this->Product->saveField('archived', 1)) { + $this->Session->setFlash(__('Product archived', true)); + $this->redirect(array('action'=>'view_principle', $product['Product']['principle_id'])); + } else { + $this->Session->setFlash(__('Failed to archive product', true)); + $this->redirect(array('action'=>'view_principle', $product['Product']['principle_id'])); + } + } + + function unarchive($id = null) { + if (!$id) { + $this->Session->setFlash(__('Invalid id for Product', true)); + $this->redirect(array('action'=>'index')); + return; + } + + $product = $this->Product->findById($id); + if (!$product) { + $this->Session->setFlash(__('Invalid Product', true)); + $this->redirect(array('action'=>'index')); + return; + } + + $this->Product->id = $id; + if ($this->Product->saveField('archived', 0)) { + $this->Session->setFlash(__('Product unarchived', true)); + $this->redirect(array('action'=>'view_archived_principle', $product['Product']['principle_id'])); + } else { + $this->Session->setFlash(__('Failed to unarchive product', true)); + $this->redirect(array('action'=>'view_archived_principle', $product['Product']['principle_id'])); } } diff --git a/php/app/views/products/view_archived_principle.ctp b/php/app/views/products/view_archived_principle.ctp new file mode 100644 index 00000000..2978908d --- /dev/null +++ b/php/app/views/products/view_archived_principle.ctp @@ -0,0 +1,42 @@ +
+ +

: Archived Products + + link(__('Back to Active Products', true), array('action'=>'view_principle', $principle['Principle']['id'])); ?> + +

+ + + + + + + + > + + + + +
Title
+ + + link(__('View', true), array('action'=>'view', $product['Product']['id'])); ?> + link(__('Un-Archive', true), array('action'=>'unarchive', $product['Product']['id']), null, sprintf(__('Are you sure you want to un-archive %s?', true), $product['Product']['title'])); ?> + + link(__('Delete', true), array('action'=>'delete', $product['Product']['id']), null, sprintf(__('Are you sure you want to permanently delete %s?', true), $product['Product']['title'])); ?> + +
+ + +
+ + + + diff --git a/php/app/views/products/view_principle.ctp b/php/app/views/products/view_principle.ctp index d1b01083..7ac7130d 100755 --- a/php/app/views/products/view_principle.ctp +++ b/php/app/views/products/view_principle.ctp @@ -1,6 +1,10 @@
-

: Products

+

: Products + + link(__('View Archived Products', true), array('action'=>'view_archived_principle', $principle['Principle']['id'])); ?> + +

@@ -24,6 +28,7 @@ foreach ($products as $product): link(__('View', true), array('action'=>'view', $product['Product']['id'])); ?> link(__('Edit', true), array('action'=>'edit', $product['Product']['id'])); ?> link(__('Create New Product based on this', true), array('action'=>'cloneProduct', $product['Product']['id'])); ?> + link(__('Archive', true), array('action'=>'archive', $product['Product']['id']), null, sprintf(__('Are you sure you want to archive %s?', true), $product['Product']['title'])); ?>
Title
diff --git a/php/app/webroot/css/quotenik.css b/php/app/webroot/css/quotenik.css index 4b78c451..5a4e09e4 100755 --- a/php/app/webroot/css/quotenik.css +++ b/php/app/webroot/css/quotenik.css @@ -618,8 +618,19 @@ td.rightAlign { /* View Products Table */ table.productTable { - width: auto; + width: 100%; +} +table.productTable th { + text-align: center; +} + +table.productTable td { + text-align: left; +} + +table.productTable td.actions { + text-align: right; }