ACME-2026

 view release on metacpan or  search on metacpan

lib/ACME/2026.pm  view on Meta::CPAN


    my $changed = 0;
    for my $key (qw(title list priority due)) {
        next unless exists $attrs{$key};
        $item->{$key} = $attrs{$key};
        $changed = 1;
    }

    if (exists $attrs{tags} || exists $attrs{tag}) {
        $item->{tags} = _normalize_tags($attrs{tags}, $attrs{tag});
        $changed = 1;
    }

    return $item unless $changed;

    $item->{updated_at} = _now();
    _touch($plan);
    _maybe_autosave($plan);

    return $item;
}

sub delete_item {
    my ($plan, $id) = @_;
    _ensure_plan($plan);

    my $items = $plan->{items};
    for my $idx (0 .. $#$items) {
        next unless defined $items->[$idx]{id} && $items->[$idx]{id} == $id;
        my $item = splice(@$items, $idx, 1);
        _touch($plan);
        _maybe_autosave($plan);
        return $item;
    }

    return;
}

sub get_item {
    my ($plan, $id) = @_;
    _ensure_plan($plan);
    return _find_item($plan, $id);
}

sub add_note {
    my ($plan, $id, $note) = @_;
    _ensure_plan($plan);
    croak 'add_note requires a note' unless defined $note && length $note;

    my $item = _find_item($plan, $id);
    croak "No item with id $id" unless $item;

    _add_note($plan, $item, $note);
    _maybe_autosave($plan);

    return $item;
}

sub complete_item {
    my ($plan, $id, %opts) = @_;
    return _set_status($plan, $id, 'done', %opts);
}

sub skip_item {
    my ($plan, $id, %opts) = @_;
    return _set_status($plan, $id, 'skipped', %opts);
}

sub reopen_item {
    my ($plan, $id, %opts) = @_;
    return _set_status($plan, $id, 'todo', %opts);
}

sub items {
    my ($plan, %filters) = @_;
    _ensure_plan($plan);

    my @items = @{ $plan->{items} || [] };

    if (defined $filters{status}) {
        my $status = _normalize_status($filters{status});
        @items = grep { $_->{status} eq $status } @items;
    }

    if (defined $filters{list}) {
        @items = grep { defined $_->{list} && $_->{list} eq $filters{list} } @items;
    }

    my @tags;
    push @tags, $filters{tag} if defined $filters{tag};
    if (defined $filters{tags}) {
        if (ref $filters{tags} eq 'ARRAY') {
            push @tags, @{ $filters{tags} };
        } else {
            push @tags, $filters{tags};
        }
    }

    if (@tags) {
        @items = grep {
            my %item_tags = map { $_ => 1 } @{ $_->{tags} || [] };
            my $match = 0;
            for my $tag (@tags) {
                next unless defined $tag && length $tag;
                if ($item_tags{$tag}) {
                    $match = 1;
                    last;
                }
            }
            $match;
        } @items;
    }

    if (defined $filters{priority}) {
        @items = grep { defined $_->{priority} && $_->{priority} == $filters{priority} } @items;
    }

    if (defined $filters{min_priority}) {
        @items = grep { defined $_->{priority} && $_->{priority} >= $filters{min_priority} } @items;
    }

    if (defined $filters{max_priority}) {
        @items = grep { defined $_->{priority} && $_->{priority} <= $filters{max_priority} } @items;
    }

    if (defined $filters{due_before}) {
        @items = grep { defined $_->{due} && $_->{due} le $filters{due_before} } @items;
    }

    if (defined $filters{due_after}) {
        @items = grep { defined $_->{due} && $_->{due} ge $filters{due_after} } @items;
    }

    if (defined $filters{sort}) {
        @items = _sort_items(\@items, $filters{sort});
    }

    return @items;
}

sub stats {
    my ($plan, %filters) = @_;
    _ensure_plan($plan);

    my @items = items($plan, %filters);
    my %stats = (
        total => scalar @items,
        todo => 0,
        done => 0,
        skipped => 0,
    );

    for my $item (@items) {
        $stats{ $item->{status} }++ if exists $stats{ $item->{status} };
    }

    $stats{complete_pct} = $stats{total}
        ? int(($stats{done} / $stats{total}) * 100 + 0.5)
        : 0;

    return \%stats;
}

sub _set_status {
    my ($plan, $id, $status, %opts) = @_;
    _ensure_plan($plan);

    _reject_unknown('_set_status', \%opts, qw(note));
    my $item = _find_item($plan, $id);
    croak "No item with id $id" unless $item;

    $item->{status} = _normalize_status($status);
    $item->{updated_at} = _now();
    if (defined $opts{note}) {
        _add_note($plan, $item, $opts{note});
    } else {
        _touch($plan);
    }
    _maybe_autosave($plan);

    return $item;
}

sub _normalize_opts {
    return %{ $_[0] } if @_ == 1 && ref $_[0] eq 'HASH';
    return @_;
}

sub _normalize_plan {
    my ($plan) = @_;
    _ensure_plan($plan);

    $plan->{title} = '2026' unless defined $plan->{title} && length $plan->{title};
    $plan->{items} = [] unless ref $plan->{items} eq 'ARRAY';
    $plan->{autosave} = $plan->{autosave} ? 1 : 0;

    my $max_id = 0;
    for my $item (@{ $plan->{items} }) {
        next unless ref $item eq 'HASH';
        $max_id = $item->{id} if defined $item->{id} && $item->{id} > $max_id;
    }

    $plan->{next_id} = $plan->{next_id} || ($max_id + 1);
    my $next_id = $plan->{next_id};

    for my $item (@{ $plan->{items} }) {
        next unless ref $item eq 'HASH';
        if (!defined $item->{id}) {
            $item->{id} = $next_id++;
        }
        $item->{status} = _normalize_status($item->{status});
        $item->{tags} = _normalize_tags($item->{tags});
        $item->{notes} = _normalize_notes($item->{notes});
        $item->{priority} = defined $item->{priority} ? $item->{priority} : 3;
        $item->{list} = defined $item->{list} ? $item->{list} : 'General';
        $item->{created_at} = _now() unless defined $item->{created_at};
        $item->{updated_at} = $item->{created_at} unless defined $item->{updated_at};
    }

    $plan->{next_id} = $next_id if $next_id > $plan->{next_id};
    $plan->{created_at} = _now() unless defined $plan->{created_at};
    $plan->{updated_at} = $plan->{created_at} unless defined $plan->{updated_at};

    return $plan;
}

sub _normalize_status {
    my ($status) = @_;



( run in 2.157 seconds using v1.01-cache-2.11-cpan-75ffa21a3d4 )