diff --git a/REPORTS_BACKEND_REWORK_PLAN.md b/REPORTS_BACKEND_REWORK_PLAN.md
new file mode 100644
index 0000000..950c665
--- /dev/null
+++ b/REPORTS_BACKEND_REWORK_PLAN.md
@@ -0,0 +1,398 @@
+# Reports Backend Rework Plan
+
+## Overview
+Transform the current hardcoded report system into a flexible, database-driven architecture that allows dynamic report configuration without code changes.
+
+## Current Architecture Analysis
+
+### Existing Structure
+- **Report Classes**: Individual PHP classes (`ActiveContractsReport`, `ActivitiesPerPeriodReport`, etc.)
+- **Registry Pattern**: `ReportRegistry` stores report instances in memory
+- **Service Provider**: `ReportServiceProvider` registers reports at boot time
+- **Base Class**: `BaseEloquentReport` provides common pagination logic
+- **Contract Interface**: `Report` interface defines required methods (`slug`, `name`, `description`, `inputs`, `columns`, `query`)
+- **Controller**: `ReportController` handles index, show, data, export routes
+
+### Current Features
+1. **Report Definition**: Each report defines:
+ - Slug (unique identifier)
+ - Name & Description
+ - Input parameters (filters)
+ - Column definitions
+ - Eloquent query builder
+2. **Filter Types**: `date`, `string`, `select:client`, etc.
+3. **Export**: PDF and CSV export functionality
+4. **Pagination**: Server-side pagination support
+
+## Proposed New Architecture
+
+### 1. Database Schema
+
+#### `reports` Table
+```sql
+CREATE TABLE reports (
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
+ slug VARCHAR(255) UNIQUE NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ description TEXT NULL,
+ category VARCHAR(100) NULL, -- e.g., 'contracts', 'activities', 'financial'
+ enabled BOOLEAN DEFAULT TRUE,
+ order INT DEFAULT 0,
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+ INDEX idx_slug (slug),
+ INDEX idx_enabled_order (enabled, order)
+);
+```
+
+#### `report_entities` Table
+Defines which database entities (models) the report queries.
+
+```sql
+CREATE TABLE report_entities (
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
+ report_id BIGINT UNSIGNED NOT NULL,
+ model_class VARCHAR(255) NOT NULL, -- e.g., 'App\Models\Contract'
+ alias VARCHAR(50) NULL, -- table alias for joins
+ join_type ENUM('base', 'join', 'leftJoin', 'rightJoin') DEFAULT 'base',
+ join_first VARCHAR(100) NULL, -- first column for join
+ join_operator VARCHAR(10) NULL, -- =, !=, etc.
+ join_second VARCHAR(100) NULL, -- second column for join
+ order INT DEFAULT 0,
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+ FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE,
+ INDEX idx_report_order (report_id, order)
+);
+```
+
+#### `report_columns` Table
+Defines selectable columns and their presentation.
+
+```sql
+CREATE TABLE report_columns (
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
+ report_id BIGINT UNSIGNED NOT NULL,
+ key VARCHAR(100) NOT NULL, -- column identifier
+ label VARCHAR(255) NOT NULL, -- display label
+ type VARCHAR(50) DEFAULT 'string', -- string, number, date, boolean, currency
+ expression TEXT NOT NULL, -- SQL expression or column path
+ sortable BOOLEAN DEFAULT TRUE,
+ visible BOOLEAN DEFAULT TRUE,
+ order INT DEFAULT 0,
+ format_options JSON NULL, -- { "decimals": 2, "prefix": "$" }
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+ FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE,
+ INDEX idx_report_order (report_id, order)
+);
+```
+
+#### `report_filters` Table
+Defines available filter parameters.
+
+```sql
+CREATE TABLE report_filters (
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
+ report_id BIGINT UNSIGNED NOT NULL,
+ key VARCHAR(100) NOT NULL, -- filter identifier
+ label VARCHAR(255) NOT NULL,
+ type VARCHAR(50) NOT NULL, -- date, string, select, multiselect, number, boolean
+ nullable BOOLEAN DEFAULT TRUE,
+ default_value TEXT NULL,
+ options JSON NULL, -- For select/multiselect: [{"label":"...", "value":"..."}]
+ data_source VARCHAR(255) NULL, -- e.g., 'clients', 'segments' for dynamic selects
+ order INT DEFAULT 0,
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+ FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE,
+ INDEX idx_report_order (report_id, order)
+);
+```
+
+#### `report_conditions` Table
+Defines WHERE clause conditions (rules) for filtering data.
+
+```sql
+CREATE TABLE report_conditions (
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
+ report_id BIGINT UNSIGNED NOT NULL,
+ column VARCHAR(255) NOT NULL, -- e.g., 'contracts.start_date'
+ operator VARCHAR(50) NOT NULL, -- =, !=, >, <, >=, <=, LIKE, IN, BETWEEN, IS NULL, etc.
+ value_type VARCHAR(50) NOT NULL, -- static, filter, expression
+ value TEXT NULL, -- static value or expression
+ filter_key VARCHAR(100) NULL, -- references report_filters.key
+ logical_operator ENUM('AND', 'OR') DEFAULT 'AND',
+ group_id INT NULL, -- for grouping conditions (AND within group, OR between groups)
+ order INT DEFAULT 0,
+ enabled BOOLEAN DEFAULT TRUE,
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+ FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE,
+ INDEX idx_report_group (report_id, group_id, order)
+);
+```
+
+#### `report_orders` Table
+Defines default ORDER BY clauses.
+
+```sql
+CREATE TABLE report_orders (
+ id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
+ report_id BIGINT UNSIGNED NOT NULL,
+ column VARCHAR(255) NOT NULL,
+ direction ENUM('ASC', 'DESC') DEFAULT 'ASC',
+ order INT DEFAULT 0,
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+ FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE,
+ INDEX idx_report_order (report_id, order)
+);
+```
+
+### 2. Model Structure
+
+#### Report Model
+```php
+class Report extends Model
+{
+ protected $fillable = ['slug', 'name', 'description', 'category', 'enabled', 'order'];
+ protected $casts = ['enabled' => 'boolean'];
+
+ public function entities(): HasMany { return $this->hasMany(ReportEntity::class); }
+ public function columns(): HasMany { return $this->hasMany(ReportColumn::class); }
+ public function filters(): HasMany { return $this->hasMany(ReportFilter::class); }
+ public function conditions(): HasMany { return $this->hasMany(ReportCondition::class); }
+ public function orders(): HasMany { return $this->hasMany(ReportOrder::class); }
+}
+```
+
+### 3. Query Builder Service
+
+Create `ReportQueryBuilder` service to dynamically construct queries:
+
+```php
+class ReportQueryBuilder
+{
+ public function build(Report $report, array $filters = []): Builder
+ {
+ // 1. Start with base model query
+ // 2. Apply joins from report_entities
+ // 3. Select columns from report_columns
+ // 4. Apply conditions from report_conditions
+ // 5. Apply filter values to parameterized conditions
+ // 6. Apply ORDER BY from report_orders
+ // 7. Return Builder instance
+ }
+}
+```
+
+### 4. Backward Compatibility Layer
+
+Keep existing Report classes but load from database:
+
+```php
+class DatabaseReport extends BaseEloquentReport implements Report
+{
+ public function __construct(protected Report $dbReport) {}
+
+ public function slug(): string { return $this->dbReport->slug; }
+ public function name(): string { return $this->dbReport->name; }
+ public function description(): ?string { return $this->dbReport->description; }
+
+ public function inputs(): array {
+ return $this->dbReport->filters()
+ ->orderBy('order')
+ ->get()
+ ->map(fn($f) => [
+ 'key' => $f->key,
+ 'type' => $f->type,
+ 'label' => $f->label,
+ 'nullable' => $f->nullable,
+ 'default' => $f->default_value,
+ 'options' => $f->options,
+ ])
+ ->toArray();
+ }
+
+ public function columns(): array {
+ return $this->dbReport->columns()
+ ->where('visible', true)
+ ->orderBy('order')
+ ->get()
+ ->map(fn($c) => ['key' => $c->key, 'label' => $c->label])
+ ->toArray();
+ }
+
+ public function query(array $filters): Builder {
+ return app(ReportQueryBuilder::class)->build($this->dbReport, $filters);
+ }
+}
+```
+
+### 5. Migration Strategy
+
+#### Phase 1: Database Setup
+1. Create migrations for all new tables
+2. Create models with relationships
+3. Create seeders to migrate existing hardcoded reports
+
+#### Phase 2: Service Layer
+1. Build `ReportQueryBuilder` service
+2. Build `DatabaseReport` adapter class
+3. Update `ReportRegistry` to load from database
+4. Create report management CRUD (admin UI)
+
+#### Phase 3: Testing & Validation
+1. Unit tests for query builder
+2. Integration tests comparing old vs new results
+3. Performance benchmarks
+4. Export functionality validation
+
+#### Phase 4: Migration Seeder
+1. Create seeder that converts each hardcoded report into database records
+2. Example for `ActiveContractsReport`:
+ ```php
+ $report = Report::create([
+ 'slug' => 'active-contracts',
+ 'name' => 'Aktivne pogodbe',
+ 'description' => 'Pogodbe, ki so aktivne...',
+ 'enabled' => true,
+ ]);
+
+ // Add entities (joins)
+ $report->entities()->create([
+ 'model_class' => 'App\Models\Contract',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ]);
+
+ $report->entities()->create([
+ 'model_class' => 'App\Models\ClientCase',
+ 'join_type' => 'join',
+ 'join_first' => 'contracts.client_case_id',
+ 'join_operator' => '=',
+ 'join_second' => 'client_cases.id',
+ 'order' => 1,
+ ]);
+
+ // Add columns
+ $report->columns()->create([
+ 'key' => 'contract_reference',
+ 'label' => 'Pogodba',
+ 'expression' => 'contracts.reference',
+ 'order' => 0,
+ ]);
+
+ // Add filters
+ $report->filters()->create([
+ 'key' => 'client_uuid',
+ 'label' => 'Stranka',
+ 'type' => 'select',
+ 'data_source' => 'clients',
+ 'nullable' => true,
+ 'order' => 0,
+ ]);
+
+ // Add conditions
+ $report->conditions()->create([
+ 'column' => 'contracts.start_date',
+ 'operator' => '<=',
+ 'value_type' => 'expression',
+ 'value' => 'CURRENT_DATE',
+ 'group_id' => 1,
+ 'order' => 0,
+ ]);
+ ```
+
+#### Phase 5: Remove Old Report System
+Once the new database-driven system is validated and working:
+
+1. **Delete Hardcoded Report Classes**:
+ - Remove `app/Reports/ActiveContractsReport.php`
+ - Remove `app/Reports/ActivitiesPerPeriodReport.php`
+ - Remove `app/Reports/ActionsDecisionsCountReport.php`
+ - Remove `app/Reports/DecisionsCountReport.php`
+ - Remove `app/Reports/FieldJobsCompletedReport.php`
+ - Remove `app/Reports/SegmentActivityCountsReport.php`
+
+2. **Remove Base Classes/Interfaces** (if no longer needed):
+ - Remove `app/Reports/BaseEloquentReport.php`
+ - Remove `app/Reports/Contracts/Report.php` interface
+
+3. **Remove/Update Service Provider**:
+ - Remove `app/Providers/ReportServiceProvider.php`
+ - Or update it to only load reports from database
+
+4. **Update ReportRegistry**:
+ - Modify to load from database instead of manual registration
+ - Remove all hardcoded `register()` calls
+
+5. **Clean Up Config**:
+ - Remove any report-specific configuration files if they exist
+ - Update `bootstrap/providers.php` to remove ReportServiceProvider
+
+6. **Documentation Cleanup**:
+ - Update any documentation referencing old report classes
+ - Add migration guide for future report creation
+
+### 6. Admin UI for Report Management
+
+Create CRUD interface at `Settings/Reports/*`:
+- **Index**: List all reports with enable/disable toggle
+- **Create**: Wizard-style form for building new reports
+- **Edit**: Visual query builder interface
+- **Test**: Preview report results
+- **Clone**: Duplicate existing report as starting point
+
+### 7. Advanced Features (Future)
+
+1. **Calculated Fields**: Allow expressions like `(column_a + column_b) / 2`
+2. **Aggregations**: Support SUM, AVG, COUNT, MIN, MAX
+3. **Subqueries**: Define subquery relationships
+4. **Report Templates**: Predefined report structures
+5. **Scheduled Reports**: Email reports on schedule
+6. **Report Sharing**: Share reports with specific users/roles
+7. **Version History**: Track report definition changes
+8. **Report Permissions**: Control who can view/edit reports
+
+## Benefits
+
+1. **No Code Changes**: Add/modify reports through UI
+2. **Flexibility**: Non-developers can create reports
+3. **Consistency**: All reports follow same structure
+4. **Maintainability**: Centralized report logic
+5. **Reusability**: Share entities, filters, conditions
+6. **Version Control**: Track changes to report definitions
+7. **Performance**: Optimize query builder once
+8. **Export**: Works with any report automatically
+
+## Risks & Considerations
+
+1. **Complexity**: Query builder must handle diverse SQL patterns
+2. **Performance**: Dynamic query building overhead
+3. **Security**: SQL injection risks with user input
+4. **Learning Curve**: Team needs to understand new system
+5. **Testing**: Comprehensive test suite required
+6. **Migration**: Convert all existing reports correctly
+7. **Edge Cases**: Complex queries may be difficult to represent
+
+## Timeline Estimate
+
+- **Phase 1 (Database)**: 2-3 days
+- **Phase 2 (Services)**: 4-5 days
+- **Phase 3 (Testing)**: 2-3 days
+- **Phase 4 (Migration)**: 1-2 days
+- **Phase 5 (Cleanup)**: 1 day
+- **Admin UI**: 3-4 days
+- **Total**: 13-18 days
+
+## Success Criteria
+
+1. ✅ All existing reports work identically
+2. ✅ New reports can be created via UI
+3. ✅ Export functionality preserved
+4. ✅ Performance within 10% of current
+5. ✅ Zero SQL injection vulnerabilities
+6. ✅ Comprehensive test coverage (>80%)
+7. ✅ Documentation complete
diff --git a/REPORTS_FRONTEND_REWORK_PLAN.md b/REPORTS_FRONTEND_REWORK_PLAN.md
new file mode 100644
index 0000000..60df866
--- /dev/null
+++ b/REPORTS_FRONTEND_REWORK_PLAN.md
@@ -0,0 +1,528 @@
+# Reports Frontend Rework Plan
+
+## Overview
+This plan outlines the modernization of Reports frontend pages (`Index.vue` and `Show.vue`) using shadcn-vue components and AppCard containers, following the same patterns established in the Settings pages rework.
+
+## Current State Analysis
+
+### Reports/Index.vue (30 lines)
+**Current Implementation:**
+- Simple grid layout with native divs
+- Report cards: `border rounded-lg p-4 bg-white shadow-sm hover:shadow-md`
+- Grid: `md:grid-cols-2 lg:grid-cols-3`
+- Each card shows: name (h2), description (p), Link to report
+- **No shadcn-vue components used**
+
+**Identified Issues:**
+- Native HTML/Tailwind instead of shadcn-vue Card
+- Inconsistent with Settings pages styling
+- No icons for visual interest
+- Basic hover effects only
+
+### Reports/Show.vue (314 lines)
+**Current Implementation:**
+- Complex page with filters, export buttons, and data table
+- Header section: title, description, export buttons (lines 190-196)
+ - Buttons: `px-3 py-2 rounded bg-gray-200 hover:bg-gray-300`
+- Filter section: grid layout `md:grid-cols-4` (lines 218-270)
+ - Native inputs: `border rounded px-2 py-1`
+ - Native selects: `border rounded px-2 py-1`
+ - DatePicker component (already working)
+ - Filter buttons: Apply (`bg-indigo-600`) and Reset (`bg-gray-100`)
+- Data table: DataTableServer component (lines 285-300)
+- Formatting functions: formatNumberEU, formatDateEU, formatDateTimeEU, formatCell
+
+**Identified Issues:**
+- No Card containers for sections
+- Native buttons instead of shadcn Button
+- Native input/select elements instead of shadcn Input/Select
+- No visual separation between sections
+- Filter section could be extracted to partial
+
+## Target Architecture
+
+### Pattern Reference from Settings Pages
+
+**Settings/Index.vue Pattern:**
+```vue
+
+
+
+
+ Title
+
+ Description
+
+
+ Action →
+
+
+```
+
+**Settings/Archive/Index.vue Pattern:**
+- Uses AppCard for main container
+- Extracted partials: ArchiveRuleCard, CreateRuleForm, EditRuleForm
+- Alert components for warnings
+- Badge components for status indicators
+
+## Implementation Plan
+
+### Phase 1: Reports/Index.vue Rework (Simple)
+
+**Goal:** Replace native divs with shadcn-vue Card components, add icons
+
+**Changes:**
+1. **Import shadcn-vue components:**
+ ```js
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
+ import { Button } from "@/Components/ui/button";
+ import { BarChart3, FileText, Activity, Users, TrendingUp, Calendar } from "lucide-vue-next";
+ ```
+
+2. **Add icon mapping for reports:**
+ ```js
+ const reportIcons = {
+ 'contracts': FileText,
+ 'field': TrendingUp,
+ 'activities': Activity,
+ // fallback icon
+ default: BarChart3,
+ };
+
+ function getReportIcon(category) {
+ return reportIcons[category] || reportIcons.default;
+ }
+ ```
+
+3. **Replace report card structure:**
+ - Remove native `
`
+ - Use `
`
+ - Structure:
+ ```vue
+
+
+
+
+ {{ report.name }}
+
+ {{ report.description }}
+
+
+
+
+ Odpri →
+
+
+
+
+ ```
+
+4. **Update page header:**
+ - Wrap in proper container with consistent spacing
+ - Match Settings/Index.vue header style
+
+**Estimated Changes:**
+- Lines: 30 → ~65 lines (with imports and icon logic)
+- Files modified: 1 (Index.vue)
+- Files created: 0
+
+**Risk Level:** Low (simple page, straightforward replacement)
+
+---
+
+### Phase 2: Reports/Show.vue Rework - Structure (Medium)
+
+**Goal:** Add Card containers for sections, replace native buttons
+
+**Changes:**
+
+1. **Import shadcn-vue components:**
+ ```js
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
+ import { Button } from "@/Components/ui/button";
+ import { Input } from "@/Components/ui/input";
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
+ import { Label } from "@/Components/ui/label";
+ import { Badge } from "@/Components/ui/badge";
+ import { Separator } from "@/Components/ui/separator";
+ import { Download, Filter, RotateCcw } from "lucide-vue-next";
+ ```
+
+2. **Wrap header + export buttons in Card:**
+ ```vue
+
+
+
+
+ {{ name }}
+ {{ description }}
+
+
+
+
+ CSV
+
+
+
+ PDF
+
+
+
+ Excel
+
+
+
+
+
+ ```
+
+3. **Wrap filters in Card:**
+ ```vue
+
+
+
+
+ Filtri
+
+
+
+
+
+
+
+
+
+
+
+ Prikaži
+
+
+
+ Ponastavi
+
+
+
+
+ ```
+
+4. **Wrap DataTableServer in Card:**
+ ```vue
+
+
+ Rezultati
+
+ Skupaj {{ meta?.total || 0 }} {{ meta?.total === 1 ? 'rezultat' : 'rezultatov' }}
+
+
+
+
+ />
+
+
+ ```
+
+5. **Replace all native buttons with shadcn Button:**
+ - Export buttons: `variant="outline" size="sm"`
+ - Apply filter button: default variant
+ - Reset button: `variant="outline"`
+
+**Estimated Changes:**
+- Lines: 314 → ~350 lines (with imports and Card wrappers)
+- Files modified: 1 (Show.vue)
+- Files created: 0
+- **Keep formatting functions unchanged** (working correctly)
+
+**Risk Level:** Low-Medium (more complex but no logic changes)
+
+---
+
+### Phase 3: Reports/Show.vue - Replace Native Inputs (Medium)
+
+**Goal:** Replace native input/select elements with shadcn-vue components
+
+**Changes:**
+
+1. **Replace date inputs:**
+ ```vue
+
+
+ {{ inp.label || inp.key }}
+
+
+ ```
+
+2. **Replace text/number inputs:**
+ ```vue
+
+ {{ inp.label || inp.key }}
+
+
+ ```
+
+3. **Replace select inputs (user/client):**
+ ```vue
+
+
{{ inp.label || inp.key }}
+
+
+
+
+
+ — brez —
+
+ {{ u.name }}
+
+
+
+
Nalagam…
+
+ ```
+
+4. **Update filter grid layout:**
+ - Change from `md:grid-cols-4` to `md:grid-cols-2 lg:grid-cols-4`
+ - Use `space-y-2` for label/input spacing
+ - Consistent gap: `gap-4`
+
+**Estimated Changes:**
+- Lines: ~350 → ~380 lines (shadcn Input/Select have more markup)
+- Files modified: 1 (Show.vue)
+- Files created: 0
+
+**Risk Level:** Medium (v-model binding changes, test thoroughly)
+
+---
+
+### Phase 4: Optional - Extract Filter Section Partial (Optional)
+
+**Goal:** Reduce Show.vue complexity by extracting filter logic
+
+**Decision Criteria:**
+- If filter section exceeds ~80 lines → extract to partial
+- If multiple filter types need separate handling → extract
+
+**Potential Partial Structure:**
+```
+resources/js/Pages/Reports/Partials/
+ FilterSection.vue
+```
+
+**FilterSection.vue:**
+- Props: `inputs`, `filters` (reactive object), `userOptions`, `clientOptions`, `loading states`
+- Emits: `@apply`, `@reset`
+- Contains: entire filter grid + buttons
+
+**Benefits:**
+- Show.vue reduced from ~380 lines to ~300 lines
+- Filter logic isolated and reusable
+- Easier to maintain filter types
+
+**Risks:**
+- Adds complexity with props/emits
+- Might not be worth it if filter logic is simple
+
+**Recommendation:** Evaluate after Phase 3 completion. If filter section is clean and under 80 lines, skip this phase.
+
+---
+
+## Component Inventory
+
+### shadcn-vue Components Needed
+
+**Already Installed (verify):**
+- Card, CardHeader, CardTitle, CardDescription, CardContent
+- Button
+- Input
+- Select, SelectTrigger, SelectValue, SelectContent, SelectItem
+- Label
+- Badge
+- Separator
+
+**Need to Check:**
+- lucide-vue-next icons (Download, Filter, RotateCcw, BarChart3, FileText, Activity, TrendingUp, Calendar)
+
+### Custom Components
+- AppCard (if needed for consistency)
+- DatePicker (already working, keep as-is)
+- DataTableServer (keep as-is)
+
+---
+
+## Testing Checklist
+
+### Reports/Index.vue Testing:
+- [ ] Cards display with correct icons
+- [ ] Card hover effects work
+- [ ] Links navigate to correct report
+- [ ] Grid layout responsive (2 cols MD, 3 cols LG)
+- [ ] Icons match report categories
+
+### Reports/Show.vue Testing:
+- [ ] Header Card displays title, description, export buttons
+- [ ] Export buttons work (CSV, PDF, Excel)
+- [ ] Filter Card displays all filter inputs correctly
+- [ ] Date filters use DatePicker component
+- [ ] User/Client selects load options async
+- [ ] Apply filters button triggers report refresh
+- [ ] Reset button clears all filters
+- [ ] DataTableServer Card displays results
+- [ ] Formatting functions work (dates, numbers, currencies)
+- [ ] Pagination works
+- [ ] All 6 reports render correctly:
+ - [ ] active-contracts
+ - [ ] field-jobs-completed
+ - [ ] decisions-counts
+ - [ ] segment-activity-counts
+ - [ ] actions-decisions-counts
+ - [ ] activities-per-period
+
+---
+
+## Implementation Order
+
+### Step 1: Reports/Index.vue (30 min)
+1. Import shadcn-vue components + icons
+2. Add icon mapping function
+3. Replace native divs with Card structure
+4. Test navigation and layout
+5. Verify responsive grid
+
+### Step 2: Reports/Show.vue - Structure (45 min)
+1. Import shadcn-vue components + icons
+2. Wrap header + exports in Card
+3. Wrap filters in Card
+4. Wrap DataTableServer in Card
+5. Replace all native buttons
+6. Test all 6 reports
+
+### Step 3: Reports/Show.vue - Inputs (60 min)
+1. Replace text/number inputs with shadcn Input
+2. Replace select inputs with shadcn Select
+3. Add Label components
+4. Test v-model bindings
+5. Test async user/client loading
+6. Test filter apply/reset
+7. Verify all filter types work
+
+### Step 4: Optional Partial Extraction (30 min, if needed)
+1. Create FilterSection.vue partial
+2. Move filter logic to partial
+3. Set up props/emits
+4. Test with all reports
+
+### Step 5: Final Testing (30 min)
+1. Test complete workflow (Index → Show → Filters → Export)
+2. Verify all 6 reports
+3. Test responsive layouts (mobile, tablet, desktop)
+4. Check formatting consistency
+5. Verify no regressions
+
+**Total Estimated Time:** 2.5 - 3.5 hours
+
+---
+
+## Risk Assessment
+
+### Low Risk:
+- Index.vue rework (simple structure, straightforward replacement)
+- Adding Card containers to Show.vue
+- Replacing native buttons with shadcn Button
+
+### Medium Risk:
+- Replacing native inputs with shadcn Input/Select
+ - v-model bindings might need adjustments
+ - Async select loading needs testing
+ - Number input behavior might differ
+
+### Mitigation Strategies:
+1. Test each phase incrementally
+2. Keep formatting functions unchanged (already working)
+3. Test v-model bindings immediately after input replacement
+4. Verify async loading with console logs
+5. Test all 6 reports after each phase
+6. Keep git commits small and atomic
+
+---
+
+## Success Criteria
+
+### Functional Requirements:
+✅ All reports navigate from Index page
+✅ All filters work correctly (date, text, number, user select, client select)
+✅ Apply filters refreshes report data
+✅ Reset filters clears all inputs
+✅ Export buttons generate CSV/PDF/Excel files
+✅ DataTableServer displays results correctly
+✅ Pagination works
+✅ Formatting functions work (dates, numbers)
+
+### Visual Requirements:
+✅ Consistent Card-based layout
+✅ shadcn-vue components throughout
+✅ Icons for visual interest
+✅ Hover effects on cards
+✅ Proper spacing and alignment
+✅ Responsive layout (mobile, tablet, desktop)
+✅ Matches Settings pages style
+
+### Code Quality:
+✅ No code duplication
+✅ Clean component imports
+✅ Consistent naming conventions
+✅ Proper TypeScript/Vue 3 patterns
+✅ Formatting functions unchanged
+✅ No regressions in functionality
+
+---
+
+## Notes
+
+- **DatePicker component:** Already working, imported correctly, no changes needed
+- **Formatting functions:** Keep unchanged (formatNumberEU, formatDateEU, formatDateTimeEU, formatCell)
+- **DataTableServer:** Keep as-is, already working well
+- **Async loading:** User/client select loading works, just needs shadcn Select wrapper
+- **Pattern consistency:** Follow Settings/Index.vue and Settings/Archive/Index.vue patterns
+- **Icon usage:** Add icons to Index.vue for visual interest, use lucide-vue-next
+- **Button variants:** Use `variant="outline"` for secondary actions, default for primary
+
+---
+
+## Post-Implementation
+
+After completing all phases:
+
+1. **Documentation:**
+ - Update this document with actual implementation notes
+ - Document any deviations from plan
+ - Note any unexpected issues
+
+2. **Code Review:**
+ - Check for consistent component usage
+ - Verify no native HTML/CSS buttons/inputs remain
+ - Ensure proper import structure
+
+3. **User Feedback:**
+ - Test with actual users
+ - Gather feedback on UI improvements
+ - Note any requested adjustments
+
+4. **Performance:**
+ - Verify no performance regressions
+ - Check bundle size impact
+ - Monitor async loading times
+
+---
+
+## Conclusion
+
+This plan provides a structured approach to modernizing the Reports frontend pages using shadcn-vue components. The phased approach allows for incremental testing and reduces risk. The estimated total time is 2.5-3.5 hours, with low to medium risk level.
+
+**Recommendation:** Start with Phase 1 (Index.vue) as a proof of concept, then proceed to Phase 2 and 3 for Show.vue. Evaluate Phase 4 (partial extraction) after Phase 3 completion based on actual complexity.
diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php
index 197cd61..6264a67 100644
--- a/app/Http/Controllers/ImportController.php
+++ b/app/Http/Controllers/ImportController.php
@@ -827,6 +827,6 @@ public function destroy(Request $request, Import $import)
$import->delete();
- return back()->with(['ok' => true]);
+ return back()->with('success', 'Import deleted successfully');
}
}
diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php
index 7236a18..a4e3f97 100644
--- a/app/Http/Controllers/ReportController.php
+++ b/app/Http/Controllers/ReportController.php
@@ -2,7 +2,8 @@
namespace App\Http\Controllers;
-use App\Reports\ReportRegistry;
+use App\Models\Report;
+use App\Services\ReportQueryBuilder;
use Illuminate\Http\Request;
use Inertia\Inertia;
@@ -10,15 +11,19 @@
class ReportController extends Controller
{
- public function __construct(protected ReportRegistry $registry) {}
+ public function __construct(protected ReportQueryBuilder $queryBuilder) {}
public function index(Request $request)
{
- $reports = collect($this->registry->all())
+ $reports = Report::where('enabled', true)
+ ->orderBy('order')
+ ->orderBy('name')
+ ->get()
->map(fn ($r) => [
- 'slug' => $r->slug(),
- 'name' => $r->name(),
- 'description' => $r->description(),
+ 'slug' => $r->slug,
+ 'name' => $r->name,
+ 'description' => $r->description,
+ 'category' => $r->category,
])
->values();
@@ -29,26 +34,30 @@ public function index(Request $request)
public function show(string $slug, Request $request)
{
- $report = $this->registry->findBySlug($slug);
- abort_if(! $report, 404);
- $report->authorize($request);
+ $report = Report::with(['filters', 'columns'])
+ ->where('slug', $slug)
+ ->where('enabled', true)
+ ->firstOrFail();
// Accept filters & pagination from query and return initial data for server-driven table
- $filters = $this->validateFilters($report->inputs(), $request);
+ $inputs = $this->buildInputsArray($report);
+ $filters = $this->validateFilters($inputs, $request);
\Log::info('Report filters', ['filters' => $filters, 'request' => $request->all()]);
+
$perPage = (int) ($request->integer('per_page') ?: 25);
- $paginator = $report->paginate($filters, $perPage);
+ $query = $this->queryBuilder->build($report, $filters);
+ $paginator = $query->paginate($perPage);
$rows = collect($paginator->items())
->map(fn ($row) => $this->normalizeRow($row))
->values();
return Inertia::render('Reports/Show', [
- 'slug' => $report->slug(),
- 'name' => $report->name(),
- 'description' => $report->description(),
- 'inputs' => $report->inputs(),
- 'columns' => $report->columns(),
+ 'slug' => $report->slug,
+ 'name' => $report->name,
+ 'description' => $report->description,
+ 'inputs' => $inputs,
+ 'columns' => $this->buildColumnsArray($report),
'rows' => $rows,
'meta' => [
'total' => $paginator->total(),
@@ -62,14 +71,17 @@ public function show(string $slug, Request $request)
public function data(string $slug, Request $request)
{
- $report = $this->registry->findBySlug($slug);
- abort_if(! $report, 404);
- $report->authorize($request);
+ $report = Report::with(['filters', 'columns'])
+ ->where('slug', $slug)
+ ->where('enabled', true)
+ ->firstOrFail();
- $filters = $this->validateFilters($report->inputs(), $request);
+ $inputs = $this->buildInputsArray($report);
+ $filters = $this->validateFilters($inputs, $request);
$perPage = (int) ($request->integer('per_page') ?: 25);
- $paginator = $report->paginate($filters, $perPage);
+ $query = $this->queryBuilder->build($report, $filters);
+ $paginator = $query->paginate($perPage);
$rows = collect($paginator->items())
->map(fn ($row) => $this->normalizeRow($row))
@@ -85,20 +97,23 @@ public function data(string $slug, Request $request)
public function export(string $slug, Request $request)
{
- $report = $this->registry->findBySlug($slug);
- abort_if(! $report, 404);
- $report->authorize($request);
+ $report = Report::with(['filters', 'columns'])
+ ->where('slug', $slug)
+ ->where('enabled', true)
+ ->firstOrFail();
- $filters = $this->validateFilters($report->inputs(), $request);
+ $inputs = $this->buildInputsArray($report);
+ $filters = $this->validateFilters($inputs, $request);
$format = strtolower((string) $request->get('format', 'csv'));
- $rows = $report->query($filters)->get()->map(fn ($row) => $this->normalizeRow($row));
- $columns = $report->columns();
- $filename = $report->slug().'-'.now()->format('Ymd_His');
+ $query = $this->queryBuilder->build($report, $filters);
+ $rows = $query->get()->map(fn ($row) => $this->normalizeRow($row));
+ $columns = $this->buildColumnsArray($report);
+ $filename = $report->slug.'-'.now()->format('Ymd_His');
if ($format === 'pdf') {
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('reports.pdf.table', [
- 'name' => $report->name(),
+ 'name' => $report->name,
'columns' => $columns,
'rows' => $rows,
]);
@@ -299,6 +314,35 @@ protected function validateFilters(array $inputs, Request $request): array
return $request->validate($rules);
}
+ /**
+ * Build inputs array from report filters.
+ */
+ protected function buildInputsArray(Report $report): array
+ {
+ return $report->filters->map(fn($filter) => [
+ 'key' => $filter->key,
+ 'type' => $filter->type,
+ 'label' => $filter->label,
+ 'nullable' => $filter->nullable,
+ 'default' => $filter->default_value,
+ 'options' => $filter->options,
+ ])->toArray();
+ }
+
+ /**
+ * Build columns array from report columns.
+ */
+ protected function buildColumnsArray(Report $report): array
+ {
+ return $report->columns
+ ->where('visible', true)
+ ->map(fn($col) => [
+ 'key' => $col->key,
+ 'label' => $col->label,
+ ])
+ ->toArray();
+ }
+
/**
* Ensure derived export/display fields exist on row objects.
*/
diff --git a/app/Http/Controllers/Settings/ReportSettingsController.php b/app/Http/Controllers/Settings/ReportSettingsController.php
new file mode 100644
index 0000000..6e09a70
--- /dev/null
+++ b/app/Http/Controllers/Settings/ReportSettingsController.php
@@ -0,0 +1,293 @@
+orderBy('name')->get();
+
+ return Inertia::render('Settings/Reports/Index', [
+ 'reports' => $reports,
+ ]);
+ }
+
+ public function edit(Report $report)
+ {
+ $report->load(['entities', 'columns', 'filters', 'conditions', 'orders']);
+
+ return Inertia::render('Settings/Reports/Edit', [
+ 'report' => $report,
+ ]);
+ }
+
+ public function store(Request $request)
+ {
+ $validated = $request->validate([
+ 'slug' => 'required|string|unique:reports,slug|max:255',
+ 'name' => 'required|string|max:255',
+ 'description' => 'nullable|string',
+ 'category' => 'nullable|string|max:100',
+ 'enabled' => 'boolean',
+ 'order' => 'integer',
+ ]);
+
+ $report = Report::create($validated);
+
+ return redirect()->route('settings.reports.index')
+ ->with('success', 'Report created successfully.');
+ }
+
+ public function update(Request $request, Report $report)
+ {
+ $validated = $request->validate([
+ 'slug' => 'required|string|unique:reports,slug,' . $report->id . '|max:255',
+ 'name' => 'required|string|max:255',
+ 'description' => 'nullable|string',
+ 'category' => 'nullable|string|max:100',
+ 'enabled' => 'boolean',
+ 'order' => 'integer',
+ ]);
+
+ $report->update($validated);
+
+ return redirect()->route('settings.reports.index')
+ ->with('success', 'Report updated successfully.');
+ }
+
+ public function destroy(Report $report)
+ {
+ $report->delete();
+
+ return redirect()->route('settings.reports.index')
+ ->with('success', 'Report deleted successfully.');
+ }
+
+ public function toggleEnabled(Report $report)
+ {
+ $report->update(['enabled' => !$report->enabled]);
+
+ return back()->with('success', 'Report status updated.');
+ }
+
+ // Entity CRUD
+ public function storeEntity(Request $request, Report $report)
+ {
+ $validated = $request->validate([
+ 'model_class' => 'required|string|max:255',
+ 'alias' => 'nullable|string|max:50',
+ 'join_type' => 'required|in:base,join,leftJoin,rightJoin',
+ 'join_first' => 'nullable|string|max:100',
+ 'join_operator' => 'nullable|string|max:10',
+ 'join_second' => 'nullable|string|max:100',
+ 'order' => 'integer',
+ ]);
+
+ $report->entities()->create($validated);
+
+ return back()->with('success', 'Entity added successfully.');
+ }
+
+ public function updateEntity(Request $request, ReportEntity $entity)
+ {
+ $validated = $request->validate([
+ 'model_class' => 'required|string|max:255',
+ 'alias' => 'nullable|string|max:50',
+ 'join_type' => 'required|in:base,join,leftJoin,rightJoin',
+ 'join_first' => 'nullable|string|max:100',
+ 'join_operator' => 'nullable|string|max:10',
+ 'join_second' => 'nullable|string|max:100',
+ 'order' => 'integer',
+ ]);
+
+ $entity->update($validated);
+
+ return back()->with('success', 'Entity updated successfully.');
+ }
+
+ public function destroyEntity(ReportEntity $entity)
+ {
+ $entity->delete();
+
+ return back()->with('success', 'Entity deleted successfully.');
+ }
+
+ // Column CRUD
+ public function storeColumn(Request $request, Report $report)
+ {
+ $validated = $request->validate([
+ 'key' => 'required|string|max:100',
+ 'label' => 'required|string|max:255',
+ 'type' => 'required|string|max:50',
+ 'expression' => 'required|string',
+ 'sortable' => 'boolean',
+ 'visible' => 'boolean',
+ 'order' => 'integer',
+ 'format_options' => 'nullable|array',
+ ]);
+
+ $report->columns()->create($validated);
+
+ return back()->with('success', 'Column added successfully.');
+ }
+
+ public function updateColumn(Request $request, ReportColumn $column)
+ {
+ $validated = $request->validate([
+ 'key' => 'required|string|max:100',
+ 'label' => 'required|string|max:255',
+ 'type' => 'required|string|max:50',
+ 'expression' => 'required|string',
+ 'sortable' => 'boolean',
+ 'visible' => 'boolean',
+ 'order' => 'integer',
+ 'format_options' => 'nullable|array',
+ ]);
+
+ $column->update($validated);
+
+ return back()->with('success', 'Column updated successfully.');
+ }
+
+ public function destroyColumn(ReportColumn $column)
+ {
+ $column->delete();
+
+ return back()->with('success', 'Column deleted successfully.');
+ }
+
+ // Filter CRUD
+ public function storeFilter(Request $request, Report $report)
+ {
+ $validated = $request->validate([
+ 'key' => 'required|string|max:100',
+ 'label' => 'required|string|max:255',
+ 'type' => 'required|string|max:50',
+ 'nullable' => 'boolean',
+ 'default_value' => 'nullable|string',
+ 'options' => 'nullable|array',
+ 'data_source' => 'nullable|string|max:255',
+ 'order' => 'integer',
+ ]);
+
+ $report->filters()->create($validated);
+
+ return back()->with('success', 'Filter added successfully.');
+ }
+
+ public function updateFilter(Request $request, ReportFilter $filter)
+ {
+ $validated = $request->validate([
+ 'key' => 'required|string|max:100',
+ 'label' => 'required|string|max:255',
+ 'type' => 'required|string|max:50',
+ 'nullable' => 'boolean',
+ 'default_value' => 'nullable|string',
+ 'options' => 'nullable|array',
+ 'data_source' => 'nullable|string|max:255',
+ 'order' => 'integer',
+ ]);
+
+ $filter->update($validated);
+
+ return back()->with('success', 'Filter updated successfully.');
+ }
+
+ public function destroyFilter(ReportFilter $filter)
+ {
+ $filter->delete();
+
+ return back()->with('success', 'Filter deleted successfully.');
+ }
+
+ // Condition CRUD
+ public function storeCondition(Request $request, Report $report)
+ {
+ $validated = $request->validate([
+ 'column' => 'required|string|max:255',
+ 'operator' => 'required|string|max:50',
+ 'value_type' => 'required|in:static,filter,expression',
+ 'value' => 'nullable|string',
+ 'filter_key' => 'nullable|string|max:100',
+ 'logical_operator' => 'required|in:AND,OR',
+ 'group_id' => 'nullable|integer',
+ 'order' => 'integer',
+ 'enabled' => 'boolean',
+ ]);
+
+ $report->conditions()->create($validated);
+
+ return back()->with('success', 'Condition added successfully.');
+ }
+
+ public function updateCondition(Request $request, ReportCondition $condition)
+ {
+ $validated = $request->validate([
+ 'column' => 'required|string|max:255',
+ 'operator' => 'required|string|max:50',
+ 'value_type' => 'required|in:static,filter,expression',
+ 'value' => 'nullable|string',
+ 'filter_key' => 'nullable|string|max:100',
+ 'logical_operator' => 'required|in:AND,OR',
+ 'group_id' => 'nullable|integer',
+ 'order' => 'integer',
+ 'enabled' => 'boolean',
+ ]);
+
+ $condition->update($validated);
+
+ return back()->with('success', 'Condition updated successfully.');
+ }
+
+ public function destroyCondition(ReportCondition $condition)
+ {
+ $condition->delete();
+
+ return back()->with('success', 'Condition deleted successfully.');
+ }
+
+ // Order CRUD
+ public function storeOrder(Request $request, Report $report)
+ {
+ $validated = $request->validate([
+ 'column' => 'required|string|max:255',
+ 'direction' => 'required|in:ASC,DESC',
+ 'order' => 'integer',
+ ]);
+
+ $report->orders()->create($validated);
+
+ return back()->with('success', 'Order clause added successfully.');
+ }
+
+ public function updateOrder(Request $request, ReportOrder $order)
+ {
+ $validated = $request->validate([
+ 'column' => 'required|string|max:255',
+ 'direction' => 'required|in:ASC,DESC',
+ 'order' => 'integer',
+ ]);
+
+ $order->update($validated);
+
+ return back()->with('success', 'Order clause updated successfully.');
+ }
+
+ public function destroyOrder(ReportOrder $order)
+ {
+ $order->delete();
+
+ return back()->with('success', 'Order clause deleted successfully.');
+ }
+}
diff --git a/app/Models/Report.php b/app/Models/Report.php
new file mode 100644
index 0000000..05feaae
--- /dev/null
+++ b/app/Models/Report.php
@@ -0,0 +1,48 @@
+ 'boolean',
+ 'order' => 'integer',
+ ];
+
+ public function entities(): HasMany
+ {
+ return $this->hasMany(ReportEntity::class)->orderBy('order');
+ }
+
+ public function columns(): HasMany
+ {
+ return $this->hasMany(ReportColumn::class)->orderBy('order');
+ }
+
+ public function filters(): HasMany
+ {
+ return $this->hasMany(ReportFilter::class)->orderBy('order');
+ }
+
+ public function conditions(): HasMany
+ {
+ return $this->hasMany(ReportCondition::class)->orderBy('order');
+ }
+
+ public function orders(): HasMany
+ {
+ return $this->hasMany(ReportOrder::class)->orderBy('order');
+ }
+}
diff --git a/app/Models/ReportColumn.php b/app/Models/ReportColumn.php
new file mode 100644
index 0000000..6c63ddd
--- /dev/null
+++ b/app/Models/ReportColumn.php
@@ -0,0 +1,33 @@
+ 'boolean',
+ 'visible' => 'boolean',
+ 'order' => 'integer',
+ 'format_options' => 'array',
+ ];
+
+ public function report(): BelongsTo
+ {
+ return $this->belongsTo(Report::class);
+ }
+}
diff --git a/app/Models/ReportCondition.php b/app/Models/ReportCondition.php
new file mode 100644
index 0000000..6195ae4
--- /dev/null
+++ b/app/Models/ReportCondition.php
@@ -0,0 +1,33 @@
+ 'boolean',
+ 'order' => 'integer',
+ 'group_id' => 'integer',
+ ];
+
+ public function report(): BelongsTo
+ {
+ return $this->belongsTo(Report::class);
+ }
+}
diff --git a/app/Models/ReportEntity.php b/app/Models/ReportEntity.php
new file mode 100644
index 0000000..149ff58
--- /dev/null
+++ b/app/Models/ReportEntity.php
@@ -0,0 +1,29 @@
+ 'integer',
+ ];
+
+ public function report(): BelongsTo
+ {
+ return $this->belongsTo(Report::class);
+ }
+}
diff --git a/app/Models/ReportFilter.php b/app/Models/ReportFilter.php
new file mode 100644
index 0000000..a3afd3f
--- /dev/null
+++ b/app/Models/ReportFilter.php
@@ -0,0 +1,32 @@
+ 'boolean',
+ 'order' => 'integer',
+ 'options' => 'array',
+ ];
+
+ public function report(): BelongsTo
+ {
+ return $this->belongsTo(Report::class);
+ }
+}
diff --git a/app/Models/ReportOrder.php b/app/Models/ReportOrder.php
new file mode 100644
index 0000000..6e57217
--- /dev/null
+++ b/app/Models/ReportOrder.php
@@ -0,0 +1,25 @@
+ 'integer',
+ ];
+
+ public function report(): BelongsTo
+ {
+ return $this->belongsTo(Report::class);
+ }
+}
diff --git a/app/Providers/ReportServiceProvider.php b/app/Providers/ReportServiceProvider.php
deleted file mode 100644
index fbc9d88..0000000
--- a/app/Providers/ReportServiceProvider.php
+++ /dev/null
@@ -1,36 +0,0 @@
-app->singleton(ReportRegistry::class, function () {
- $registry = new ReportRegistry;
- // Register built-in reports here
- $registry->register(new FieldJobsCompletedReport);
- $registry->register(new SegmentActivityCountsReport);
- $registry->register(new ActionsDecisionsCountReport);
- $registry->register(new ActivitiesPerPeriodReport);
- $registry->register(new DecisionsCountReport);
- $registry->register(new ActiveContractsReport);
-
- return $registry;
- });
- }
-
- public function boot(): void
- {
- //
- }
-}
diff --git a/app/Reports/ActionsDecisionsCountReport.php b/app/Reports/ActionsDecisionsCountReport.php
deleted file mode 100644
index 70efda3..0000000
--- a/app/Reports/ActionsDecisionsCountReport.php
+++ /dev/null
@@ -1,53 +0,0 @@
- 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
- ['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
- ];
- }
-
- public function columns(): array
- {
- return [
- ['key' => 'action_name', 'label' => 'Dejanje'],
- ['key' => 'decision_name', 'label' => 'Odločitev'],
- ['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
- ];
- }
-
- public function query(array $filters): Builder
- {
- return Activity::query()
- ->leftJoin('actions', 'activities.action_id', '=', 'actions.id')
- ->leftJoin('decisions', 'activities.decision_id', '=', 'decisions.id')
- ->when(! empty($filters['from']), fn ($q) => $q->whereDate('activities.created_at', '>=', $filters['from']))
- ->when(! empty($filters['to']), fn ($q) => $q->whereDate('activities.created_at', '<=', $filters['to']))
- ->groupBy('actions.name', 'decisions.name')
- ->selectRaw("COALESCE(actions.name, '—') as action_name, COALESCE(decisions.name, '—') as decision_name, COUNT(*) as activities_count");
- }
-}
diff --git a/app/Reports/ActiveContractsReport.php b/app/Reports/ActiveContractsReport.php
deleted file mode 100644
index 9623dcc..0000000
--- a/app/Reports/ActiveContractsReport.php
+++ /dev/null
@@ -1,78 +0,0 @@
- 'client_uuid', 'type' => 'select:client', 'label' => 'Stranka', 'nullable' => true],
- ];
- }
-
- public function columns(): array
- {
- return [
- ['key' => 'contract_reference', 'label' => 'Pogodba'],
- ['key' => 'client_name', 'label' => 'Stranka'],
- ['key' => 'person_name', 'label' => 'Zadeva (oseba)'],
- ['key' => 'start_date', 'label' => 'Začetek'],
- ['key' => 'end_date', 'label' => 'Konec'],
- ['key' => 'balance_amount', 'label' => 'Saldo'],
- ];
- }
-
- public function query(array $filters): Builder
- {
- $asOf = now()->toDateString();
-
- return Contract::query()
- ->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
- ->leftJoin('clients', 'client_cases.client_id', '=', 'clients.id')
- ->leftJoin('person as client_people', 'clients.person_id', '=', 'client_people.id')
- ->leftJoin('person as subject_people', 'client_cases.person_id', '=', 'subject_people.id')
- ->leftJoin('accounts', 'contracts.id', '=', 'accounts.contract_id')
- ->when(! empty($filters['client_uuid']), fn ($q) => $q->where('clients.uuid', $filters['client_uuid']))
- // Active as of date: start_date <= as_of (or null) AND (end_date is null OR end_date >= as_of)
- ->where(function ($q) use ($asOf) {
- $q->whereNull('contracts.start_date')
- ->orWhereDate('contracts.start_date', '<=', $asOf);
- })
- ->where(function ($q) use ($asOf) {
- $q->whereNull('contracts.end_date')
- ->orWhereDate('contracts.end_date', '>=', $asOf);
- })
- ->select([
- 'contracts.id',
- 'contracts.start_date',
- 'contracts.end_date',
- ])
- ->addSelect([
- \DB::raw('contracts.reference as contract_reference'),
- \DB::raw('client_people.full_name as client_name'),
- \DB::raw('subject_people.full_name as person_name'),
- \DB::raw('CAST(accounts.balance_amount AS FLOAT) as balance_amount'),
- ])
- ->orderBy('contracts.start_date', 'asc');
- }
-}
diff --git a/app/Reports/ActivitiesPerPeriodReport.php b/app/Reports/ActivitiesPerPeriodReport.php
deleted file mode 100644
index 5d3ec08..0000000
--- a/app/Reports/ActivitiesPerPeriodReport.php
+++ /dev/null
@@ -1,95 +0,0 @@
- 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
- ['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
- ['key' => 'period', 'type' => 'string', 'label' => 'Obdobje (day|week|month)', 'default' => 'day'],
- ];
- }
-
- public function columns(): array
- {
- return [
- ['key' => 'period', 'label' => 'Obdobje'],
- ['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
- ];
- }
-
- public function query(array $filters): Builder
- {
- $periodRaw = $filters['period'] ?? 'day';
- $period = in_array($periodRaw, ['day', 'week', 'month'], true) ? $periodRaw : 'day';
- $driver = DB::getDriverName();
-
- // Build database-compatible period expressions
- if ($driver === 'sqlite') {
- if ($period === 'day') {
- // Use string slice to avoid timezone conversion differences in SQLite
- $selectExpr = DB::raw('SUBSTR(activities.created_at, 1, 10) as period');
- $groupExpr = DB::raw('SUBSTR(activities.created_at, 1, 10)');
- $orderExpr = DB::raw('SUBSTR(activities.created_at, 1, 10)');
- } elseif ($period === 'month') {
- $selectExpr = DB::raw("strftime('%Y-%m-01', activities.created_at) as period");
- $groupExpr = DB::raw("strftime('%Y-%m-01', activities.created_at)");
- $orderExpr = DB::raw("strftime('%Y-%m-01', activities.created_at)");
- } else { // week
- $selectExpr = DB::raw("strftime('%Y-%W', activities.created_at) as period");
- $groupExpr = DB::raw("strftime('%Y-%W', activities.created_at)");
- $orderExpr = DB::raw("strftime('%Y-%W', activities.created_at)");
- }
- } elseif ($driver === 'mysql') {
- if ($period === 'day') {
- $selectExpr = DB::raw('DATE(activities.created_at) as period');
- $groupExpr = DB::raw('DATE(activities.created_at)');
- $orderExpr = DB::raw('DATE(activities.created_at)');
- } elseif ($period === 'month') {
- $selectExpr = DB::raw("DATE_FORMAT(activities.created_at, '%Y-%m-01') as period");
- $groupExpr = DB::raw("DATE_FORMAT(activities.created_at, '%Y-%m-01')");
- $orderExpr = DB::raw("DATE_FORMAT(activities.created_at, '%Y-%m-01')");
- } else { // week
- // ISO week-year-week number for grouping; adequate for summary grouping
- $selectExpr = DB::raw("DATE_FORMAT(activities.created_at, '%x-%v') as period");
- $groupExpr = DB::raw("DATE_FORMAT(activities.created_at, '%x-%v')");
- $orderExpr = DB::raw("DATE_FORMAT(activities.created_at, '%x-%v')");
- }
- } else { // postgres and others supporting date_trunc
- $selectExpr = DB::raw("date_trunc('".$period."', activities.created_at) as period");
- $groupExpr = DB::raw("date_trunc('".$period."', activities.created_at)");
- $orderExpr = DB::raw("date_trunc('".$period."', activities.created_at)");
- }
-
- return Activity::query()
- ->when(! empty($filters['from']), fn ($q) => $q->whereDate('activities.created_at', '>=', $filters['from']))
- ->when(! empty($filters['to']), fn ($q) => $q->whereDate('activities.created_at', '<=', $filters['to']))
- ->groupBy($groupExpr)
- ->orderBy($orderExpr)
- ->select($selectExpr)
- ->selectRaw('COUNT(*) as activities_count');
- }
-}
diff --git a/app/Reports/BaseEloquentReport.php b/app/Reports/BaseEloquentReport.php
deleted file mode 100644
index fbe8cee..0000000
--- a/app/Reports/BaseEloquentReport.php
+++ /dev/null
@@ -1,33 +0,0 @@
- $filters
- */
- public function paginate(array $filters, int $perPage = 25): LengthAwarePaginator
- {
- /** @var EloquentBuilder|QueryBuilder $query */
- $query = $this->query($filters);
-
- return $query->paginate($perPage);
- }
-}
diff --git a/app/Reports/Contracts/Report.php b/app/Reports/Contracts/Report.php
deleted file mode 100644
index 60bf1e3..0000000
--- a/app/Reports/Contracts/Report.php
+++ /dev/null
@@ -1,54 +0,0 @@
- 'from', 'type' => 'date', 'label' => 'Od', 'default' => today()]
- *
- * @return array>
- */
- public function inputs(): array;
-
- /**
- * Return column definitions for the table and exports.
- * Example: [ ['key' => 'id', 'label' => '#'], ['key' => 'user', 'label' => 'Uporabnik'] ]
- *
- * @return array>
- */
- public function columns(): array;
-
- /**
- * Build the data source query for the report based on validated filters.
- * Should return an Eloquent or Query builder.
- *
- * @param array $filters
- * @return EloquentBuilder|QueryBuilder
- */
- public function query(array $filters);
-
- /**
- * Optional per-report authorization logic.
- */
- public function authorize(Request $request): void;
-
- /**
- * Execute the report and return a paginator for UI.
- *
- * @param array $filters
- */
- public function paginate(array $filters, int $perPage = 25): LengthAwarePaginator;
-}
diff --git a/app/Reports/DecisionsCountReport.php b/app/Reports/DecisionsCountReport.php
deleted file mode 100644
index 2fb5dc8..0000000
--- a/app/Reports/DecisionsCountReport.php
+++ /dev/null
@@ -1,51 +0,0 @@
- 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
- ['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
- ];
- }
-
- public function columns(): array
- {
- return [
- ['key' => 'decision_name', 'label' => 'Odločitev'],
- ['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
- ];
- }
-
- public function query(array $filters): Builder
- {
- return Activity::query()
- ->leftJoin('decisions', 'activities.decision_id', '=', 'decisions.id')
- ->when(!empty($filters['from']), fn ($q) => $q->whereDate('activities.created_at', '>=', $filters['from']))
- ->when(!empty($filters['to']), fn ($q) => $q->whereDate('activities.created_at', '<=', $filters['to']))
- ->groupBy('decisions.name')
- ->selectRaw("COALESCE(decisions.name, '—') as decision_name, COUNT(*) as activities_count");
- }
-}
diff --git a/app/Reports/FieldJobsCompletedReport.php b/app/Reports/FieldJobsCompletedReport.php
deleted file mode 100644
index 395aa88..0000000
--- a/app/Reports/FieldJobsCompletedReport.php
+++ /dev/null
@@ -1,60 +0,0 @@
- 'from', 'type' => 'date', 'label' => 'Od', 'default' => now()->startOfMonth()->toDateString()],
- ['key' => 'to', 'type' => 'date', 'label' => 'Do', 'default' => now()->toDateString()],
- ['key' => 'user_id', 'type' => 'select:user', 'label' => 'Uporabnik', 'default' => null],
- ];
- }
-
- public function columns(): array
- {
- return [
- ['key' => 'id', 'label' => '#'],
- ['key' => 'contract_reference', 'label' => 'Pogodba'],
- ['key' => 'assigned_user_name', 'label' => 'Terenski'],
- ['key' => 'completed_at', 'label' => 'Zaključeno'],
- ['key' => 'notes', 'label' => 'Opombe'],
- ];
- }
-
- /**
- * @param array $filters
- */
- public function query(array $filters): EloquentBuilder
- {
- $from = isset($filters['from']) ? now()->parse($filters['from'])->startOfDay() : now()->startOfMonth();
- $to = isset($filters['to']) ? now()->parse($filters['to'])->endOfDay() : now()->endOfDay();
-
- return FieldJob::query()
- ->whereNull('cancelled_at')
- ->whereBetween('completed_at', [$from, $to])
- ->when(! empty($filters['user_id']), fn ($q) => $q->where('assigned_user_id', $filters['user_id']))
- ->with(['assignedUser:id,name', 'contract:id,reference'])
- ->select(['id', 'assigned_user_id', 'contract_id', 'completed_at', 'notes']);
- }
-}
diff --git a/app/Reports/ReportRegistry.php b/app/Reports/ReportRegistry.php
deleted file mode 100644
index d63ca5f..0000000
--- a/app/Reports/ReportRegistry.php
+++ /dev/null
@@ -1,29 +0,0 @@
- */
- protected array $reports = [];
-
- public function register(Report $report): void
- {
- $this->reports[$report->slug()] = $report;
- }
-
- /**
- * @return array
- */
- public function all(): array
- {
- return $this->reports;
- }
-
- public function findBySlug(string $slug): ?Report
- {
- return $this->reports[$slug] ?? null;
- }
-}
diff --git a/app/Reports/SegmentActivityCountsReport.php b/app/Reports/SegmentActivityCountsReport.php
deleted file mode 100644
index f530af1..0000000
--- a/app/Reports/SegmentActivityCountsReport.php
+++ /dev/null
@@ -1,54 +0,0 @@
- 'from', 'type' => 'date', 'label' => 'Od', 'nullable' => true],
- ['key' => 'to', 'type' => 'date', 'label' => 'Do', 'nullable' => true],
- ];
- }
-
- public function columns(): array
- {
- return [
- ['key' => 'segment_name', 'label' => 'Segment'],
- ['key' => 'activities_count', 'label' => 'Št. aktivnosti'],
- ];
- }
-
- public function query(array $filters): Builder
- {
- $q = Activity::query()
- ->join('actions', 'activities.action_id', '=', 'actions.id')
- ->leftJoin('segments', 'actions.segment_id', '=', 'segments.id')
- ->when(! empty($filters['from']), fn ($qq) => $qq->whereDate('activities.created_at', '>=', $filters['from']))
- ->when(! empty($filters['to']), fn ($qq) => $qq->whereDate('activities.created_at', '<=', $filters['to']))
- ->groupBy('segments.name')
- ->selectRaw("COALESCE(segments.name, 'Brez segmenta') as segment_name, COUNT(*) as activities_count");
-
- return $q;
- }
-}
diff --git a/app/Services/Import/Handlers/EmailHandler.php b/app/Services/Import/Handlers/EmailHandler.php
index 3489acb..4cc4bf5 100644
--- a/app/Services/Import/Handlers/EmailHandler.php
+++ b/app/Services/Import/Handlers/EmailHandler.php
@@ -73,30 +73,21 @@ public function process(Import $import, array $mapped, array $raw, array $contex
];
}
- $existing = $this->resolve($mapped, $context);
-
- // Check for duplicates if configured
- if ($this->getOption('deduplicate', true) && $existing) {
- // Update person_id if different
- if ($existing->person_id !== $personId) {
- $existing->person_id = $personId;
- $existing->save();
-
- return [
- 'action' => 'updated',
- 'entity' => $existing,
- 'applied_fields' => ['person_id'],
- ];
- }
+ // Check if this email already exists for THIS person
+ $existing = Email::where('person_id', $personId)
+ ->where('value', strtolower(trim($email)))
+ ->first();
+ // If email already exists for this person, skip
+ if ($existing) {
return [
'action' => 'skipped',
'entity' => $existing,
- 'message' => 'Email already exists',
+ 'message' => 'Email already exists for this person',
];
}
- // Create new email
+ // Create new email for this person
$payload = $this->buildPayload($mapped, new Email);
$payload['person_id'] = $personId;
diff --git a/app/Services/ReportQueryBuilder.php b/app/Services/ReportQueryBuilder.php
new file mode 100644
index 0000000..d2f8fe6
--- /dev/null
+++ b/app/Services/ReportQueryBuilder.php
@@ -0,0 +1,248 @@
+load(['entities', 'columns', 'conditions', 'orders']);
+
+ // 1. Start with base model query
+ $query = $this->buildBaseQuery($report);
+
+ // 2. Apply joins from report_entities
+ $this->applyJoins($query, $report);
+
+ // 3. Select columns from report_columns
+ $this->applySelects($query, $report);
+
+ // 4. Apply GROUP BY if aggregate functions are used
+ $this->applyGroupBy($query, $report);
+
+ // 5. Apply conditions from report_conditions
+ $this->applyConditions($query, $report, $filters);
+
+ // 6. Apply ORDER BY from report_orders
+ $this->applyOrders($query, $report);
+
+ return $query;
+ }
+
+ /**
+ * Build the base query from the first entity.
+ */
+ protected function buildBaseQuery(Report $report): Builder
+ {
+ $baseEntity = $report->entities->firstWhere('join_type', 'base');
+
+ if (!$baseEntity) {
+ throw new \RuntimeException("Report {$report->slug} has no base entity defined.");
+ }
+
+ $modelClass = $baseEntity->model_class;
+
+ if (!class_exists($modelClass)) {
+ throw new \RuntimeException("Model class {$modelClass} does not exist.");
+ }
+
+ return $modelClass::query();
+ }
+
+ /**
+ * Apply joins from report entities.
+ */
+ protected function applyJoins(Builder $query, Report $report): void
+ {
+ $entities = $report->entities->where('join_type', '!=', 'base');
+
+ foreach ($entities as $entity) {
+ $table = $this->getTableFromModel($entity->model_class);
+
+ // Use alias if provided
+ if ($entity->alias) {
+ $table = "{$table} as {$entity->alias}";
+ }
+
+ $joinMethod = $entity->join_type;
+
+ $query->{$joinMethod}(
+ $table,
+ $entity->join_first,
+ $entity->join_operator ?? '=',
+ $entity->join_second
+ );
+ }
+ }
+
+ /**
+ * Apply column selections.
+ */
+ protected function applySelects(Builder $query, Report $report): void
+ {
+ $columns = $report->columns
+ ->where('visible', true)
+ ->map(fn($col) => DB::raw("{$col->expression} as {$col->key}"))
+ ->toArray();
+
+ if (!empty($columns)) {
+ $query->select($columns);
+ }
+ }
+
+ /**
+ * Apply GROUP BY clause if aggregate functions are detected.
+ */
+ protected function applyGroupBy(Builder $query, Report $report): void
+ {
+ $visibleColumns = $report->columns->where('visible', true);
+
+ // Check if any column uses aggregate functions
+ $hasAggregates = $visibleColumns->contains(function ($col) {
+ return preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression);
+ });
+
+ if (!$hasAggregates) {
+ return; // No aggregates, no grouping needed
+ }
+
+ // Find non-aggregate columns that need to be in GROUP BY
+ $groupByColumns = $visibleColumns
+ ->filter(function ($col) {
+ // Check if this column does NOT use an aggregate function
+ return !preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression);
+ })
+ ->map(function ($col) {
+ // Extract the actual column expression (before any COALESCE, CAST, etc.)
+ // For COALESCE(segments.name, 'default'), we need segments.name
+ if (preg_match('/COALESCE\s*\(\s*([^,]+)\s*,/i', $col->expression, $matches)) {
+ return trim($matches[1]);
+ }
+ // For simple columns, use as-is
+ return $col->expression;
+ })
+ ->filter() // Remove empty values
+ ->values()
+ ->toArray();
+
+ if (!empty($groupByColumns)) {
+ foreach ($groupByColumns as $column) {
+ $query->groupBy(DB::raw($column));
+ }
+ }
+ }
+
+ /**
+ * Apply conditions (WHERE clauses).
+ */
+ protected function applyConditions(Builder $query, Report $report, array $filters): void
+ {
+ $conditions = $report->conditions->where('enabled', true);
+
+ // Group conditions by group_id
+ $groups = $conditions->groupBy('group_id');
+
+ foreach ($groups as $groupId => $groupConditions) {
+ $query->where(function ($subQuery) use ($groupConditions, $filters) {
+ foreach ($groupConditions as $condition) {
+ $value = $this->resolveConditionValue($condition, $filters);
+
+ // Skip if filter-based and no value provided
+ if ($condition->value_type === 'filter' && $value === null) {
+ continue;
+ }
+
+ $method = $condition->logical_operator === 'OR' ? 'orWhere' : 'where';
+
+ $this->applyCondition($subQuery, $condition, $value, $method);
+ }
+ });
+ }
+ }
+
+ /**
+ * Apply a single condition to the query.
+ */
+ protected function applyCondition(Builder $query, $condition, $value, string $method): void
+ {
+ $column = $condition->column;
+ $operator = strtoupper($condition->operator);
+
+ switch ($operator) {
+ case 'IS NULL':
+ $query->{$method . 'Null'}($column);
+ break;
+
+ case 'IS NOT NULL':
+ $query->{$method . 'NotNull'}($column);
+ break;
+
+ case 'IN':
+ $values = is_array($value) ? $value : explode(',', $value);
+ $query->{$method . 'In'}($column, $values);
+ break;
+
+ case 'NOT IN':
+ $values = is_array($value) ? $value : explode(',', $value);
+ $query->{$method . 'NotIn'}($column, $values);
+ break;
+
+ case 'BETWEEN':
+ if (is_array($value) && count($value) === 2) {
+ $query->{$method . 'Between'}($column, $value);
+ }
+ break;
+
+ case 'LIKE':
+ $query->{$method}($column, 'LIKE', $value);
+ break;
+
+ default:
+ $query->{$method}($column, $operator, $value);
+ break;
+ }
+ }
+
+ /**
+ * Resolve condition value based on value_type.
+ */
+ protected function resolveConditionValue($condition, array $filters)
+ {
+ return match ($condition->value_type) {
+ 'static' => $condition->value,
+ 'filter' => $filters[$condition->filter_key] ?? null,
+ 'expression' => DB::raw($condition->value),
+ default => null,
+ };
+ }
+
+ /**
+ * Apply ORDER BY clauses.
+ */
+ protected function applyOrders(Builder $query, Report $report): void
+ {
+ foreach ($report->orders as $order) {
+ $query->orderBy($order->column, $order->direction);
+ }
+ }
+
+ /**
+ * Get table name from model class.
+ */
+ protected function getTableFromModel(string $modelClass): string
+ {
+ if (!class_exists($modelClass)) {
+ throw new \RuntimeException("Model class {$modelClass} does not exist.");
+ }
+
+ return (new $modelClass)->getTable();
+ }
+}
diff --git a/bootstrap/providers.php b/bootstrap/providers.php
index 460af4e..67d2d5f 100644
--- a/bootstrap/providers.php
+++ b/bootstrap/providers.php
@@ -5,5 +5,4 @@
App\Providers\AuthServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
App\Providers\JetstreamServiceProvider::class,
- App\Providers\ReportServiceProvider::class,
];
diff --git a/clean-duplicates.php b/clean-duplicates.php
new file mode 100644
index 0000000..8602d0f
--- /dev/null
+++ b/clean-duplicates.php
@@ -0,0 +1,147 @@
+make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
+
+echo "=== Checking for duplicates ===\n\n";
+
+// Check Actions table
+echo "ACTIONS TABLE:\n";
+echo "-------------\n";
+$actionDuplicates = DB::table('actions')
+ ->select('name', DB::raw('COUNT(*) as total_count'))
+ ->groupBy('name')
+ ->havingRaw('COUNT(*) > 1')
+ ->get();
+
+if ($actionDuplicates->count() > 0) {
+ echo "Found duplicate actions:\n";
+ foreach ($actionDuplicates as $dup) {
+ echo " - '{$dup->name}' appears {$dup->total_count} times\n";
+
+ // Get all IDs for this name
+ $records = DB::table('actions')
+ ->where('name', $dup->name)
+ ->orderBy('id')
+ ->get(['id', 'name', 'created_at']);
+
+ echo " IDs: ";
+ foreach ($records as $record) {
+ echo $record->id . " ";
+ }
+ echo "\n";
+ }
+} else {
+ echo "No duplicates found.\n";
+}
+
+echo "\n";
+
+// Check Decisions table
+echo "DECISIONS TABLE:\n";
+echo "---------------\n";
+$decisionDuplicates = DB::table('decisions')
+ ->select('name', DB::raw('COUNT(*) as total_count'))
+ ->groupBy('name')
+ ->havingRaw('COUNT(*) > 1')
+ ->get();
+
+if ($decisionDuplicates->count() > 0) {
+ echo "Found duplicate decisions:\n";
+ foreach ($decisionDuplicates as $dup) {
+ echo " - '{$dup->name}' appears {$dup->total_count} times\n";
+
+ // Get all IDs for this name
+ $records = DB::table('decisions')
+ ->where('name', $dup->name)
+ ->orderBy('id')
+ ->get(['id', 'name', 'created_at']);
+
+ echo " IDs: ";
+ foreach ($records as $record) {
+ echo $record->id . " ";
+ }
+ echo "\n";
+ }
+} else {
+ echo "No duplicates found.\n";
+}
+
+echo "\n=== Removing duplicates ===\n\n";
+
+// Remove duplicate actions (keep the first one)
+if ($actionDuplicates->count() > 0) {
+ foreach ($actionDuplicates as $dup) {
+ $records = DB::table('actions')
+ ->where('name', $dup->name)
+ ->orderBy('id')
+ ->get(['id']);
+
+ // Keep the first, delete the rest
+ $toDelete = $records->skip(1)->pluck('id')->toArray();
+
+ if (count($toDelete) > 0) {
+ DB::table('actions')->whereIn('id', $toDelete)->delete();
+ echo "Deleted duplicate actions for '{$dup->name}': IDs " . implode(', ', $toDelete) . "\n";
+ }
+ }
+}
+
+// Remove duplicate decisions (keep the first one)
+if ($decisionDuplicates->count() > 0) {
+ foreach ($decisionDuplicates as $dup) {
+ $records = DB::table('decisions')
+ ->where('name', $dup->name)
+ ->orderBy('id')
+ ->get(['id']);
+
+ // Keep the first, delete the rest
+ $toDelete = $records->skip(1)->pluck('id')->toArray();
+
+ if (count($toDelete) > 0) {
+ DB::table('decisions')->whereIn('id', $toDelete)->delete();
+ echo "Deleted duplicate decisions for '{$dup->name}': IDs " . implode(', ', $toDelete) . "\n";
+ }
+ }
+}
+
+echo "\n";
+
+// Check and clean action_decision pivot table
+echo "ACTION_DECISION PIVOT TABLE:\n";
+echo "---------------------------\n";
+
+// Find duplicates in pivot table
+$pivotDuplicates = DB::table('action_decision')
+ ->select('action_id', 'decision_id', DB::raw('COUNT(*) as total_count'))
+ ->groupBy('action_id', 'decision_id')
+ ->havingRaw('COUNT(*) > 1')
+ ->get();
+
+if ($pivotDuplicates->count() > 0) {
+ echo "Found duplicate pivot entries:\n";
+ foreach ($pivotDuplicates as $dup) {
+ echo " - action_id: {$dup->action_id}, decision_id: {$dup->decision_id} appears {$dup->total_count} times\n";
+
+ // Get all IDs for this combination
+ $records = DB::table('action_decision')
+ ->where('action_id', $dup->action_id)
+ ->where('decision_id', $dup->decision_id)
+ ->orderBy('id')
+ ->get(['id']);
+
+ // Keep the first, delete the rest
+ $toDelete = $records->skip(1)->pluck('id')->toArray();
+
+ if (count($toDelete) > 0) {
+ DB::table('action_decision')->whereIn('id', $toDelete)->delete();
+ echo " Deleted duplicate pivot entries: IDs " . implode(', ', $toDelete) . "\n";
+ }
+ }
+} else {
+ echo "No duplicates found.\n";
+}
+
+echo "\n=== Cleanup complete ===\n";
diff --git a/database/migrations/2025_12_28_210222_create_reports_table.php b/database/migrations/2025_12_28_210222_create_reports_table.php
new file mode 100644
index 0000000..d56e5dc
--- /dev/null
+++ b/database/migrations/2025_12_28_210222_create_reports_table.php
@@ -0,0 +1,36 @@
+id();
+ $table->string('slug')->unique();
+ $table->string('name');
+ $table->text('description')->nullable();
+ $table->string('category', 100)->nullable();
+ $table->boolean('enabled')->default(true);
+ $table->integer('order')->default(0);
+ $table->timestamps();
+
+ $table->index('slug');
+ $table->index(['enabled', 'order']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('reports');
+ }
+};
diff --git a/database/migrations/2025_12_28_210230_create_report_columns_table.php b/database/migrations/2025_12_28_210230_create_report_columns_table.php
new file mode 100644
index 0000000..e4f5bbb
--- /dev/null
+++ b/database/migrations/2025_12_28_210230_create_report_columns_table.php
@@ -0,0 +1,38 @@
+id();
+ $table->foreignId('report_id')->constrained()->cascadeOnDelete();
+ $table->string('key', 100);
+ $table->string('label');
+ $table->string('type', 50)->default('string');
+ $table->text('expression');
+ $table->boolean('sortable')->default(true);
+ $table->boolean('visible')->default(true);
+ $table->integer('order')->default(0);
+ $table->json('format_options')->nullable();
+ $table->timestamps();
+
+ $table->index(['report_id', 'order']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('report_columns');
+ }
+};
diff --git a/database/migrations/2025_12_28_210230_create_report_entities_table.php b/database/migrations/2025_12_28_210230_create_report_entities_table.php
new file mode 100644
index 0000000..51b4c15
--- /dev/null
+++ b/database/migrations/2025_12_28_210230_create_report_entities_table.php
@@ -0,0 +1,37 @@
+id();
+ $table->foreignId('report_id')->constrained()->cascadeOnDelete();
+ $table->string('model_class');
+ $table->string('alias', 50)->nullable();
+ $table->enum('join_type', ['base', 'join', 'leftJoin', 'rightJoin'])->default('base');
+ $table->string('join_first', 100)->nullable();
+ $table->string('join_operator', 10)->nullable();
+ $table->string('join_second', 100)->nullable();
+ $table->integer('order')->default(0);
+ $table->timestamps();
+
+ $table->index(['report_id', 'order']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('report_entities');
+ }
+};
diff --git a/database/migrations/2025_12_28_210230_create_report_filters_table.php b/database/migrations/2025_12_28_210230_create_report_filters_table.php
new file mode 100644
index 0000000..c27009a
--- /dev/null
+++ b/database/migrations/2025_12_28_210230_create_report_filters_table.php
@@ -0,0 +1,38 @@
+id();
+ $table->foreignId('report_id')->constrained()->cascadeOnDelete();
+ $table->string('key', 100);
+ $table->string('label');
+ $table->string('type', 50);
+ $table->boolean('nullable')->default(true);
+ $table->text('default_value')->nullable();
+ $table->json('options')->nullable();
+ $table->string('data_source')->nullable();
+ $table->integer('order')->default(0);
+ $table->timestamps();
+
+ $table->index(['report_id', 'order']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('report_filters');
+ }
+};
diff --git a/database/migrations/2025_12_28_210231_create_report_conditions_table.php b/database/migrations/2025_12_28_210231_create_report_conditions_table.php
new file mode 100644
index 0000000..8933ec6
--- /dev/null
+++ b/database/migrations/2025_12_28_210231_create_report_conditions_table.php
@@ -0,0 +1,39 @@
+id();
+ $table->foreignId('report_id')->constrained()->cascadeOnDelete();
+ $table->string('column');
+ $table->string('operator', 50);
+ $table->string('value_type', 50);
+ $table->text('value')->nullable();
+ $table->string('filter_key', 100)->nullable();
+ $table->enum('logical_operator', ['AND', 'OR'])->default('AND');
+ $table->integer('group_id')->nullable();
+ $table->integer('order')->default(0);
+ $table->boolean('enabled')->default(true);
+ $table->timestamps();
+
+ $table->index(['report_id', 'group_id', 'order']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('report_conditions');
+ }
+};
diff --git a/database/migrations/2025_12_28_210231_create_report_orders_table.php b/database/migrations/2025_12_28_210231_create_report_orders_table.php
new file mode 100644
index 0000000..bbf8179
--- /dev/null
+++ b/database/migrations/2025_12_28_210231_create_report_orders_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->foreignId('report_id')->constrained()->cascadeOnDelete();
+ $table->string('column');
+ $table->enum('direction', ['ASC', 'DESC'])->default('ASC');
+ $table->integer('order')->default(0);
+ $table->timestamps();
+
+ $table->index(['report_id', 'order']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('report_orders');
+ }
+};
diff --git a/database/seeders/ReportsSeeder.php b/database/seeders/ReportsSeeder.php
new file mode 100644
index 0000000..e9c11c3
--- /dev/null
+++ b/database/seeders/ReportsSeeder.php
@@ -0,0 +1,786 @@
+seedActiveContractsReport();
+ $this->seedFieldJobsCompletedReport();
+ $this->seedDecisionsCountReport();
+ $this->seedSegmentActivityCountsReport();
+ $this->seedActionsDecisionsCountReport();
+ $this->seedActivitiesPerPeriodReport();
+ }
+
+ protected function seedActiveContractsReport(): void
+ {
+ $report = Report::create([
+ 'slug' => 'active-contracts',
+ 'name' => 'Aktivne pogodbe',
+ 'description' => 'Pogodbe, ki so aktivne na izbrani dan, z možnostjo filtriranja po stranki.',
+ 'category' => 'contracts',
+ 'enabled' => true,
+ 'order' => 1,
+ ]);
+
+ // Entities (joins)
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\Contract',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ]);
+
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\ClientCase',
+ 'join_type' => 'join',
+ 'join_first' => 'contracts.client_case_id',
+ 'join_operator' => '=',
+ 'join_second' => 'client_cases.id',
+ 'order' => 1,
+ ]);
+
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\Client',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'client_cases.client_id',
+ 'join_operator' => '=',
+ 'join_second' => 'clients.id',
+ 'order' => 2,
+ ]);
+
+ $report->entities()->createMany([
+ [
+ 'model_class' => 'App\\Models\\Person\\Person',
+ 'alias' => 'client_people',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'clients.person_id',
+ 'join_operator' => '=',
+ 'join_second' => 'client_people.id',
+ 'order' => 3,
+ ],
+ [
+ 'model_class' => 'App\\Models\\Person\\Person',
+ 'alias' => 'subject_people',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'client_cases.person_id',
+ 'join_operator' => '=',
+ 'join_second' => 'subject_people.id',
+ 'order' => 4,
+ ],
+ ]);
+
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\Account',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'contracts.id',
+ 'join_operator' => '=',
+ 'join_second' => 'accounts.contract_id',
+ 'order' => 5,
+ ]);
+
+ // Columns
+ $report->columns()->createMany([
+ [
+ 'key' => 'contract_reference',
+ 'label' => 'Pogodba',
+ 'type' => 'string',
+ 'expression' => 'contracts.reference',
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'client_name',
+ 'label' => 'Stranka',
+ 'type' => 'string',
+ 'expression' => 'client_people.full_name',
+ 'order' => 1,
+ ],
+ [
+ 'key' => 'person_name',
+ 'label' => 'Zadeva (oseba)',
+ 'type' => 'string',
+ 'expression' => 'subject_people.full_name',
+ 'order' => 2,
+ ],
+ [
+ 'key' => 'start_date',
+ 'label' => 'Začetek',
+ 'type' => 'date',
+ 'expression' => 'contracts.start_date',
+ 'order' => 3,
+ ],
+ [
+ 'key' => 'end_date',
+ 'label' => 'Konec',
+ 'type' => 'date',
+ 'expression' => 'contracts.end_date',
+ 'order' => 4,
+ ],
+ [
+ 'key' => 'balance_amount',
+ 'label' => 'Saldo',
+ 'type' => 'currency',
+ 'expression' => 'CAST(accounts.balance_amount AS FLOAT)',
+ 'order' => 5,
+ ],
+ ]);
+
+ // Filters
+ $report->filters()->create([
+ 'key' => 'client_uuid',
+ 'label' => 'Stranka',
+ 'type' => 'select:client',
+ 'nullable' => true,
+ 'order' => 0,
+ ]);
+
+ // Conditions - Active as of today
+ $asOf = 'CURRENT_DATE';
+
+ // start_date <= as_of (or null)
+ $report->conditions()->create([
+ 'column' => 'contracts.start_date',
+ 'operator' => '<=',
+ 'value_type' => 'expression',
+ 'value' => $asOf,
+ 'logical_operator' => 'OR',
+ 'group_id' => 1,
+ 'order' => 0,
+ ]);
+
+ $report->conditions()->create([
+ 'column' => 'contracts.start_date',
+ 'operator' => 'IS NULL',
+ 'value_type' => 'static',
+ 'logical_operator' => 'OR',
+ 'group_id' => 1,
+ 'order' => 1,
+ ]);
+
+ // end_date >= as_of (or null)
+ $report->conditions()->create([
+ 'column' => 'contracts.end_date',
+ 'operator' => '>=',
+ 'value_type' => 'expression',
+ 'value' => $asOf,
+ 'logical_operator' => 'OR',
+ 'group_id' => 2,
+ 'order' => 0,
+ ]);
+
+ $report->conditions()->create([
+ 'column' => 'contracts.end_date',
+ 'operator' => 'IS NULL',
+ 'value_type' => 'static',
+ 'logical_operator' => 'OR',
+ 'group_id' => 2,
+ 'order' => 1,
+ ]);
+
+ // client_uuid filter condition
+ $report->conditions()->create([
+ 'column' => 'clients.uuid',
+ 'operator' => '=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'client_uuid',
+ 'logical_operator' => 'AND',
+ 'group_id' => 3,
+ 'order' => 0,
+ ]);
+
+ // Orders
+ $report->orders()->create([
+ 'column' => 'contracts.start_date',
+ 'direction' => 'ASC',
+ 'order' => 0,
+ ]);
+ }
+
+ protected function seedFieldJobsCompletedReport(): void
+ {
+ $report = Report::create([
+ 'slug' => 'field-jobs-completed',
+ 'name' => 'Zaključeni tereni',
+ 'description' => 'Pregled zaključenih terenov po datumu in uporabniku.',
+ 'category' => 'field',
+ 'enabled' => true,
+ 'order' => 2,
+ ]);
+
+ // Base entity
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\FieldJob',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ]);
+
+ // Join contracts table
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\Contract',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'field_jobs.contract_id',
+ 'join_operator' => '=',
+ 'join_second' => 'contracts.id',
+ 'order' => 1,
+ ]);
+
+ // Join users table
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\User',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'field_jobs.assigned_user_id',
+ 'join_operator' => '=',
+ 'join_second' => 'users.id',
+ 'order' => 2,
+ ]);
+
+ // Columns
+ $report->columns()->createMany([
+ [
+ 'key' => 'id',
+ 'label' => '#',
+ 'type' => 'number',
+ 'expression' => 'field_jobs.id',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'contract_reference',
+ 'label' => 'Pogodba',
+ 'type' => 'string',
+ 'expression' => 'contracts.reference',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 1,
+ ],
+ [
+ 'key' => 'assigned_user_name',
+ 'label' => 'Terenski',
+ 'type' => 'string',
+ 'expression' => 'users.name',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 2,
+ ],
+ [
+ 'key' => 'completed_at',
+ 'label' => 'Zaključeno',
+ 'type' => 'date',
+ 'expression' => 'field_jobs.completed_at',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 3,
+ ],
+ [
+ 'key' => 'notes',
+ 'label' => 'Opombe',
+ 'type' => 'string',
+ 'expression' => 'field_jobs.notes',
+ 'sortable' => false,
+ 'visible' => true,
+ 'order' => 4,
+ ],
+ ]);
+
+ // Filters
+ $report->filters()->createMany([
+ [
+ 'key' => 'from',
+ 'label' => 'Od',
+ 'type' => 'date',
+ 'nullable' => false,
+ 'default_value' => now()->startOfMonth()->toDateString(),
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'to',
+ 'label' => 'Do',
+ 'type' => 'date',
+ 'nullable' => false,
+ 'default_value' => now()->toDateString(),
+ 'order' => 1,
+ ],
+ [
+ 'key' => 'user_id',
+ 'label' => 'Uporabnik',
+ 'type' => 'select:user',
+ 'nullable' => true,
+ 'order' => 2,
+ ],
+ ]);
+
+ // Conditions
+ $report->conditions()->createMany([
+ [
+ 'column' => 'field_jobs.cancelled_at',
+ 'operator' => 'IS NULL',
+ 'value_type' => 'static',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 0,
+ 'enabled' => true,
+ ],
+ [
+ 'column' => 'field_jobs.completed_at',
+ 'operator' => 'BETWEEN',
+ 'value_type' => 'filter',
+ 'filter_key' => 'from,to',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 1,
+ 'enabled' => true,
+ ],
+ [
+ 'column' => 'field_jobs.assigned_user_id',
+ 'operator' => '=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'user_id',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 2,
+ 'enabled' => true,
+ ],
+ ]);
+
+ // Order
+ $report->orders()->create([
+ 'column' => 'field_jobs.completed_at',
+ 'direction' => 'DESC',
+ 'order' => 0,
+ ]);
+ }
+
+ protected function seedDecisionsCountReport(): void
+ {
+ $report = Report::create([
+ 'slug' => 'decisions-counts',
+ 'name' => 'Odločitve – štetje',
+ 'description' => 'Število aktivnosti po odločitvah v izbranem obdobju.',
+ 'category' => 'activities',
+ 'enabled' => true,
+ 'order' => 3,
+ ]);
+
+ // Entities
+ $report->entities()->createMany([
+ [
+ 'model_class' => 'App\\Models\\Activity',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ],
+ [
+ 'model_class' => 'App\\Models\\Decision',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'activities.decision_id',
+ 'join_operator' => '=',
+ 'join_second' => 'decisions.id',
+ 'order' => 1,
+ ],
+ ]);
+
+ // Columns
+ $report->columns()->createMany([
+ [
+ 'key' => 'decision_name',
+ 'label' => 'Odločitev',
+ 'type' => 'string',
+ 'expression' => "COALESCE(decisions.name, '—')",
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'activities_count',
+ 'label' => 'Št. aktivnosti',
+ 'type' => 'number',
+ 'expression' => 'COUNT(*)',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 1,
+ ],
+ ]);
+
+ // Filters
+ $report->filters()->createMany([
+ [
+ 'key' => 'from',
+ 'label' => 'Od',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'to',
+ 'label' => 'Do',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 1,
+ ],
+ ]);
+
+ // Conditions
+ $report->conditions()->createMany([
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '>=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'from',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 0,
+ 'enabled' => true,
+ ],
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '<=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'to',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 1,
+ 'enabled' => true,
+ ],
+ ]);
+
+ // Order
+ $report->orders()->create([
+ 'column' => 'activities_count',
+ 'direction' => 'DESC',
+ 'order' => 0,
+ ]);
+ }
+
+ protected function seedSegmentActivityCountsReport(): void
+ {
+ $report = Report::create([
+ 'slug' => 'segment-activity-counts',
+ 'name' => 'Aktivnosti po segmentih',
+ 'description' => 'Število aktivnosti po segmentih v izbranem obdobju (glede na segment dejanja).',
+ 'category' => 'activities',
+ 'enabled' => true,
+ 'order' => 4,
+ ]);
+
+ // Entities
+ $report->entities()->createMany([
+ [
+ 'model_class' => 'App\\Models\\Activity',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ],
+ [
+ 'model_class' => 'App\\Models\\Action',
+ 'join_type' => 'join',
+ 'join_first' => 'activities.action_id',
+ 'join_operator' => '=',
+ 'join_second' => 'actions.id',
+ 'order' => 1,
+ ],
+ [
+ 'model_class' => 'App\\Models\\Segment',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'actions.segment_id',
+ 'join_operator' => '=',
+ 'join_second' => 'segments.id',
+ 'order' => 2,
+ ],
+ ]);
+
+ // Columns
+ $report->columns()->createMany([
+ [
+ 'key' => 'segment_name',
+ 'label' => 'Segment',
+ 'type' => 'string',
+ 'expression' => "COALESCE(segments.name, 'Brez segmenta')",
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'activities_count',
+ 'label' => 'Št. aktivnosti',
+ 'type' => 'number',
+ 'expression' => 'COUNT(*)',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 1,
+ ],
+ ]);
+
+ // Filters
+ $report->filters()->createMany([
+ [
+ 'key' => 'from',
+ 'label' => 'Od',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'to',
+ 'label' => 'Do',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 1,
+ ],
+ ]);
+
+ // Conditions
+ $report->conditions()->createMany([
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '>=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'from',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 0,
+ 'enabled' => true,
+ ],
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '<=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'to',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 1,
+ 'enabled' => true,
+ ],
+ ]);
+
+ // Order
+ $report->orders()->create([
+ 'column' => 'activities_count',
+ 'direction' => 'DESC',
+ 'order' => 0,
+ ]);
+ }
+
+ protected function seedActionsDecisionsCountReport(): void
+ {
+ $report = Report::create([
+ 'slug' => 'actions-decisions-counts',
+ 'name' => 'Dejanja / Odločitve – štetje',
+ 'description' => 'Število aktivnosti po dejanjih in odločitvah v obdobju.',
+ 'category' => 'activities',
+ 'enabled' => true,
+ 'order' => 5,
+ ]);
+
+ // Entities
+ $report->entities()->createMany([
+ [
+ 'model_class' => 'App\\Models\\Activity',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ],
+ [
+ 'model_class' => 'App\\Models\\Action',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'activities.action_id',
+ 'join_operator' => '=',
+ 'join_second' => 'actions.id',
+ 'order' => 1,
+ ],
+ [
+ 'model_class' => 'App\\Models\\Decision',
+ 'join_type' => 'leftJoin',
+ 'join_first' => 'activities.decision_id',
+ 'join_operator' => '=',
+ 'join_second' => 'decisions.id',
+ 'order' => 2,
+ ],
+ ]);
+
+ // Columns
+ $report->columns()->createMany([
+ [
+ 'key' => 'action_name',
+ 'label' => 'Dejanje',
+ 'type' => 'string',
+ 'expression' => "COALESCE(actions.name, '—')",
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'decision_name',
+ 'label' => 'Odločitev',
+ 'type' => 'string',
+ 'expression' => "COALESCE(decisions.name, '—')",
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 1,
+ ],
+ [
+ 'key' => 'activities_count',
+ 'label' => 'Št. aktivnosti',
+ 'type' => 'number',
+ 'expression' => 'COUNT(*)',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 2,
+ ],
+ ]);
+
+ // Filters
+ $report->filters()->createMany([
+ [
+ 'key' => 'from',
+ 'label' => 'Od',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'to',
+ 'label' => 'Do',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 1,
+ ],
+ ]);
+
+ // Conditions
+ $report->conditions()->createMany([
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '>=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'from',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 0,
+ 'enabled' => true,
+ ],
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '<=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'to',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 1,
+ 'enabled' => true,
+ ],
+ ]);
+
+ // Order
+ $report->orders()->create([
+ 'column' => 'activities_count',
+ 'direction' => 'DESC',
+ 'order' => 0,
+ ]);
+ }
+
+ protected function seedActivitiesPerPeriodReport(): void
+ {
+ $report = Report::create([
+ 'slug' => 'activities-per-period',
+ 'name' => 'Aktivnosti po obdobjih',
+ 'description' => 'Seštevek aktivnosti po dneh/tednih/mesecih v obdobju.',
+ 'category' => 'activities',
+ 'enabled' => true,
+ 'order' => 6,
+ ]);
+
+ // Base entity
+ $report->entities()->create([
+ 'model_class' => 'App\\Models\\Activity',
+ 'join_type' => 'base',
+ 'order' => 0,
+ ]);
+
+ // Columns (simplified - period grouping handled in ReportQueryBuilder or controller)
+ $report->columns()->createMany([
+ [
+ 'key' => 'period',
+ 'label' => 'Obdobje',
+ 'type' => 'string',
+ 'expression' => 'DATE(activities.created_at)',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'activities_count',
+ 'label' => 'Št. aktivnosti',
+ 'type' => 'number',
+ 'expression' => 'COUNT(*)',
+ 'sortable' => true,
+ 'visible' => true,
+ 'order' => 1,
+ ],
+ ]);
+
+ // Filters
+ $report->filters()->createMany([
+ [
+ 'key' => 'from',
+ 'label' => 'Od',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 0,
+ ],
+ [
+ 'key' => 'to',
+ 'label' => 'Do',
+ 'type' => 'date',
+ 'nullable' => true,
+ 'order' => 1,
+ ],
+ [
+ 'key' => 'period',
+ 'label' => 'Obdobje (day/week/month)',
+ 'type' => 'string',
+ 'nullable' => false,
+ 'default_value' => 'day',
+ 'order' => 2,
+ ],
+ ]);
+
+ // Conditions
+ $report->conditions()->createMany([
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '>=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'from',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 0,
+ 'enabled' => true,
+ ],
+ [
+ 'column' => 'activities.created_at',
+ 'operator' => '<=',
+ 'value_type' => 'filter',
+ 'filter_key' => 'to',
+ 'logical_operator' => 'AND',
+ 'group_id' => 1,
+ 'order' => 1,
+ 'enabled' => true,
+ ],
+ ]);
+
+ // Order
+ $report->orders()->create([
+ 'column' => 'period',
+ 'direction' => 'ASC',
+ 'order' => 0,
+ ]);
+ }
+}
diff --git a/resources/js/Components/DataTable/DataTableClient.vue b/resources/js/Components/DataTable/DataTableClient.vue
index 37bdfdd..b7a440d 100644
--- a/resources/js/Components/DataTable/DataTableClient.vue
+++ b/resources/js/Components/DataTable/DataTableClient.vue
@@ -2,6 +2,7 @@
import { computed, ref, watch } from "vue";
import SkeletonTable from "@/Components/Skeleton/SkeletonTable.vue";
import EmptyState from "@/Components/EmptyState.vue";
+import DataTablePaginationClient from "@/Components/DataTable/DataTablePaginationClient.vue";
import {
Table,
TableHeader,
@@ -10,6 +11,8 @@ import {
TableRow,
TableCell,
} from "@/Components/ui/table";
+import { Button } from "../ui/button";
+import { ArrowDownNarrowWide, ArrowUpWideNarrowIcon } from "lucide-vue-next";
const props = defineProps({
columns: { type: Array, required: true }, // [{ key, label, sortable?, align?, class?, formatter? }]
@@ -29,6 +32,7 @@ const props = defineProps({
rowKey: { type: [String, Function], default: "id" },
showToolbar: { type: Boolean, default: true },
// Pagination UX options
+ showPagination: { type: Boolean, default: true },
showPageStats: { type: Boolean, default: true },
showGoto: { type: Boolean, default: true },
maxPageLinks: { type: Number, default: 5 }, // odd number preferred
@@ -139,29 +143,6 @@ const pageRows = computed(() => sortedRows.value.slice(startIndex.value, endInde
const showingFrom = computed(() => (total.value === 0 ? 0 : startIndex.value + 1));
const showingTo = computed(() => (total.value === 0 ? 0 : endIndex.value));
-const gotoInput = ref("");
-function goToPageInput() {
- const raw = String(gotoInput.value || "").trim();
- const n = Number(raw);
- if (!Number.isFinite(n)) return;
- const target = Math.max(1, Math.min(lastPage.value, Math.floor(n)));
- if (target !== currentPage.value) setPage(target);
- gotoInput.value = "";
-}
-
-const visiblePages = computed(() => {
- const pages = [];
- const count = lastPage.value;
- if (count <= 1) return [1];
- const windowSize = Math.max(3, props.maxPageLinks);
- const half = Math.floor(windowSize / 2);
- let start = Math.max(1, currentPage.value - half);
- let end = Math.min(count, start + windowSize - 1);
- start = Math.max(1, Math.min(start, end - windowSize + 1));
- for (let p = start; p <= end; p++) pages.push(p);
- return pages;
-});
-
function setPage(p) {
emit("update:page", Math.min(Math.max(1, p), lastPage.value));
}
@@ -196,28 +177,26 @@ function setPageSize(ps) {
-
-
-
+
+
+
-
{{ col.label }}
- ▲
+
▼
-
+ >
+
{{ col.label }}
@@ -232,11 +211,7 @@ function setPageSize(ps) {
@click="$emit('row:click', row)"
class="cursor-default hover:bg-gray-50/50"
>
-
+
-
+
@@ -286,112 +258,19 @@ function setPageSize(ps) {
-
-
-
- Prikazano: {{ showingFrom }}–{{ showingTo }} od {{ total }}
- Ni zadetkov
-
-
-
-
- ««
-
-
-
- «
-
-
-
-
- 1
-
-
…
-
-
-
- {{ p }}
-
-
-
-
…
-
- {{ lastPage }}
-
-
-
-
- »
-
-
-
- »»
-
-
-
-
-
- / {{ lastPage }}
-
-
-
+
+
+
diff --git a/resources/js/Components/DataTable/DataTablePaginationClient.vue b/resources/js/Components/DataTable/DataTablePaginationClient.vue
new file mode 100644
index 0000000..1259b30
--- /dev/null
+++ b/resources/js/Components/DataTable/DataTablePaginationClient.vue
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
+ Prejšnja
+
+
+ Naslednja
+
+
+
+
+
+
+
+
+
Prikazano
+
+ {{ showingFrom }}
+ -
+ {{ showingTo }}
+
+
od
+
+ {{ total }}
+
+
+
+ Ni zadetkov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ lastPage }}
+
+
+
+
+
diff --git a/resources/js/Components/app/ui/AppCheckboxArray.vue b/resources/js/Components/app/ui/AppCheckboxArray.vue
new file mode 100644
index 0000000..c822403
--- /dev/null
+++ b/resources/js/Components/app/ui/AppCheckboxArray.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/resources/js/Components/app/ui/AppCombobox.vue b/resources/js/Components/app/ui/AppCombobox.vue
index 537063f..064335c 100644
--- a/resources/js/Components/app/ui/AppCombobox.vue
+++ b/resources/js/Components/app/ui/AppCombobox.vue
@@ -55,6 +55,7 @@ const selectedItem = computed(() =>
function selectItem(selectedValue) {
const newValue = selectedValue === props.modelValue ? "" : selectedValue;
emit("update:modelValue", newValue);
+ console.log(selectedValue);
open.value = false;
}
@@ -83,7 +84,11 @@ function selectItem(selectedValue) {
v-for="item in items"
:key="item.value"
:value="item.value"
- @select="selectItem"
+ @select="
+ (ev) => {
+ selectItem(ev.detail.value);
+ }
+ "
>
{{ item.label }}
+import { cn } from "@/lib/utils";
+import { cva } from "class-variance-authority";
+
+const props = defineProps({
+ variant: {
+ type: String,
+ default: "default",
+ validator: (value) => ["default", "destructive"].includes(value),
+ },
+ class: {
+ type: String,
+ default: "",
+ },
+});
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+
+
+
+
+
+
diff --git a/resources/js/Components/ui/alert/AlertDescription.vue b/resources/js/Components/ui/alert/AlertDescription.vue
new file mode 100644
index 0000000..2254a20
--- /dev/null
+++ b/resources/js/Components/ui/alert/AlertDescription.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/resources/js/Components/ui/alert/AlertTitle.vue b/resources/js/Components/ui/alert/AlertTitle.vue
new file mode 100644
index 0000000..cc6effb
--- /dev/null
+++ b/resources/js/Components/ui/alert/AlertTitle.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/resources/js/Components/ui/alert/index.js b/resources/js/Components/ui/alert/index.js
new file mode 100644
index 0000000..41ad929
--- /dev/null
+++ b/resources/js/Components/ui/alert/index.js
@@ -0,0 +1,3 @@
+export { default as Alert } from "./Alert.vue";
+export { default as AlertTitle } from "./AlertTitle.vue";
+export { default as AlertDescription } from "./AlertDescription.vue";
diff --git a/resources/js/Components/ui/checkbox/CheckboxArray.vue b/resources/js/Components/ui/checkbox/CheckboxArray.vue
new file mode 100644
index 0000000..c822403
--- /dev/null
+++ b/resources/js/Components/ui/checkbox/CheckboxArray.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/resources/js/Pages/Imports/Import.vue b/resources/js/Pages/Imports/Import.vue
index 3886eb3..28a46d9 100644
--- a/resources/js/Pages/Imports/Import.vue
+++ b/resources/js/Pages/Imports/Import.vue
@@ -751,6 +751,14 @@ async function fetchColumns() {
async function applyTemplateToImport() {
if (!importId.value || !form.value.import_template_id) return;
+
+ // Find the selected template to get its UUID
+ const template = (props.templates || []).find((t) => t.id === form.value.import_template_id);
+ if (!template?.uuid) {
+ console.error('Template UUID not found');
+ return;
+ }
+
try {
if (templateApplied.value) {
const ok = window.confirm(
@@ -762,7 +770,7 @@ async function applyTemplateToImport() {
}
await axios.post(
route("importTemplates.apply", {
- template: form.value.import_template_id,
+ template: template.uuid,
import: importId.value,
}),
{},
diff --git a/resources/js/Pages/Imports/Index.vue b/resources/js/Pages/Imports/Index.vue
index e7ff701..5368a0b 100644
--- a/resources/js/Pages/Imports/Index.vue
+++ b/resources/js/Pages/Imports/Index.vue
@@ -2,7 +2,16 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, router } from "@inertiajs/vue3";
import { ref, computed } from "vue";
-import ConfirmationModal from "@/Components/ConfirmationModal.vue";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/Components/ui/alert-dialog";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
@@ -225,41 +234,45 @@ function formatDateTimeNoSeconds(value) {
-
+
+
+
+
+ {
+ if (!val) {
+ confirming = false;
+ deletingId = null;
+ }
+ }
+ "
+ >
+
+
+ Potrditev brisanja
+
+ Ste prepričani, da želite izbrisati ta uvoz? Datoteka bo odstranjena iz
+ shrambe, če je še prisotna.
+
+
+ {{ errorMsg }}
+
+
- Potrditev brisanja
-
-
- Ste prepričani, da želite izbrisati ta uvoz? Datoteka bo odstranjena iz
- shrambe, če je še prisotna.
-
- {{ errorMsg }}
-
-
-
- Prekliči
-
-
- Izbriši
-
-
-
-
-
-
+ Prekliči
+
+
+ Izbriši
+
+
+
+
diff --git a/resources/js/Pages/Reports/Index.vue b/resources/js/Pages/Reports/Index.vue
index bdef29f..0d7e109 100644
--- a/resources/js/Pages/Reports/Index.vue
+++ b/resources/js/Pages/Reports/Index.vue
@@ -1,27 +1,105 @@
-
-
-
-
Poročila
-
Izberite poročilo za pregled in izvoz.
+
+
+
+
Poročila
+
+ Izberite poročilo za pregled in izvoz podatkov
+
-
-
-
{{ r.name }}
-
{{ r.description }}
-
Odpri →
-
+
+
+
+
+
+
+ {{ r.name }}
+
+ {{ r.description }}
+
+
+
+
+ Odpri →
+
+
+
+
diff --git a/resources/js/Pages/Reports/Show.vue b/resources/js/Pages/Reports/Show.vue
index 1d487ad..14c9675 100644
--- a/resources/js/Pages/Reports/Show.vue
+++ b/resources/js/Pages/Reports/Show.vue
@@ -1,9 +1,27 @@
-
- Archive Settings
-
+
+
+
+
+
+ Archive rule conditions are temporarily inactive
+
+
+
+ All enabled rules apply to the focus entity and its selected related tables
+ without date/other filters. Stored condition JSON is preserved for future
+ reactivation.
+
+ The "Run Now" action is currently disabled.
+
+
Chain Path Help
+
Supported chained related tables (dot notation):
+
+
Only these chains are processed; others are ignored.
+
+
+
-
-
-
Archive rule conditions are temporarily inactive.
-
All enabled rules apply to the focus entity and its selected related tables without date/other filters. Stored condition JSON is preserved for future reactivation.
-
The "Run Now" action is currently disabled.
-
-
Chain Path Help
-
Supported chained related tables (dot notation):
-
-
Only these chains are processed; others are ignored.
-
-
-
-
-
-
-
-
- {{ s.name || "Untitled Rule #" + s.id }}
- Disabled
-
-
- {{ s.description }}
-
-
- Strategy: {{ s.strategy }} • Soft: {{ s.soft ? "Yes" : "No" }}
-
-
-
-
- Edit
-
-
-
- {{ s.enabled ? "Disable" : "Enable" }}
-
-
- Delete
-
-
-
-
-
{{
- JSON.stringify(s.entities, null, 2)
- }}
-
-
-
- No archive rules.
-
-
-
-
-
New Rule
-
-
- Segment (optional)
-
- -- none --
-
- {{ seg.name }}
-
-
-
-
- Action (optional)
- {
- newForm.decision_id = null;
- }
- "
- class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
- >
- -- none --
-
- {{ a.name }}
-
-
-
-
- Decision (optional)
-
- -- none --
-
- {{ d.name }}
-
-
-
-
-
Name
-
+
+
+
+
+
+
+
-
- {{ newForm.errors.name }}
+
+ No archive rules defined yet.
-
- Focus Entity
-
- -- choose --
-
- {{ ae.name || ae.focus }}
-
-
-
-
-
Related Tables
-
-
-
- {{ r }}
-
-
-
-
-
Description
-
-
- {{ newForm.errors.description }}
-
-
-
-
- Enabled
-
-
-
- Soft Archive
-
-
-
- Reactivate (undo archive)
-
-
-
Strategy
-
- Immediate
- Scheduled
- Queued
- Manual (never auto-run)
-
-
- {{ newForm.errors.strategy }}
-
-
-
- Create
-
-
- Please fix validation errors.
-
-
+
-
-
- Edit Rule #{{ editingSetting.id }}
-
-
-
- Manual strategy: this rule will only run when triggered manually.
-
-
- Segment (optional)
-
- -- none --
-
- {{ seg.name }}
-
-
-
-
- Action (optional)
- {
- editForm.decision_id = null;
- }
- "
- class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
- >
- -- none --
-
- {{ a.name }}
-
-
-
-
- Decision (optional)
-
- -- none --
-
- {{ d.name }}
-
-
-
-
-
Name
-
-
- {{ editForm.errors.name }}
-
-
-
- Focus Entity
-
- -- choose --
-
- {{ ae.name || ae.focus }}
-
-
-
-
-
Related Tables
-
-
-
- {{ r }}
-
-
-
-
-
Description
-
-
- {{ editForm.errors.description }}
-
-
-
-
- Enabled
-
-
-
- Soft Archive
-
-
-
- Reactivate (undo archive)
-
-
-
Strategy
-
- Immediate
- Scheduled
- Queued
- Manual (never auto-run)
-
-
- {{ editForm.errors.strategy }}
-
-
-
-
- Update
-
-
- Cancel
-
-
-
- Please fix validation errors.
-
-
+
+
+
+
diff --git a/resources/js/Pages/Settings/Archive/Partials/ArchiveRuleCard.vue b/resources/js/Pages/Settings/Archive/Partials/ArchiveRuleCard.vue
new file mode 100644
index 0000000..8428fe8
--- /dev/null
+++ b/resources/js/Pages/Settings/Archive/Partials/ArchiveRuleCard.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+ {{ rule.name || "Untitled Rule #" + rule.id }}
+
+
+ Disabled
+
+
+
+ {{ rule.description }}
+
+
+ Strategy: {{ rule.strategy }}
+ •
+ Soft: {{ rule.soft ? "Yes" : "No" }}
+
+ • Reactivate Mode
+
+
+
+
+
+
+
+
+
+
+
+
+ Uredi
+
+
+
+ {{ rule.enabled ? "Disable" : "Enable" }}
+
+
+
+ Izbriši
+
+
+
+
+
+
{{ JSON.stringify(rule.entities, null, 2) }}
+
+
+
diff --git a/resources/js/Pages/Settings/Archive/Partials/CreateRuleForm.vue b/resources/js/Pages/Settings/Archive/Partials/CreateRuleForm.vue
new file mode 100644
index 0000000..44cfdcd
--- /dev/null
+++ b/resources/js/Pages/Settings/Archive/Partials/CreateRuleForm.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+ New Archive Rule
+
+
+
+ Segment (optional)
+
+
+
+
+
+ -- none --
+
+ {{ seg.name }}
+
+
+
+
+
+
+ Action (optional)
+
+
+
+
+
+ -- none --
+
+ {{ a.name }}
+
+
+
+
+
+
+ Decision (optional)
+
+
+
+
+
+ -- none --
+
+ {{ d.name }}
+
+
+
+
+
+
+ Name
+
+
+
+
+
+ Focus Entity
+
+
+
+
+
+
+ {{ ae.name || ae.focus }}
+
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+
+ Soft Archive
+
+
+
+
+
+
+ Reactivate (undo archive)
+
+
+
+
+ Strategy
+
+
+
+
+
+ Immediate
+ Scheduled
+ Queued
+ Manual (never auto-run)
+
+
+
+
+
+
+ Create Rule
+
+
+
+ Please fix validation errors.
+
+
+
+
diff --git a/resources/js/Pages/Settings/Archive/Partials/EditRuleForm.vue b/resources/js/Pages/Settings/Archive/Partials/EditRuleForm.vue
new file mode 100644
index 0000000..76c03e3
--- /dev/null
+++ b/resources/js/Pages/Settings/Archive/Partials/EditRuleForm.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+ Edit Rule #{{ setting.id }}
+
+
+
+
+ Manual strategy: this rule will only run when triggered manually.
+
+
+
+
+ Segment (optional)
+
+
+
+
+
+ -- none --
+
+ {{ seg.name }}
+
+
+
+
+
+
+ Action (optional)
+
+
+
+
+
+ -- none --
+
+ {{ a.name }}
+
+
+
+
+
+
+ Decision (optional)
+
+
+
+
+
+ -- none --
+
+ {{ d.name }}
+
+
+
+
+
+
+ Name
+
+
+
+
+
+ Focus Entity
+
+
+
+
+
+
+ {{ ae.name || ae.focus }}
+
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+
+ Soft Archive
+
+
+
+
+
+
+ Reactivate (undo archive)
+
+
+
+
+ Strategy
+
+
+
+
+
+ Immediate
+ Scheduled
+ Queued
+ Manual (never auto-run)
+
+
+
+
+
+
+
+ Update
+
+ Cancel
+
+
+
+ Please fix validation errors.
+
+
+
+
diff --git a/resources/js/Pages/Settings/ContractConfigs/Index.vue b/resources/js/Pages/Settings/ContractConfigs/Index.vue
index ffe7e13..1106a89 100644
--- a/resources/js/Pages/Settings/ContractConfigs/Index.vue
+++ b/resources/js/Pages/Settings/ContractConfigs/Index.vue
@@ -1,58 +1,141 @@
@@ -60,116 +143,195 @@ const destroyConfig = () => {
-
-
-
- Contract configurations
-
-
+ New
-
-
-
-
-
- Type
- Segment
- Active
- Actions
-
-
-
-
- {{ cfg.type?.name }}
- {{ cfg.segment?.name }} (initial)
- {{ cfg.active ? 'Yes' : 'No' }}
-
- Edit
- Delete
-
-
-
- No configurations.
-
-
-
-
-
+
+
+
+
+
+ Contract configurations
+
+
+ New
+
+
+ (sort = v)"
+ @update:page="(v) => (page = v)"
+ @update:pageSize="(v) => (pageSize = v)"
+ >
+
+ {{ row.type?.name }}
+
+
+
+ {{ row.segment?.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Uredi
+
+
+
+ Izbriši
+
+
+
+
+
+
-
-
+
+
+
+ New Contract Configuration
+
+
Contract Type
-
- -- select type --
- {{ t.name }}
-
-
+
+
+
+
+
+
+ {{ t.name }}
+
+
+
+
Segment
-
- -- select segment --
- {{ s.name }}
-
-
+
+
+
+
+
+
+ {{ s.name }}
+
+
+
+
-
- Mark as initial
+
+ Mark as initial
-
-
+
+
+ Cancel
+ Create
+
+
+
-
-
+
+
+
+ Edit Configuration
+
+
-
+
+
+ Cancel
+ Save
+
+
+
-
-
- Delete configuration
-
-
- Are you sure you want to delete configuration for type "{{ toDelete?.type?.name }}"?
-
-
- Cancel
- Delete
-
-
+
+
+
+ Delete configuration
+
+
+ Are you sure you want to delete configuration for type "{{
+ toDelete?.type?.name
+ }}"?
+
+
+ Cancel
+ Delete
+
+
+
diff --git a/resources/js/Pages/Settings/FieldJob/Index.vue b/resources/js/Pages/Settings/FieldJob/Index.vue
index 6355d2f..d213395 100644
--- a/resources/js/Pages/Settings/FieldJob/Index.vue
+++ b/resources/js/Pages/Settings/FieldJob/Index.vue
@@ -1,14 +1,28 @@
@@ -157,383 +181,299 @@ watch(
-
-
-
Field Job Settings
-
+ New
-
+
+
+
+
+
+ Field Job Settings
+
+
+ New
+
+
-
-
diff --git a/resources/js/Pages/Settings/Index.vue b/resources/js/Pages/Settings/Index.vue
index 0b81507..8321f25 100644
--- a/resources/js/Pages/Settings/Index.vue
+++ b/resources/js/Pages/Settings/Index.vue
@@ -1,84 +1,105 @@
-
+
-
-
-
-
Segments
-
Manage segments used across the app.
-
- Open Segments
-
-
-
Payments
-
- Defaults for payments and auto-activity.
-
-
- Open Payment Settings
-
-
-
Workflow
-
- Configure actions and decisions relationships.
-
-
- Open Workflow
-
-
-
Field Job Settings
-
- Configure segment-based field job rules.
-
-
- Open Field Job
-
-
-
Contract Configs
-
- Auto-assign initial segments for contracts by type.
-
-
- Open Contract Configs
-
-
-
Archive Settings
-
- Define rules for archiving or soft-deleting aged data.
-
-
- Open Archive Settings
-
+
+
+
Settings
+
+ Manage your application configuration and preferences
+
+
+
+
+
+
+
+
+
+
+
{{ card.title }}
+
+ {{ card.description }}
+
+
+
+
+ Open Settings
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Partials/ActionTable.vue b/resources/js/Pages/Settings/Partials/ActionTable.vue
deleted file mode 100644
index 71b171c..0000000
--- a/resources/js/Pages/Settings/Partials/ActionTable.vue
+++ /dev/null
@@ -1,362 +0,0 @@
-
-
-
-
-
(sort = v)"
- @update:page="(v) => (page = v)"
- @update:pageSize="(v) => (pageSize = v)"
- >
-
-
-
- {{ row.color_tag || "" }}
-
-
-
- {{ row.decisions?.length ?? 0 }}
-
-
-
- {{ row.segment?.name || "" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Shranjuje.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Shranjuje.
-
-
-
-
-
- Delete action
-
- Are you sure you want to delete action "{{ toDelete?.name }}"? This cannot be
- undone.
-
-
-
- Cancel
-
- Delete
-
-
-
diff --git a/resources/js/Pages/Settings/Payments/Index.vue b/resources/js/Pages/Settings/Payments/Index.vue
index d10e3d2..83a4790 100644
--- a/resources/js/Pages/Settings/Payments/Index.vue
+++ b/resources/js/Pages/Settings/Payments/Index.vue
@@ -1,103 +1,168 @@
-
-
Nastavitve plačil
-
-
-
-
Privzeta valuta
-
-
{{ form.errors.default_currency }}
-
-
-
-
-
- Ustvari aktivnost ob dodanem plačilu
-
-
-
-
-
-
Privzeto dejanje
-
- — Brez —
- {{ a.name }}
-
-
{{ form.errors.default_action_id }}
+
+
+
+
+ Nastavitve plačil
+
+
-
Privzeta odločitev
-
- — Najprej izberite dejanje —
- {{ d.name }}
-
-
{{ form.errors.default_decision_id }}
+
Privzeta valuta
+
+
+ {{ form.errors.default_currency }}
+
-
-
-
Predloga opombe aktivnosti
-
-
Podprti žetoni: {amount}, {currency}
-
{{ form.errors.activity_note_template }}
-
+
+
+
+ Ustvari aktivnost ob dodanem plačilu
+
+
+
+
+
+
Privzeto dejanje
+
+
+
+
+
+ — Brez —
+ {{
+ a.name
+ }}
+
+
+
+ {{ form.errors.default_action_id }}
+
+
+
+
+
Privzeta odločitev
+
+
+
+
+
+ — Najprej izberite dejanje —
+ {{
+ d.name
+ }}
+
+
+
+ {{ form.errors.default_decision_id }}
+
+
+
+
+
+
Predloga opombe aktivnosti
+
+
Podprti žetoni: {amount}, {currency}
+
+ {{ form.errors.activity_note_template }}
+
+
- Ponastavi
- Shrani
+ Ponastavi
+ Shrani
-
+
diff --git a/resources/js/Pages/Settings/Reports/Edit.vue b/resources/js/Pages/Settings/Reports/Edit.vue
new file mode 100644
index 0000000..9294b35
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Edit.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit Report: {{ report.name }}
+
+
+ Configure entities, columns, filters, and conditions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ report.name }}
+
+ {{ report.description || "No description" }}
+
+
+
+
+ Disabled
+ {{ report.category }}
+
+
+
+
+
+
+ Slug:
+ {{ report.slug }}
+
+
+ Order:
+ {{ report.order }}
+
+
+ Status:
+ {{ report.enabled ? "Enabled" : "Disabled" }}
+
+
+
+
+
+
+
+
+
+
+ Entities
+ {{ report.entities?.length || 0 }}
+
+
+
+ Columns
+ {{ report.columns?.length || 0 }}
+
+
+
+ Filters
+ {{ report.filters?.length || 0 }}
+
+
+
+ Conditions
+ {{ report.conditions?.length || 0 }}
+
+
+
+ Orders
+ {{ report.orders?.length || 0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Reports/Index.vue b/resources/js/Pages/Settings/Reports/Index.vue
new file mode 100644
index 0000000..1c0c15e
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Index.vue
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+ Reports Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Database Reports
+
+ Manage configurable reports with dynamic queries and filters
+
+
+
+
+
+
+ Create Report
+
+
+
+
+
+ No reports configured yet. Create your first report to get started.
+
+
+
+
+
+
+
+ {{ report.name }}
+ Disabled
+ {{ report.category }}
+
+
+ {{ report.description || "No description" }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+ Configure Details
+
+
+
+
+
+
+ {{ report.enabled ? "Disable" : "Enable" }}
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+ Slug:
+ {{ report.slug }}
+
+
+ Order:
+ {{ report.order }}
+
+
+
+
+
+
+
+
+
+
+
+ Create New Report
+
+ Create a new database-driven report configuration
+
+
+
+
+
+
Slug *
+
+
Unique identifier for the report
+
+
+
+ Name *
+
+
+
+
+ Description
+
+
+
+
+ Category
+
+
+
+
+ Display Order
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+ Cancel
+
+
+ Create Report
+
+
+
+
+
+
+
+
+
+
+ Edit Report
+
+ Update report configuration
+
+
+
+
+
+ Slug *
+
+
+
+
+ Name *
+
+
+
+
+ Description
+
+
+
+
+ Category
+
+
+
+
+ Display Order
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+ Cancel
+
+
+ Update Report
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Reports/Partials/ColumnsSection.vue b/resources/js/Pages/Settings/Reports/Partials/ColumnsSection.vue
new file mode 100644
index 0000000..b07a5ab
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Partials/ColumnsSection.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
+ Report Columns
+
+ Define which columns to select and display in the report
+
+
+
+
+ Add Column
+
+
+
+
+
+ No columns configured. Add columns to display in the report.
+
+
+
+
+
+
+
+
+ {{ column.label }}
+ {{ column.type }}
+ sortable
+
+
+ {{ column.key }}
+
+
+ {{ column.expression }}
+
+
Order: {{ column.order }}
+
+
+
+
+
+
+
+
+
+
+
+ Add Column
+
+ Add a new column to the report output
+
+
+
+
+
+
+
+ Type *
+
+
+
+
+
+ string
+ number
+ date
+ boolean
+ currency
+
+
+
+
+
+
SQL Expression *
+
+
SQL expression or column path
+
+
+
+
+
+
+ Sortable
+
+
+
+
+
+
+ Visible
+
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Add Column
+
+
+
+
+
+
+
+
+
+
+ Edit Column
+
+ Update column configuration
+
+
+
+
+
+
+
+ Type *
+
+
+
+
+
+ string
+ number
+ date
+ boolean
+ currency
+
+
+
+
+
+ SQL Expression *
+
+
+
+
+
+
+
+ Sortable
+
+
+
+
+
+
+ Visible
+
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Update Column
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Reports/Partials/ConditionsSection.vue b/resources/js/Pages/Settings/Reports/Partials/ConditionsSection.vue
new file mode 100644
index 0000000..8357702
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Partials/ConditionsSection.vue
@@ -0,0 +1,418 @@
+
+
+
+
+
+
+
+ WHERE Conditions
+
+ Define WHERE clause rules for filtering data
+
+
+
+
+ Add Condition
+
+
+
+
+
+ No conditions configured. Add WHERE conditions to filter query results.
+
+
+
+
+
+
+
+ {{ condition.logical_operator }}
+
+ Group {{ condition.group_id || 0 }}
+ disabled
+
+
+ {{ condition.column }} {{ condition.operator }}
+ "{{ condition.value }}"
+ filter({{ condition.filter_key }})
+ {{ condition.value }}
+
+
+ Type: {{ condition.value_type }} | Order: {{ condition.order }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Condition
+
+ Add a new WHERE clause condition
+
+
+
+
+
+ Column *
+
+
+
+
+ Operator *
+
+
+
+
+
+ =
+ !=
+ >
+ <
+ >=
+ <=
+ LIKE
+ IN
+ NOT IN
+ BETWEEN
+ IS NULL
+ IS NOT NULL
+
+
+
+
+
+ Value Type *
+
+
+
+
+
+ static (hardcoded value)
+ filter (from user input)
+ expression (SQL expression)
+
+
+
+
+
+ Filter Key *
+
+
+
+
+ Value
+
+
+
+
+
+ Logical Operator *
+
+
+
+
+
+ AND
+ OR
+
+
+
+
+
+ Group ID
+
+
+
+
+
+ Order
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+ Cancel
+
+
+ Add Condition
+
+
+
+
+
+
+
+
+
+
+ Edit Condition
+
+ Update condition configuration
+
+
+
+
+
+ Column *
+
+
+
+
+ Operator *
+
+
+
+
+
+ =
+ !=
+ >
+ <
+ >=
+ <=
+ LIKE
+ IN
+ NOT IN
+ BETWEEN
+ IS NULL
+ IS NOT NULL
+
+
+
+
+
+ Value Type *
+
+
+
+
+
+ static (hardcoded value)
+ filter (from user input)
+ expression (SQL expression)
+
+
+
+
+
+ Filter Key *
+
+
+
+
+ Value
+
+
+
+
+
+ Logical Operator *
+
+
+
+
+
+ AND
+ OR
+
+
+
+
+
+ Group ID
+
+
+
+
+
+ Order
+
+
+
+
+
+
+ Enabled
+
+
+
+
+
+ Cancel
+
+
+ Update Condition
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Reports/Partials/EntitiesSection.vue b/resources/js/Pages/Settings/Reports/Partials/EntitiesSection.vue
new file mode 100644
index 0000000..b8d8088
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Partials/EntitiesSection.vue
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+ Database Entities & Joins
+
+ Configure which models/tables to query and how to join them
+
+
+
+
+ Add Entity
+
+
+
+
+
+ No entities configured. Add a base entity to get started.
+
+
+
+
+
+
+
+ {{ entity.join_type }}
+
+ {{ entity.model_class }}
+ as {{ entity.alias }}
+
+
+ {{ entity.join_first }} {{ entity.join_operator }} {{ entity.join_second }}
+
+
Order: {{ entity.order }}
+
+
+
+
+
+
+
+
+
+
+
+ Add Entity
+
+ Add a model/table to the report query
+
+
+
+
+
+ Model Class *
+
+
+
+
+ Alias (optional)
+
+
+
+
+ Join Type *
+
+
+
+
+
+ base (no join)
+ join (INNER JOIN)
+ leftJoin (LEFT JOIN)
+ rightJoin (RIGHT JOIN)
+
+
+
+
+
+
+ Join First Column *
+
+
+
+
+ Operator *
+
+
+
+
+
+ =
+ !=
+ >
+ <
+ >=
+ <=
+
+
+
+
+
+ Join Second Column *
+
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Add Entity
+
+
+
+
+
+
+
+
+
+
+ Edit Entity
+
+ Update entity configuration
+
+
+
+
+
+ Model Class *
+
+
+
+
+ Alias (optional)
+
+
+
+
+ Join Type *
+
+
+
+
+
+ base (no join)
+ join (INNER JOIN)
+ leftJoin (LEFT JOIN)
+ rightJoin (RIGHT JOIN)
+
+
+
+
+
+
+ Join First Column *
+
+
+
+
+ Operator *
+
+
+
+
+
+ =
+ !=
+ >
+ <
+ >=
+ <=
+
+
+
+
+
+ Join Second Column *
+
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Update Entity
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Reports/Partials/FiltersSection.vue b/resources/js/Pages/Settings/Reports/Partials/FiltersSection.vue
new file mode 100644
index 0000000..2d26f1d
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Partials/FiltersSection.vue
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+ Report Filters
+
+ Define input parameters that users can provide to filter the report
+
+
+
+
+ Add Filter
+
+
+
+
+
+ No filters configured. Add filters to allow users to filter report results.
+
+
+
+
+
+
+ {{ filter.label }}
+ {{ filter.type }}
+ nullable
+ {{ filter.data_source }}
+
+
+ {{ filter.key }}
+
+
+ Default: {{ filter.default_value }}
+
+
Order: {{ filter.order }}
+
+
+
+
+
+
+
+
+
+
+
+ Add Filter
+
+ Add a new filter parameter for the report
+
+
+
+
+
+
+
+ Type *
+
+
+
+
+
+ string
+ date
+ number
+ boolean
+ select
+ select:client
+ select:user
+ multiselect
+
+
+
+
+
+
Data Source (optional)
+
+
For dynamic selects
+
+
+
+ Default Value (optional)
+
+
+
+
+
+
+ Nullable (filter is optional)
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Add Filter
+
+
+
+
+
+
+
+
+
+
+ Edit Filter
+
+ Update filter configuration
+
+
+
+
+
+
+
+ Type *
+
+
+
+
+
+ string
+ date
+ number
+ boolean
+ select
+ select:client
+ select:user
+ multiselect
+
+
+
+
+
+ Data Source (optional)
+
+
+
+
+ Default Value (optional)
+
+
+
+
+
+
+ Nullable (filter is optional)
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Update Filter
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Reports/Partials/OrdersSection.vue b/resources/js/Pages/Settings/Reports/Partials/OrdersSection.vue
new file mode 100644
index 0000000..5ab445e
--- /dev/null
+++ b/resources/js/Pages/Settings/Reports/Partials/OrdersSection.vue
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+
+ ORDER BY Clauses
+
+ Define how to sort the report results
+
+
+
+
+ Add Order
+
+
+
+
+
+ No order clauses configured. Add ORDER BY clauses to sort results.
+
+
+
+
+
+
+
+
+
{{ orderClause.column }}
+
+ {{ orderClause.direction }}
+
+
+
Order: {{ orderClause.order }}
+
+
+
+
+
+
+
+
+
+
+
+ Add Order Clause
+
+ Add a new ORDER BY clause
+
+
+
+
+
+ Column *
+
+
+
+
+ Direction *
+
+
+
+
+
+ ASC (Ascending)
+ DESC (Descending)
+
+
+
+
+
+
Order
+
+
Determines sort priority (lower = higher priority)
+
+
+
+
+ Cancel
+
+
+ Add Order
+
+
+
+
+
+
+
+
+
+
+ Edit Order Clause
+
+ Update order clause configuration
+
+
+
+
+
+ Column *
+
+
+
+
+ Direction *
+
+
+
+
+
+ ASC (Ascending)
+ DESC (Descending)
+
+
+
+
+
+ Order
+
+
+
+
+
+ Cancel
+
+
+ Update Order
+
+
+
+
+
+
diff --git a/resources/js/Pages/Settings/Segments/Index.vue b/resources/js/Pages/Settings/Segments/Index.vue
index f6e5bf0..d8b00d2 100644
--- a/resources/js/Pages/Settings/Segments/Index.vue
+++ b/resources/js/Pages/Settings/Segments/Index.vue
@@ -2,12 +2,17 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
-import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
-import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
-import PrimaryButton from "@/Components/PrimaryButton.vue";
+import AppCard from "@/Components/app/ui/card/AppCard.vue";
+import CardTitle from "@/Components/ui/card/CardTitle.vue";
+import { LayoutGrid } from "lucide-vue-next";
+import { Button } from "@/Components/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/Components/ui/dialog";
+import { Input } from "@/Components/ui/input";
+import { Checkbox } from "@/Components/ui/checkbox";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
+import { Badge } from "@/Components/ui/badge";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
-import TextInput from "@/Components/TextInput.vue";
const props = defineProps({
segments: Array,
@@ -79,144 +84,150 @@ const update = () => {
-
-
-
Segments
-
+ New
+
+
+
+
+
+ Segments
+
+
+ New
+
+
+
+
+
+
+
+ ID
+ Name
+ Description
+ Active
+ Exclude
+ Actions
+
+
+
+
+ {{ s.id }}
+ {{ s.name }}
+ {{ s.description }}
+
+
+ {{ s.active ? "Yes" : "No" }}
+
+
+
+
+ {{ s.exclude ? "Yes" : "No" }}
+
+
+
+
+ Edit
+
+
+
+
+
-
-
+
+
+
+
+ New Segment
+
+
+
+ Cancel
+ Create
+
+
+
-
-
+
+
+
+
+ Edit Segment
+
+
+
+
+ Cancel
+ Save
+
+
+
+
diff --git a/resources/js/Pages/Settings/Workflow/Index.vue b/resources/js/Pages/Settings/Workflow/Index.vue
index 3173a3f..29e38d7 100644
--- a/resources/js/Pages/Settings/Workflow/Index.vue
+++ b/resources/js/Pages/Settings/Workflow/Index.vue
@@ -1,9 +1,12 @@
+
+
+
+
+
+
+
+ Filtri
+
+
+
+
+
+
+ Dodaj akcijo
+
+
+
(sort = v)"
+ @update:page="(v) => (page = v)"
+ @update:pageSize="(v) => (pageSize = v)"
+ >
+
+
+
+ {{ row.color_tag || "" }}
+
+
+
+ {{ row.decisions?.length ?? 0 }}
+
+
+
+ {{ row.segment?.name || "" }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Uredi
+
+
+
+ Izbriši
+
+
+
+
+
+
+
+
+
+
+ Spremeni akcijo
+
+
+
+ Ime
+
+
+
+
+
+
+
+
+
+ Shranjuje.
+
+
+
+ Cancel
+ Shrani
+
+
+
+
+
+
+
+ Dodaj akcijo
+
+
+
+ Ime
+
+
+
+
+
+
+
+
+
+ Shranjuje.
+
+
+
+ Cancel
+ Dodaj
+
+
+
+
+
+
+
+ Delete action
+
+
+ Are you sure you want to delete action "{{ toDelete?.name }}"? This cannot be
+ undone.
+
+
+ Cancel
+ Delete
+
+
+
+
diff --git a/resources/js/Pages/Settings/Partials/DecisionTable.vue b/resources/js/Pages/Settings/Workflow/Partials/DecisionTable.vue
similarity index 61%
rename from resources/js/Pages/Settings/Partials/DecisionTable.vue
rename to resources/js/Pages/Settings/Workflow/Partials/DecisionTable.vue
index c7381a6..4dc6f63 100644
--- a/resources/js/Pages/Settings/Partials/DecisionTable.vue
+++ b/resources/js/Pages/Settings/Workflow/Partials/DecisionTable.vue
@@ -1,19 +1,46 @@
-
-
-
-
-
-
+
+
+
+
+ Filtri
+
+
+
+
+
+
-
-
-
-
-
-
- Vse predloge
-
- {{ t.name }}
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ Vse predloge
+
+ {{ t.name }}
+
+
+
+
+
+
+
+
+ Samo auto mail
+
+
-
+
-
-
+ Dodaj odločitev
+
+ + Dodaj odločitev
-
+
{
:page="page"
:pageSize="pageSize"
:showToolbar="false"
+ :showPagination="true"
@update:sort="(v) => (sort = v)"
@update:page="(v) => (page = v)"
@update:pageSize="(v) => (pageSize = v)"
@@ -496,63 +546,61 @@ const destroyDecision = () => {
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Uredi
+
+
+
+ Izbriši
+
+
+
-
-
-
-
-
+
+
+
+ Spremeni odločitev
+
+
+
+ Ime
+
-
- Samodejna pošta (auto mail)
+
+
+ Samodejna pošta (auto mail)
+
-
- — Brez —
-
- {{ t.name }}
-
-
+
+
+
+
+
+ — Brez —
+
+ {{ t.name }}
+
+
+
-
@@ -595,43 +638,44 @@ const destroyDecision = () => {
-
- — Izberi —
-
- {{ opt.name || opt.key || `#${opt.id}` }}
-
-
+
+
+
+
+
+ — Izberi —
+
+ {{ opt.name || opt.key || `#${opt.id}` }}
+
+
+
-
- Vrstni red
+
-
@@ -639,16 +683,17 @@ const destroyDecision = () => {
-
- — Izberi segment —
-
- {{ s.name }}
-
-
+
+
+
+
+
+ — Izberi segment —
+
+ {{ s.name }}
+
+
+
{
-
+
Deaktiviraj prejšnje
@@ -672,16 +713,21 @@ const destroyDecision = () => {
-
- — Izberi nastavitev —
-
- {{ a.name }}
-
-
+
+
+
+
+
+ — Izberi nastavitev —
+
+ {{ a.name }}
+
+
+
{
-
+
Reactivate namesto arhiva
@@ -725,65 +767,63 @@ const destroyDecision = () => {
-
+ Dodaj dogodek + Dodaj dogodek
-
+
+ Cancel
+ Shrani
+
+
+
-
-
-
-
-
+
+
+
+ Dodaj odločitev
+
+
+
+ Ime
+
-
- Samodejna pošta (auto mail)
+
+
+ Samodejna pošta (auto mail)
+
-
- — Brez —
-
- {{ t.name }}
-
-
+
+
+
+
+ — Brez —
+
+ {{ t.name }}
+
+
+
-
@@ -830,43 +865,44 @@ const destroyDecision = () => {
-
- — Izberi —
-
- {{ opt.name || opt.key || `#${opt.id}` }}
-
-
+
+
+
+
+
+ — Izberi —
+
+ {{ opt.name || opt.key || `#${opt.id}` }}
+
+
+
-
- Vrstni red
+
-
@@ -874,16 +910,17 @@ const destroyDecision = () => {
-
- — Izberi segment —
-
- {{ s.name }}
-
-
+
+
+
+
+
+ — Izberi segment —
+
+ {{ s.name }}
+
+
+
{
-
+
Deaktiviraj prejšnje
@@ -907,16 +940,21 @@ const destroyDecision = () => {
-
- — Izberi nastavitev —
-
- {{ a.name }}
-
-
+
+
+
+
+
+ — Izberi nastavitev —
+
+ {{ a.name }}
+
+
+
-
+
Reactivate namesto arhiva
@@ -961,35 +995,42 @@ const destroyDecision = () => {
-
+ Dodaj dogodek + Dodaj dogodek
-
+
+ Cancel
+ Dodaj
+
+
+
-
- Delete decision
-
- Are you sure you want to delete decision "{{ toDelete?.name }}"? This cannot be
- undone.
-
-
-
- Cancel
-
- Delete
-
-
+
+
+
+ Delete decision
+
+
+ Are you sure you want to delete decision "{{ toDelete?.name }}"? This cannot be
+ undone.
+
+
+ Cancel
+ Delete
+
+
+
diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php
index b7f3c09..8ba832f 100644
--- a/routes/breadcrumbs.php
+++ b/routes/breadcrumbs.php
@@ -24,6 +24,18 @@
$trail->push('Contract Configs', route('settings.contractConfigs.index'));
});
+// Dashboard > Settings > Archive
+Breadcrumbs::for('settings.archive.index', function (BreadcrumbTrail $trail): void {
+ $trail->parent('settings');
+ $trail->push('Arhiv', route('settings.archive.index'));
+});
+
+// Dashboard > Settings > Reports
+Breadcrumbs::for('settings.reports.index', function (BreadcrumbTrail $trail): void {
+ $trail->parent('settings');
+ $trail->push('Reports', route('settings.reports.index'));
+});
+
// Dashboard
Breadcrumbs::for('dashboard', function (BreadcrumbTrail $trail) {
$trail->push('Nadzorna plošča', route('dashboard'));
@@ -109,6 +121,21 @@
$trail->push('Terenska dela', route('fieldjobs.index'));
});
+// Dashboard > Reports
+
+Breadcrumbs::for('reports.index', function (BreadcrumbTrail $trail) {
+ $trail->parent('dashboard');
+ $trail->push('Poročila', route('reports.index'));
+});
+
+// Dashboard > Reports > [Report]
+
+Breadcrumbs::for('reports.show', function (BreadcrumbTrail $trail, string $slug) {
+ $trail->parent('reports.index');
+ $report = \App\Models\Report::where('slug', $slug)->first();
+ $trail->push($report?->name ?? $slug, route('reports.show', $slug));
+});
+
// Dashboard > Imports
Breadcrumbs::for('imports.index', function (BreadcrumbTrail $trail) {
diff --git a/routes/web.php b/routes/web.php
index 6974673..ead45e0 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -379,6 +379,33 @@
Route::put('settings/archive/{archiveSetting}', [ArchiveSettingController::class, 'update'])->name('settings.archive.update');
Route::post('settings/archive/{archiveSetting}/run', [ArchiveSettingController::class, 'run'])->name('settings.archive.run');
Route::delete('settings/archive/{archiveSetting}', [ArchiveSettingController::class, 'destroy'])->name('settings.archive.destroy');
+ // settings / reports settings
+ Route::get('settings/reports', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'index'])->name('settings.reports.index');
+ Route::get('settings/reports/{report}/edit', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'edit'])->name('settings.reports.edit');
+ Route::post('settings/reports', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'store'])->name('settings.reports.store');
+ Route::put('settings/reports/{report}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'update'])->name('settings.reports.update');
+ Route::post('settings/reports/{report}/toggle', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'toggleEnabled'])->name('settings.reports.toggle');
+ Route::delete('settings/reports/{report}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'destroy'])->name('settings.reports.destroy');
+ // settings / reports - entities
+ Route::post('settings/reports/{report}/entities', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'storeEntity'])->name('settings.reports.entities.store');
+ Route::put('settings/reports/entities/{entity}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'updateEntity'])->name('settings.reports.entities.update');
+ Route::delete('settings/reports/entities/{entity}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'destroyEntity'])->name('settings.reports.entities.destroy');
+ // settings / reports - columns
+ Route::post('settings/reports/{report}/columns', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'storeColumn'])->name('settings.reports.columns.store');
+ Route::put('settings/reports/columns/{column}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'updateColumn'])->name('settings.reports.columns.update');
+ Route::delete('settings/reports/columns/{column}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'destroyColumn'])->name('settings.reports.columns.destroy');
+ // settings / reports - filters
+ Route::post('settings/reports/{report}/filters', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'storeFilter'])->name('settings.reports.filters.store');
+ Route::put('settings/reports/filters/{filter}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'updateFilter'])->name('settings.reports.filters.update');
+ Route::delete('settings/reports/filters/{filter}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'destroyFilter'])->name('settings.reports.filters.destroy');
+ // settings / reports - conditions
+ Route::post('settings/reports/{report}/conditions', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'storeCondition'])->name('settings.reports.conditions.store');
+ Route::put('settings/reports/conditions/{condition}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'updateCondition'])->name('settings.reports.conditions.update');
+ Route::delete('settings/reports/conditions/{condition}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'destroyCondition'])->name('settings.reports.conditions.destroy');
+ // settings / reports - orders
+ Route::post('settings/reports/{report}/orders', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'storeOrder'])->name('settings.reports.orders.store');
+ Route::put('settings/reports/orders/{order}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'updateOrder'])->name('settings.reports.orders.update');
+ Route::delete('settings/reports/orders/{order}', [\App\Http\Controllers\Settings\ReportSettingsController::class, 'destroyOrder'])->name('settings.reports.orders.destroy');
Route::get('settings/workflow', [WorkflowController::class, 'index'])->name('settings.workflow');
Route::get('settings/field-job', [FieldJobSettingController::class, 'index'])->name('settings.fieldjob.index');
@@ -444,7 +471,7 @@
Route::put('imports/templates/{template:uuid}/mappings/{mapping}', [ImportTemplateController::class, 'updateMapping'])->name('importTemplates.mappings.update');
Route::delete('imports/templates/{template:uuid}/mappings/{mapping}', [ImportTemplateController::class, 'deleteMapping'])->name('importTemplates.mappings.delete');
Route::post('imports/templates{template:uuid}/mappings/reorder', [ImportTemplateController::class, 'reorderMappings'])->name('importTemplates.mappings.reorder');
- Route::post('imports/templates/{template}/apply/{import}', [ImportTemplateController::class, 'applyToImport'])->name('importTemplates.apply');
+ Route::post('imports/templates/{template:uuid}/apply/{import}', [ImportTemplateController::class, 'applyToImport'])->name('importTemplates.apply');
// Delete an unfinished import
Route::delete('imports/{import}', [ImportController::class, 'destroy'])->name('imports.destroy');
// Route::put()