|  | package SL::Controller::PartsGroup;
 | 
  
    |  | 
 | 
  
    |  | use strict;
 | 
  
    |  | 
 | 
  
    |  | use parent qw(SL::Controller::Base);
 | 
  
    |  | 
 | 
  
    |  | use SL::DB::PartsGroup;
 | 
  
    |  | use SL::Helper::Flash;
 | 
  
    |  | use SL::Locale::String;
 | 
  
    |  | use List::MoreUtils qw(any);
 | 
  
    |  | 
 | 
  
    |  | use Rose::Object::MakeMethods::Generic (
 | 
  
    |  |   scalar                  => [ qw(partsgroup) ],
 | 
  
    |  |   # 'scalar --get_set_init' => [ qw(defaults) ],
 | 
  
    |  | );
 | 
  
    |  | 
 | 
  
    |  | __PACKAGE__->run_before('check_auth');
 | 
  
    |  | __PACKAGE__->run_before('load_partsgroup', only => [ qw(edit update delete) ]);
 | 
  
    |  | 
 | 
  
    |  | sub check_auth {
 | 
  
    |  |   $::auth->assert('config');
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | #
 | 
  
    |  | # actions
 | 
  
    |  | #
 | 
  
    |  | 
 | 
  
    |  | sub action_list {
 | 
  
    |  |   my ($self, %params) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->setup_list_action_bar;
 | 
  
    |  |   $self->render('partsgroup/list',
 | 
  
    |  |                  title => $::locale->text('Partsgroups'),
 | 
  
    |  |                  PARTSGROUPS => SL::DB::Manager::PartsGroup->get_hierarchy,
 | 
  
    |  |                );
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_sort_roots {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   # Only for sorting the root partsgroup, and adding new ones.
 | 
  
    |  |   # The simple arrows don't work on the main hierarchy view, if all subgroups
 | 
  
    |  |   # are also shown. You would need a proper drag&drop Module for this.
 | 
  
    |  | 
 | 
  
    |  |   my @root_partsgroups = grep { $_->{level} == 0 } @{ SL::DB::Manager::PartsGroup->get_hierarchy };
 | 
  
    |  | 
 | 
  
    |  |   $self->setup_show_sort_action_bar;
 | 
  
    |  |   $self->render(
 | 
  
    |  |     'partsgroup/sort_roots',
 | 
  
    |  |     title       => t8('Edit partsgroups'),
 | 
  
    |  |     PARTSGROUPS => \@root_partsgroups,
 | 
  
    |  |   );
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_new {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->partsgroup(SL::DB::PartsGroup->new());
 | 
  
    |  |   $self->show_form(title => t8('Add partsgroup'));
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_edit {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->show_form(title       => t8('Edit partsgroup'),
 | 
  
    |  |                    PARTSGROUPS => SL::DB::Manager::PartsGroup->get_hierarchy, # for dropsdown to move parts
 | 
  
    |  |                    PARTS       => $self->partsgroup->parts,
 | 
  
    |  |                   );
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_create {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->partsgroup(SL::DB::PartsGroup->new());
 | 
  
    |  |   $self->create_or_update;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_update {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->create_or_update;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_delete {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->partsgroup->db->with_transaction(sub {
 | 
  
    |  |     $self->partsgroup->delete();
 | 
  
    |  |     flash_later('info',  $::locale->text('The partsgroup has been deleted.'));
 | 
  
    |  | 
 | 
  
    |  |     1;
 | 
  
    |  |   }) || flash_later('error', $::locale->text('The partsgroup is in use and cannot be deleted.'));
 | 
  
    |  | 
 | 
  
    |  |   $self->redirect_to(action => 'list');
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | #
 | 
  
    |  | # ajax actions
 | 
  
    |  | #
 | 
  
    |  | 
 | 
  
    |  | sub action_reorder {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   SL::DB::PartsGroup->reorder_list(@{ $::form->{partsgroup_id} || [] });
 | 
  
    |  | 
 | 
  
    |  |   $self->render(\'', { type => 'json' });
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_add_partsgroup {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   unless ( $::form->{partsgroup_name} ) {
 | 
  
    |  |     return $self->js->flash('error', t8("The name must not be empty."))
 | 
  
    |  |                     ->render;
 | 
  
    |  |   };
 | 
  
    |  | 
 | 
  
    |  |   # check that name doesn't already exist in this grouping, catch before db constraint
 | 
  
    |  |   if ( SL::DB::Manager::PartsGroup->get_all_count(
 | 
  
    |  |     where => [ parent_id  => $::form->{parent_id} // undef,
 | 
  
    |  |                partsgroup => $::form->{partsgroup_name},
 | 
  
    |  |              ]) ) {
 | 
  
    |  |     return $self->js->flash('error', t8("A partsgroup with this name already exists."))
 | 
  
    |  |                     ->focus('#new_partsgroup')
 | 
  
    |  |                     ->render;
 | 
  
    |  |   };
 | 
  
    |  | 
 | 
  
    |  |   my %partsgroup_params = (
 | 
  
    |  |     partsgroup => $::form->{partsgroup_name},
 | 
  
    |  |   );
 | 
  
    |  | 
 | 
  
    |  |   $partsgroup_params{parent_id} = $::form->{parent_id} if $::form->{parent_id};
 | 
  
    |  | 
 | 
  
    |  |   my $new_partsgroup = SL::DB::PartsGroup->new(%partsgroup_params);
 | 
  
    |  |   $new_partsgroup->add_to_list(position => 'last');
 | 
  
    |  | 
 | 
  
    |  |   $self->_render_subgroups_table_body;
 | 
  
    |  |   return $self->js->val('#new_partsgroup', '')
 | 
  
    |  |                   ->flash('info', t8("Added partsgroup."))
 | 
  
    |  |                   ->focus('#new_partsgroup')
 | 
  
    |  |                   ->render;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  | sub action_add_part {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $main::lxdebug->dump(0, "add_part form", $::form );
 | 
  
    |  | 
 | 
  
    |  |   return $self->js->flash('error', t8("No part was selected."))->render
 | 
  
    |  |     unless $::form->{part_id};
 | 
  
    |  | 
 | 
  
    |  |   my $number_of_updated_parts = SL::DB::Manager::Part->update_all (
 | 
  
    |  |     set   => { partsgroup_id => $::form->{partsgroup_id} },
 | 
  
    |  |     where => [ id => $::form->{part_id},
 | 
  
    |  |                '!partsgroup_id' => $::form->{partsgroup_id}, # ignore updating to same partsgroup_id
 | 
  
    |  |              ]
 | 
  
    |  |   );
 | 
  
    |  | 
 | 
  
    |  |   if ( $number_of_updated_parts == 1 ) {
 | 
  
    |  |     $self->_render_parts_table_body; # needs $::form->{partsgroup_id}
 | 
  
    |  |     return $self->js->val('#add_part_id', undef)
 | 
  
    |  |                     ->val('#add_part_id_name', '')
 | 
  
    |  |                     ->flash('info', t8("Added part to partsgroup."))
 | 
  
    |  |                     ->render;
 | 
  
    |  |   } else {
 | 
  
    |  |     return $self->js->flash('error', t8("Part wasn't added to partsgroup!"))->render;
 | 
  
    |  |   }
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub action_update_partsgroup_for_parts{
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $main::lxdebug->dump(0, "update_partsgroup", $::form );
 | 
  
    |  | 
 | 
  
    |  |   # change type and design of existing parts to an existing part_variant
 | 
  
    |  |   # updates part_variant_map entries and lemper_part.type_id and lemper_part.design
 | 
  
    |  | 
 | 
  
    |  |   # the id of the partsgroup we are moving parts from is $::form->{current_partsgroup_id}
 | 
  
    |  | 
 | 
  
    |  |   return $self->js->flash('error', t8("No parts selected."))->render      unless $::form->{part_ids};
 | 
  
    |  |   return $self->js->flash('error', t8("No partsgroup selected."))->render unless $::form->{selected_partsgroup_id};
 | 
  
    |  | 
 | 
  
    |  |   # don't delete partsgroup ids from form, needed by _render_parts_table_body
 | 
  
    |  |   # TODO: better error handling than die, use flash?
 | 
  
    |  |   my $partsgroup = SL::DB::Manager::PartsGroup->find_by(         id => $::form->{selected_partsgroup_id} ) // die 'selected partsgroup id not valid';
 | 
  
    |  |   my $current_partsgroup = SL::DB::Manager::PartsGroup->find_by( id => $::form->{current_partsgroup_id} )  // die 'not a valid partsgroup id';
 | 
  
    |  | 
 | 
  
    |  |   my $part_ids = $::form->{part_ids} // undef;
 | 
  
    |  |   if ( scalar @{ $part_ids } ) {
 | 
  
    |  |     my $parts_updated_count = 0;
 | 
  
    |  |     $current_partsgroup->db->with_transaction(sub {
 | 
  
    |  |       $parts_updated_count = SL::DB::Manager::Part->update_all (
 | 
  
    |  |         set   => { partsgroup_id => $partsgroup->id },
 | 
  
    |  |         where => [ id => $part_ids,
 | 
  
    |  |                    # partsgroup_id => $current_partsgroup->id
 | 
  
    |  |                  ], # what if one of them has changed in the meantime due to concurrent edits? should it fail? Currently
 | 
  
    |  |       );
 | 
  
    |  |       1;
 | 
  
    |  |     }) or return $self->js->error(t8('The parts couldn\'t be updated!') . ' ' . $current_partsgroup->db->error )->render;
 | 
  
    |  |     if ( $parts_updated_count == 1 ) {
 | 
  
    |  |       $self->js->flash('info', t8("Moved #1 part.", $parts_updated_count));
 | 
  
    |  |     } else {
 | 
  
    |  |       $self->js->flash('info', t8("Moved #1 parts.", $parts_updated_count));
 | 
  
    |  |     }
 | 
  
    |  |   } else {
 | 
  
    |  |     $self->js->flash('error', t8("No parts selected"));
 | 
  
    |  |   }
 | 
  
    |  | 
 | 
  
    |  |   $self->_render_parts_table_body; # needs $::form->{current_partsgroup_id}
 | 
  
    |  |   return $self->js->render;
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | #
 | 
  
    |  | # action bars
 | 
  
    |  | #
 | 
  
    |  | 
 | 
  
    |  | sub setup_show_form_action_bar {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   my $is_new = !$self->partsgroup->id;
 | 
  
    |  | 
 | 
  
    |  |   for my $bar ($::request->layout->get('actionbar')) {
 | 
  
    |  |     $bar->add(
 | 
  
    |  |       action => [
 | 
  
    |  |         t8('Save'),
 | 
  
    |  |         submit    => [ '#form', { action => 'PartsGroup/' . ($is_new ? 'create' : 'update') } ],
 | 
  
    |  |         checks    => [ 'kivi.validate_form' ],
 | 
  
    |  |         accesskey => 'enter',
 | 
  
    |  |       ],
 | 
  
    |  | 
 | 
  
    |  |       action => [
 | 
  
    |  |         t8('Delete'),
 | 
  
    |  |         submit   => [ '#form', { action => 'PartsGroup/delete' } ],
 | 
  
    |  |         confirm  => t8('Do you really want to delete this partsgroup?'),
 | 
  
    |  |         disabled => $is_new                      ? t8('This partsgroup has not been saved yet.')
 | 
  
    |  |                   : !$self->partsgroup->orphaned ? t8('The partsgroup is in use and cannot be deleted.')
 | 
  
    |  |                   :                            undef,
 | 
  
    |  |       ],
 | 
  
    |  | 
 | 
  
    |  |       link => [
 | 
  
    |  |         t8('Abort'),
 | 
  
    |  |         link => $self->url_for(action => 'list'),
 | 
  
    |  |       ],
 | 
  
    |  |     );
 | 
  
    |  |   }
 | 
  
    |  |   $::request->layout->add_javascripts('kivi.Validator.js');
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub setup_list_action_bar {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   for my $bar ($::request->layout->get('actionbar')) {
 | 
  
    |  |     $bar->add(
 | 
  
    |  |       link => [
 | 
  
    |  |         t8('Add'),
 | 
  
    |  |         link => $self->url_for(action => 'new'),
 | 
  
    |  |       ],
 | 
  
    |  |       link => [
 | 
  
    |  |         t8('Sort'),
 | 
  
    |  |         link => $self->url_for(action => 'sort_roots'),
 | 
  
    |  |       ],
 | 
  
    |  |     );
 | 
  
    |  |   }
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub setup_show_sort_action_bar {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   for my $bar ($::request->layout->get('actionbar')) {
 | 
  
    |  |     $bar->add(
 | 
  
    |  |       link => [
 | 
  
    |  |         t8('Partsgroups'),
 | 
  
    |  |         link => $self->url_for(action => 'list'),
 | 
  
    |  |       ],
 | 
  
    |  |     );
 | 
  
    |  |   }
 | 
  
    |  | }
 | 
  
    |  | #
 | 
  
    |  | # helpers
 | 
  
    |  | #
 | 
  
    |  | 
 | 
  
    |  | sub _render_subgroups_table_body {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   my ($partsgroup, $partsgroups);
 | 
  
    |  |   if ( $::form->{parent_id} ) {
 | 
  
    |  |     $partsgroup  = SL::DB::PartsGroup->new(id => $::form->{parent_id})->load;
 | 
  
    |  |     $partsgroups = $partsgroup->children_sorted;
 | 
  
    |  |   } else {
 | 
  
    |  |     $partsgroups = SL::DB::Manager::PartsGroup->get_all(where => [ parent_id => undef ], sort_by => ('sortkey'));
 | 
  
    |  |     $main::lxdebug->message(0, "found " . scalar @{ $partsgroups } . " roots");
 | 
  
    |  |   }
 | 
  
    |  | 
 | 
  
    |  |   my $html      = $self->render('partsgroup/_subgroups_table_body', { output => 0 }, CHILDREN => $partsgroups);
 | 
  
    |  | 
 | 
  
    |  |   $self->js->html('#subgroups_table_body', $html);
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | 
 | 
  
    |  | sub _render_parts_table_body {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   # May be called when items are added to the current partsgroup
 | 
  
    |  |   #   (action_add_part with $::form->{partsgroup_id}
 | 
  
    |  |   # or after items are moved away to other partsgroups
 | 
  
    |  |   #   (action_update_partsgroup_for_parts with $::form->{current_partsgroup_id})
 | 
  
    |  |   my $parts = SL::DB::Manager::Part->get_all(
 | 
  
    |  |     where => [ partsgroup_id =>    $::form->{current_partsgroup_id}
 | 
  
    |  |                                 // $::form->{partsgroup_id}
 | 
  
    |  |              ]
 | 
  
    |  |   );
 | 
  
    |  |   my $html      = $self->render('partsgroup/_parts_table_body', { output => 0 }, PARTS => $parts);
 | 
  
    |  |   $self->js->html('#parts_table_body', $html);
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub create_or_update {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  |   my $is_new = !$self->partsgroup->id;
 | 
  
    |  | 
 | 
  
    |  |   my $params = delete($::form->{partsgroup}) || { };
 | 
  
    |  | 
 | 
  
    |  |   delete $params->{id};
 | 
  
    |  | 
 | 
  
    |  |   # parent_id needs additional checks
 | 
  
    |  |   # If the parent_id was changed the new parent_id mustn't have the current
 | 
  
    |  |   # parent_id as its ancestor, otherwise this would introdouce cycles in the
 | 
  
    |  |   # tree.
 | 
  
    |  |   # run this to prevent $params->{parent_id} to be used for assign_attributes
 | 
  
    |  | 
 | 
  
    |  |   my $old_parent_id = $self->partsgroup->parent_id; # may be undef
 | 
  
    |  |   my $new_parent_id = delete $params->{parent_id} || undef; # empty string/select will become undef
 | 
  
    |  | 
 | 
  
    |  |   my @errors;
 | 
  
    |  | 
 | 
  
    |  |   my $db = $self->partsgroup->db;
 | 
  
    |  |   if (!$db->with_transaction(sub {
 | 
  
    |  | 
 | 
  
    |  |     # assign attributes and validate
 | 
  
    |  |     $self->partsgroup->assign_attributes( %{$params} ) ;
 | 
  
    |  |     push(@errors, $self->partsgroup->validate); # check for description
 | 
  
    |  | 
 | 
  
    |  |     if (@errors) {
 | 
  
    |  |       die @errors . "\n";
 | 
  
    |  |     };
 | 
  
    |  | 
 | 
  
    |  |     if (    ( $old_parent_id == $new_parent_id )
 | 
  
    |  |          or ( !defined $old_parent_id && ! defined $new_parent_id )
 | 
  
    |  |        ) {
 | 
  
    |  |       # parent_id didn't change
 | 
  
    |  |       $self->partsgroup->save;
 | 
  
    |  | 
 | 
  
    |  |     } elsif (    ( $old_parent_id != $new_parent_id )
 | 
  
    |  |               or ( not defined $old_parent_id && $new_parent_id )
 | 
  
    |  |               or ( $old_parent_id && not defined $new_parent_id) # setting parent to undef is always allowed!
 | 
  
    |  |             ) {
 | 
  
    |  |       # parent_id has changed, check for cycles
 | 
  
    |  |       my $ancestor_ids = SL::DB::PartsGroup->new(id => $new_parent_id)->ancestor_ids;
 | 
  
    |  | 
 | 
  
    |  |       if ( any { $self->partsgroup->id == $_ } @{$ancestor_ids} ) {
 | 
  
    |  |         die "Error: This would introduce a cycle, new parent must not be a subparent\n";
 | 
  
    |  |       };
 | 
  
    |  |       $self->partsgroup->remove_from_list;
 | 
  
    |  |       $self->partsgroup->parent_id($new_parent_id);
 | 
  
    |  |       $self->partsgroup->add_to_list(position => 'last');
 | 
  
    |  |     }
 | 
  
    |  | 
 | 
  
    |  |     1;
 | 
  
    |  |   })) {
 | 
  
    |  |     die @errors ? join("\n", @errors) . "\n" : $db->error . "\n";
 | 
  
    |  |   }
 | 
  
    |  | 
 | 
  
    |  |   flash_later('info', $is_new ? t8('The partsgroup has been created.') : t8('The partsgroup has been saved.'));
 | 
  
    |  |   $self->redirect_to(action => 'list');
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub show_form {
 | 
  
    |  |   my ($self, %params) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->setup_show_form_action_bar;
 | 
  
    |  |   $self->render('partsgroup/form', %params,
 | 
  
    |  |                );
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | sub load_partsgroup {
 | 
  
    |  |   my ($self) = @_;
 | 
  
    |  | 
 | 
  
    |  |   $self->partsgroup(SL::DB::PartsGroup->new(id => $::form->{id})->load);
 | 
  
    |  | }
 | 
  
    |  | 
 | 
  
    |  | 1;
 |