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'])); ?>
+
+
| Title |
@@ -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'])); ?>
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;
}