dbviewer 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -777,7 +777,11 @@
777
777
  const primaryKeyValue = recordData[Object.keys(recordData).find(key => key === 'id') || Object.keys(recordData)[0]];
778
778
 
779
779
  if (primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== '') {
780
- relationshipsContent.appendChild(createRelationshipSection('Has Many', reverseForeignKeys, recordData, 'has_many', primaryKeyValue));
780
+ const hasManySection = createRelationshipSection('Has Many', reverseForeignKeys, recordData, 'has_many', primaryKeyValue);
781
+ relationshipsContent.appendChild(hasManySection);
782
+
783
+ // Fetch relationship counts asynchronously
784
+ fetchRelationshipCounts('<%= @table_name %>', primaryKeyValue, reverseForeignKeys, hasManySection);
781
785
  }
782
786
  }
783
787
 
@@ -1433,6 +1437,55 @@
1433
1437
  });
1434
1438
 
1435
1439
  // Helper function to create relationship sections
1440
+ // Function to fetch relationship counts from API
1441
+ async function fetchRelationshipCounts(tableName, recordId, relationships, hasManySection) {
1442
+ try {
1443
+ const response = await fetch(`/dbviewer/api/tables/${tableName}/relationship_counts?record_id=${recordId}`);
1444
+ if (!response.ok) {
1445
+ throw new Error(`HTTP error! status: ${response.status}`);
1446
+ }
1447
+
1448
+ const data = await response.json();
1449
+
1450
+ // Update each count in the UI
1451
+ const countSpans = hasManySection.querySelectorAll('.relationship-count');
1452
+
1453
+ relationships.forEach((relationship, index) => {
1454
+ const countSpan = countSpans[index];
1455
+ if (countSpan) {
1456
+ const relationshipData = data.relationships.find(r =>
1457
+ r.table === relationship.from_table && r.foreign_key === relationship.column
1458
+ );
1459
+
1460
+ if (relationshipData) {
1461
+ const count = relationshipData.count;
1462
+ let badgeClass = 'bg-secondary';
1463
+ let badgeText = `${count} record${count !== 1 ? 's' : ''}`;
1464
+
1465
+ // Use different colors based on count
1466
+ if (count > 0) {
1467
+ badgeClass = count > 10 ? 'bg-warning' : 'bg-success';
1468
+ }
1469
+
1470
+ countSpan.innerHTML = `<span class="badge ${badgeClass}">${badgeText}</span>`;
1471
+ } else {
1472
+ // Fallback if no data found
1473
+ countSpan.innerHTML = '<span class="badge bg-danger">Error</span>';
1474
+ }
1475
+ }
1476
+ });
1477
+
1478
+ } catch (error) {
1479
+ console.error('Error fetching relationship counts:', error);
1480
+
1481
+ // Show error state in UI
1482
+ const countSpans = hasManySection.querySelectorAll('.relationship-count');
1483
+ countSpans.forEach(span => {
1484
+ span.innerHTML = '<span class="badge bg-danger">Error</span>';
1485
+ });
1486
+ }
1487
+ }
1488
+
1436
1489
  function createRelationshipSection(title, relationships, recordData, type, primaryKeyValue = null) {
1437
1490
  const section = document.createElement('div');
1438
1491
  section.className = 'relationship-section mb-4';
@@ -1503,7 +1556,12 @@
1503
1556
  <span class="text-muted">${fk.from_table}.</span><strong>${fk.column}</strong>
1504
1557
  </td>
1505
1558
  <td>
1506
- <span class="badge bg-secondary">View All</span>
1559
+ <span class="relationship-count">
1560
+ <span class="badge bg-secondary">
1561
+ <span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
1562
+ Loading...
1563
+ </span>
1564
+ </span>
1507
1565
  </td>
1508
1566
  <td>
1509
1567
  <a href="/dbviewer/tables/${fk.from_table}?column_filters[${fk.column}]=${encodeURIComponent(primaryKeyValue)}"
@@ -1525,3 +1583,1013 @@
1525
1583
  return section;
1526
1584
  }
1527
1585
  </script>
1586
+
1587
+ <!-- Floating Creation Filter - Only visible on desktop and on table details page -->
1588
+ <% if has_timestamp_column?(@table_name) %>
1589
+ <div class="floating-creation-filter d-none d-lg-block">
1590
+ <button class="btn btn-primary btn-lg shadow-lg floating-filter-btn"
1591
+ type="button"
1592
+ data-bs-toggle="offcanvas"
1593
+ data-bs-target="#creationFilterOffcanvas"
1594
+ aria-controls="creationFilterOffcanvas"
1595
+ title="Creation Date Filter">
1596
+ <i class="bi bi-calendar-range"></i>
1597
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1598
+ <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-success">
1599
+ <i class="bi bi-check"></i>
1600
+ </span>
1601
+ <% end %>
1602
+ </button>
1603
+ </div>
1604
+
1605
+ <!-- Creation Filter Offcanvas -->
1606
+ <div class="offcanvas offcanvas-end" tabindex="-1" id="creationFilterOffcanvas" aria-labelledby="creationFilterOffcanvasLabel">
1607
+ <div class="offcanvas-header">
1608
+ <h5 class="offcanvas-title" id="creationFilterOffcanvasLabel">
1609
+ <i class="bi bi-calendar-range me-2"></i>Creation Date Filter
1610
+ </h5>
1611
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
1612
+ </div>
1613
+ <div class="offcanvas-body">
1614
+ <form id="floatingCreationFilterForm" action="<%= request.path %>" method="get" class="mb-0">
1615
+ <!-- Preserve existing query parameters -->
1616
+ <input type="hidden" name="page" value="<%= @current_page %>">
1617
+ <input type="hidden" name="per_page" value="<%= @per_page %>">
1618
+ <input type="hidden" name="order_by" value="<%= @order_by %>">
1619
+ <input type="hidden" name="order_direction" value="<%= @order_direction %>">
1620
+
1621
+ <!-- Preserve column filters -->
1622
+ <% @column_filters.each do |key, value| %>
1623
+ <% unless key.to_s.start_with?('created_at') %>
1624
+ <input type="hidden" name="column_filters[<%= key %>]" value="<%= value %>">
1625
+ <% end %>
1626
+ <% end %>
1627
+
1628
+ <div class="mb-3">
1629
+ <p class="text-muted small">
1630
+ <i class="bi bi-info-circle me-1"></i>
1631
+ Filter records by their creation date and time. This applies to the <code>created_at</code> column.
1632
+ </p>
1633
+ </div>
1634
+
1635
+ <!-- Date range picker -->
1636
+ <div class="mb-3">
1637
+ <label for="floatingCreationFilterRange" class="form-label">Date Range</label>
1638
+ <input type="text"
1639
+ id="floatingCreationFilterRange"
1640
+ name="creation_filter_range"
1641
+ class="form-control"
1642
+ placeholder="Select date range..."
1643
+ readonly>
1644
+ <!-- Hidden inputs for form submission -->
1645
+ <input type="hidden" id="creation_filter_start" name="creation_filter_start" value="<%= @creation_filter_start %>">
1646
+ <input type="hidden" id="creation_filter_end" name="creation_filter_end" value="<%= @creation_filter_end %>">
1647
+ </div>
1648
+
1649
+ <!-- Quick preset buttons -->
1650
+ <div class="mb-3">
1651
+ <label class="form-label">Quick Presets</label>
1652
+ <div class="d-grid gap-1">
1653
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="lastminute">
1654
+ <i class="bi bi-clock me-1"></i>Last Minute
1655
+ </button>
1656
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last5minutes">
1657
+ <i class="bi bi-clock-history me-1"></i>Last 5 Minutes
1658
+ </button>
1659
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="today">
1660
+ <i class="bi bi-calendar-day me-1"></i>Today
1661
+ </button>
1662
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="yesterday">
1663
+ <i class="bi bi-calendar-minus me-1"></i>Yesterday
1664
+ </button>
1665
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last7days">
1666
+ <i class="bi bi-calendar-week me-1"></i>Last 7 Days
1667
+ </button>
1668
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last30days">
1669
+ <i class="bi bi-calendar-month me-1"></i>Last 30 Days
1670
+ </button>
1671
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="thismonth">
1672
+ <i class="bi bi-calendar3 me-1"></i>This Month
1673
+ </button>
1674
+ </div>
1675
+ </div>
1676
+
1677
+ <div class="d-grid gap-2">
1678
+ <button type="submit" class="btn btn-primary">
1679
+ <i class="bi bi-funnel me-1"></i>Apply Filter
1680
+ </button>
1681
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1682
+ <%
1683
+ # Preserve other query params when clearing creation filter
1684
+ clear_params = {
1685
+ clear_creation_filter: true,
1686
+ page: @current_page,
1687
+ per_page: @per_page,
1688
+ order_by: @order_by,
1689
+ order_direction: @order_direction
1690
+ }
1691
+ # Add column filters except created_at ones
1692
+ @column_filters.each do |key, value|
1693
+ unless key.to_s.start_with?('created_at')
1694
+ clear_params["column_filters[#{key}]"] = value
1695
+ end
1696
+ end
1697
+ %>
1698
+ <a href="<%= request.path %>?<%= clear_params.to_query %>" class="btn btn-outline-secondary">
1699
+ <i class="bi bi-x-circle me-1"></i>Clear Filter
1700
+ </a>
1701
+ <% end %>
1702
+ </div>
1703
+
1704
+ <div class="mt-3">
1705
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1706
+ <div class="alert alert-success alert-sm">
1707
+ <i class="bi bi-check-circle-fill me-1"></i>
1708
+ <strong>Filter Active:</strong> Showing records
1709
+ <% if @creation_filter_start.present? && @creation_filter_end.present? %>
1710
+ between <%= DateTime.parse(@creation_filter_start).strftime("%b %d, %Y %I:%M %p") %>
1711
+ and <%= DateTime.parse(@creation_filter_end).strftime("%b %d, %Y %I:%M %p") %>
1712
+ <% elsif @creation_filter_start.present? %>
1713
+ from <%= DateTime.parse(@creation_filter_start).strftime("%b %d, %Y %I:%M %p") %> onwards
1714
+ <% elsif @creation_filter_end.present? %>
1715
+ up to <%= DateTime.parse(@creation_filter_end).strftime("%b %d, %Y %I:%M %p") %>
1716
+ <% end %>
1717
+
1718
+ <% if @current_page == 1 && @records && @records.rows && @records.rows.empty? %>
1719
+ <div class="mt-2 text-warning">
1720
+ <i class="bi bi-exclamation-triangle-fill me-1"></i>
1721
+ No records match the current filter criteria.
1722
+ </div>
1723
+ <% end %>
1724
+ </div>
1725
+ <% end %>
1726
+ </div>
1727
+ </form>
1728
+ </div>
1729
+ </div>
1730
+
1731
+ <style>
1732
+ /* Floating creation filter button */
1733
+ .floating-creation-filter {
1734
+ position: fixed;
1735
+ bottom: 30px;
1736
+ right: 30px;
1737
+ z-index: 1050;
1738
+ }
1739
+
1740
+ .floating-filter-btn {
1741
+ width: 60px;
1742
+ height: 60px;
1743
+ border-radius: 50%;
1744
+ display: flex;
1745
+ align-items: center;
1746
+ justify-content: center;
1747
+ font-size: 1.2rem;
1748
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1749
+ border: none;
1750
+ position: relative;
1751
+ background: var(--bs-primary);
1752
+ color: white;
1753
+ box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.4);
1754
+ }
1755
+
1756
+ .floating-filter-btn:hover {
1757
+ transform: translateY(-3px) scale(1.05);
1758
+ box-shadow: 0 8px 25px rgba(var(--bs-primary-rgb), 0.5) !important;
1759
+ background: var(--bs-primary) !important;
1760
+ color: white !important;
1761
+ }
1762
+
1763
+ .floating-filter-btn:active {
1764
+ transform: translateY(-1px) scale(1.02);
1765
+ transition: all 0.1s ease;
1766
+ }
1767
+
1768
+ .floating-filter-btn:focus {
1769
+ outline: 2px solid rgba(var(--bs-primary-rgb), 0.5);
1770
+ outline-offset: 2px;
1771
+ }
1772
+
1773
+ /* Badge for active filter indicator */
1774
+ .floating-filter-btn .badge {
1775
+ font-size: 0.6rem;
1776
+ width: 18px;
1777
+ height: 18px;
1778
+ display: flex;
1779
+ align-items: center;
1780
+ justify-content: center;
1781
+ background: var(--bs-success) !important;
1782
+ animation: pulse 2s infinite;
1783
+ }
1784
+
1785
+ @keyframes pulse {
1786
+ 0% { transform: scale(1); }
1787
+ 50% { transform: scale(1.1); }
1788
+ 100% { transform: scale(1); }
1789
+ }
1790
+
1791
+ /* Better datetime input styling for the floating filter */
1792
+ #creationFilterOffcanvas .form-control {
1793
+ border-radius: 6px;
1794
+ border: 1px solid var(--bs-border-color);
1795
+ background-color: var(--bs-body-bg);
1796
+ color: var(--bs-body-color);
1797
+ transition: all 0.15s ease-in-out;
1798
+ }
1799
+
1800
+ #creationFilterOffcanvas .form-control:focus {
1801
+ border-color: var(--bs-primary);
1802
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1803
+ background-color: var(--bs-body-bg);
1804
+ }
1805
+
1806
+ /* Offcanvas enhancements */
1807
+ #creationFilterOffcanvas {
1808
+ backdrop-filter: blur(10px);
1809
+ }
1810
+
1811
+ #creationFilterOffcanvas .offcanvas-header {
1812
+ background: var(--bs-body-bg);
1813
+ border-bottom: 1px solid var(--bs-border-color);
1814
+ padding: 1.25rem;
1815
+ }
1816
+
1817
+ #creationFilterOffcanvas .offcanvas-body {
1818
+ background: var(--bs-body-bg);
1819
+ padding: 1.25rem;
1820
+ }
1821
+
1822
+ #creationFilterOffcanvas .offcanvas-title {
1823
+ color: var(--bs-body-color);
1824
+ font-weight: 600;
1825
+ }
1826
+
1827
+ /* Dark mode specific enhancements */
1828
+ [data-bs-theme="dark"] .floating-filter-btn {
1829
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.2);
1830
+ }
1831
+
1832
+ [data-bs-theme="dark"] .floating-filter-btn:hover {
1833
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.3) !important;
1834
+ }
1835
+
1836
+ [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-header {
1837
+ background: var(--bs-dark);
1838
+ border-bottom-color: var(--bs-border-color-translucent);
1839
+ }
1840
+
1841
+ [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-body {
1842
+ background: var(--bs-dark);
1843
+ }
1844
+
1845
+ [data-bs-theme="dark"] #creationFilterOffcanvas .form-control {
1846
+ background-color: var(--bs-body-bg);
1847
+ border-color: var(--bs-border-color-translucent);
1848
+ }
1849
+
1850
+ [data-bs-theme="dark"] #creationFilterOffcanvas .form-control:focus {
1851
+ background-color: var(--bs-body-bg);
1852
+ border-color: var(--bs-primary);
1853
+ }
1854
+
1855
+ /* Date range picker styling */
1856
+ #floatingCreationFilterRange {
1857
+ cursor: pointer;
1858
+ background-color: var(--bs-body-bg);
1859
+ color: var(--bs-body-color);
1860
+ border: 1px solid var(--bs-border-color);
1861
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
1862
+ }
1863
+
1864
+ #floatingCreationFilterRange:focus {
1865
+ border-color: var(--bs-primary);
1866
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1867
+ background-color: var(--bs-body-bg);
1868
+ }
1869
+
1870
+ #floatingCreationFilterRange::placeholder {
1871
+ color: var(--bs-secondary-color);
1872
+ opacity: 0.7;
1873
+ }
1874
+
1875
+ /* Enhanced dark mode support for input */
1876
+ [data-bs-theme="dark"] #floatingCreationFilterRange {
1877
+ background-color: var(--bs-body-bg);
1878
+ border-color: var(--bs-border-color-translucent);
1879
+ color: var(--bs-body-color);
1880
+ }
1881
+
1882
+ [data-bs-theme="dark"] #floatingCreationFilterRange:focus {
1883
+ background-color: var(--bs-body-bg);
1884
+ border-color: var(--bs-primary);
1885
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1886
+ }
1887
+
1888
+ /* Preset buttons styling */
1889
+ .preset-btn {
1890
+ font-size: 0.8rem;
1891
+ padding: 0.35rem 0.75rem;
1892
+ text-align: left;
1893
+ justify-content: flex-start;
1894
+ border: 1px solid var(--bs-border-color);
1895
+ background-color: var(--bs-body-bg);
1896
+ color: var(--bs-body-color);
1897
+ transition: all 0.2s ease;
1898
+ position: relative;
1899
+ overflow: hidden;
1900
+ }
1901
+
1902
+ .preset-btn::before {
1903
+ content: '';
1904
+ position: absolute;
1905
+ top: 0;
1906
+ left: -100%;
1907
+ width: 100%;
1908
+ height: 100%;
1909
+ background: linear-gradient(90deg, transparent, rgba(var(--bs-primary-rgb), 0.1), transparent);
1910
+ transition: left 0.3s ease;
1911
+ }
1912
+
1913
+ .preset-btn:hover {
1914
+ background-color: var(--bs-primary);
1915
+ color: white;
1916
+ border-color: var(--bs-primary);
1917
+ transform: translateY(-1px);
1918
+ box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.2);
1919
+ }
1920
+
1921
+ .preset-btn:hover::before {
1922
+ left: 100%;
1923
+ }
1924
+
1925
+ .preset-btn:active {
1926
+ transform: translateY(0);
1927
+ box-shadow: 0 1px 2px rgba(var(--bs-primary-rgb), 0.2);
1928
+ }
1929
+
1930
+ .preset-btn i {
1931
+ opacity: 0.8;
1932
+ transition: opacity 0.2s ease;
1933
+ }
1934
+
1935
+ .preset-btn:hover i {
1936
+ opacity: 1;
1937
+ }
1938
+
1939
+ /* Dark mode enhancements for preset buttons */
1940
+ [data-bs-theme="dark"] .preset-btn {
1941
+ background-color: var(--bs-dark);
1942
+ border-color: var(--bs-border-color-translucent);
1943
+ color: var(--bs-body-color);
1944
+ }
1945
+
1946
+ [data-bs-theme="dark"] .preset-btn:hover {
1947
+ background-color: var(--bs-primary);
1948
+ color: white;
1949
+ border-color: var(--bs-primary);
1950
+ box-shadow: 0 2px 8px rgba(var(--bs-primary-rgb), 0.3);
1951
+ }
1952
+
1953
+ /* Flatpickr theme adjustments */
1954
+ .flatpickr-calendar {
1955
+ border-radius: 8px;
1956
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2);
1957
+ border: 1px solid var(--bs-border-color);
1958
+ background: var(--bs-body-bg);
1959
+ font-family: var(--bs-body-font-family);
1960
+ }
1961
+
1962
+ .flatpickr-months {
1963
+ background: var(--bs-body-bg);
1964
+ border-bottom: 1px solid var(--bs-border-color);
1965
+ border-radius: 8px 8px 0 0;
1966
+ }
1967
+
1968
+ .flatpickr-month {
1969
+ color: var(--bs-body-color);
1970
+ fill: var(--bs-body-color);
1971
+ }
1972
+
1973
+ .flatpickr-current-month {
1974
+ color: var(--bs-body-color);
1975
+ }
1976
+
1977
+ .flatpickr-current-month .flatpickr-monthDropdown-month {
1978
+ background: var(--bs-body-bg);
1979
+ color: var(--bs-body-color);
1980
+ }
1981
+
1982
+ .flatpickr-weekdays {
1983
+ background: var(--bs-body-bg);
1984
+ }
1985
+
1986
+ .flatpickr-weekday {
1987
+ color: var(--bs-secondary-color);
1988
+ font-weight: 600;
1989
+ font-size: 0.75rem;
1990
+ }
1991
+
1992
+ .flatpickr-day {
1993
+ color: var(--bs-body-color);
1994
+ border-radius: 4px;
1995
+ transition: all 0.2s ease;
1996
+ }
1997
+
1998
+ .flatpickr-day:hover {
1999
+ background: var(--bs-primary-bg-subtle);
2000
+ color: var(--bs-primary);
2001
+ border-color: var(--bs-primary-border-subtle);
2002
+ }
2003
+
2004
+ .flatpickr-day.selected {
2005
+ background: var(--bs-primary);
2006
+ color: white;
2007
+ border-color: var(--bs-primary);
2008
+ box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.3);
2009
+ }
2010
+
2011
+ .flatpickr-day.selected:hover {
2012
+ background: var(--bs-primary);
2013
+ color: white;
2014
+ }
2015
+
2016
+ .flatpickr-day.inRange {
2017
+ background: var(--bs-primary-bg-subtle);
2018
+ color: var(--bs-primary);
2019
+ border-color: transparent;
2020
+ }
2021
+
2022
+ .flatpickr-day.startRange {
2023
+ background: var(--bs-primary);
2024
+ color: white;
2025
+ border-radius: 4px 0 0 4px;
2026
+ }
2027
+
2028
+ .flatpickr-day.endRange {
2029
+ background: var(--bs-primary);
2030
+ color: white;
2031
+ border-radius: 0 4px 4px 0;
2032
+ }
2033
+
2034
+ .flatpickr-day.startRange.endRange {
2035
+ border-radius: 4px;
2036
+ }
2037
+
2038
+ .flatpickr-day.today {
2039
+ border-color: var(--bs-primary);
2040
+ color: var(--bs-primary);
2041
+ font-weight: 600;
2042
+ }
2043
+
2044
+ .flatpickr-day.today:hover {
2045
+ background: var(--bs-primary);
2046
+ color: white;
2047
+ }
2048
+
2049
+ .flatpickr-day.disabled {
2050
+ color: var(--bs-secondary-color);
2051
+ opacity: 0.5;
2052
+ }
2053
+
2054
+ .flatpickr-time {
2055
+ background: var(--bs-body-bg);
2056
+ border-radius: 0 0 8px 8px;
2057
+ }
2058
+
2059
+ .flatpickr-time input {
2060
+ background: var(--bs-body-bg);
2061
+ color: var(--bs-body-color);
2062
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
2063
+ }
2064
+
2065
+ .flatpickr-time input:focus {
2066
+ border-color: var(--bs-primary);
2067
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
2068
+ outline: 0;
2069
+ }
2070
+
2071
+ .flatpickr-time .flatpickr-time-separator {
2072
+ color: var(--bs-body-color);
2073
+ }
2074
+
2075
+ .flatpickr-prev-month,
2076
+ .flatpickr-next-month {
2077
+ color: var(--bs-body-color);
2078
+ fill: var(--bs-body-color);
2079
+ transition: color 0.2s ease;
2080
+ }
2081
+
2082
+ .flatpickr-prev-month:hover,
2083
+ .flatpickr-next-month:hover {
2084
+ color: var(--bs-primary);
2085
+ fill: var(--bs-primary);
2086
+ }
2087
+
2088
+ /* Dark mode specific enhancements */
2089
+ [data-bs-theme="dark"] .flatpickr-calendar {
2090
+ background: var(--bs-dark);
2091
+ border-color: var(--bs-border-color-translucent);
2092
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4);
2093
+ }
2094
+
2095
+ [data-bs-theme="dark"] .flatpickr-months {
2096
+ background: var(--bs-dark);
2097
+ border-bottom-color: var(--bs-border-color-translucent);
2098
+ }
2099
+
2100
+ [data-bs-theme="dark"] .flatpickr-weekdays {
2101
+ background: var(--bs-dark);
2102
+ }
2103
+
2104
+ /* Enhanced dark mode day styling for better contrast */
2105
+ [data-bs-theme="dark"] .flatpickr-day {
2106
+ color: #e9ecef !important;
2107
+ background: transparent;
2108
+ border: 1px solid transparent;
2109
+ }
2110
+
2111
+ [data-bs-theme="dark"] .flatpickr-day:hover {
2112
+ background: rgba(var(--bs-primary-rgb), 0.25) !important;
2113
+ color: #ffffff !important;
2114
+ border-color: rgba(var(--bs-primary-rgb), 0.4);
2115
+ }
2116
+
2117
+ [data-bs-theme="dark"] .flatpickr-day.inRange {
2118
+ background: rgba(var(--bs-primary-rgb), 0.2) !important;
2119
+ color: #ffffff !important;
2120
+ border-color: transparent;
2121
+ }
2122
+
2123
+ [data-bs-theme="dark"] .flatpickr-day.selected {
2124
+ background: var(--bs-primary) !important;
2125
+ color: #ffffff !important;
2126
+ border-color: var(--bs-primary);
2127
+ box-shadow: 0 2px 6px rgba(var(--bs-primary-rgb), 0.4);
2128
+ }
2129
+
2130
+ [data-bs-theme="dark"] .flatpickr-day.selected:hover {
2131
+ background: var(--bs-primary) !important;
2132
+ color: #ffffff !important;
2133
+ }
2134
+
2135
+ [data-bs-theme="dark"] .flatpickr-day.startRange {
2136
+ background: var(--bs-primary) !important;
2137
+ color: #ffffff !important;
2138
+ border-radius: 4px 0 0 4px;
2139
+ }
2140
+
2141
+ [data-bs-theme="dark"] .flatpickr-day.endRange {
2142
+ background: var(--bs-primary) !important;
2143
+ color: #ffffff !important;
2144
+ border-radius: 0 4px 4px 0;
2145
+ }
2146
+
2147
+ [data-bs-theme="dark"] .flatpickr-day.startRange.endRange {
2148
+ border-radius: 4px;
2149
+ }
2150
+
2151
+ [data-bs-theme="dark"] .flatpickr-day.today {
2152
+ border-color: var(--bs-primary) !important;
2153
+ color: var(--bs-primary) !important;
2154
+ font-weight: 600;
2155
+ background: rgba(var(--bs-primary-rgb), 0.1);
2156
+ }
2157
+
2158
+ [data-bs-theme="dark"] .flatpickr-day.today:hover {
2159
+ background: var(--bs-primary) !important;
2160
+ color: #ffffff !important;
2161
+ }
2162
+
2163
+ [data-bs-theme="dark"] .flatpickr-day.disabled {
2164
+ color: #6c757d !important;
2165
+ opacity: 0.4;
2166
+ background: transparent !important;
2167
+ }
2168
+
2169
+ /* Dark mode other day states */
2170
+ [data-bs-theme="dark"] .flatpickr-day.nextMonthDay,
2171
+ [data-bs-theme="dark"] .flatpickr-day.prevMonthDay {
2172
+ color: #6c757d !important;
2173
+ opacity: 0.6;
2174
+ }
2175
+
2176
+ [data-bs-theme="dark"] .flatpickr-day.nextMonthDay:hover,
2177
+ [data-bs-theme="dark"] .flatpickr-day.prevMonthDay:hover {
2178
+ background: rgba(var(--bs-primary-rgb), 0.15) !important;
2179
+ color: #adb5bd !important;
2180
+ }
2181
+
2182
+ [data-bs-theme="dark"] .flatpickr-time {
2183
+ background: var(--bs-dark);
2184
+ border-top-color: var(--bs-border-color-translucent);
2185
+ }
2186
+
2187
+ [data-bs-theme="dark"] .flatpickr-time input {
2188
+ background: var(--bs-body-bg);
2189
+ border-color: var(--bs-border-color-translucent);
2190
+ color: #e9ecef !important;
2191
+ }
2192
+
2193
+ .flatpickr-calendar.hasTime .flatpickr-time {
2194
+ border-top: var(--bs-border-color-translucent);
2195
+ }
2196
+
2197
+ .flatpickr-next-month {
2198
+ color: var(--bs-body-color);
2199
+ fill: var(--bs-body-color);
2200
+ }
2201
+
2202
+ span.flatpickr-weekday {
2203
+ color: var(--bs-secondary-color);
2204
+ font-weight: 600;
2205
+ font-size: 0.75rem;
2206
+ }
2207
+
2208
+ [data-bs-theme="dark"] .flatpickr-time input:focus {
2209
+ border-color: var(--bs-primary);
2210
+ background: var(--bs-body-bg);
2211
+ color: #ffffff !important;
2212
+ }
2213
+
2214
+ [data-bs-theme="dark"] .flatpickr-time .flatpickr-time-separator {
2215
+ color: #e9ecef !important;
2216
+ }
2217
+
2218
+ /* Better focus states */
2219
+ .flatpickr-day:focus {
2220
+ outline: 2px solid var(--bs-primary);
2221
+ outline-offset: -2px;
2222
+ z-index: 10;
2223
+ }
2224
+
2225
+ /* Animation for calendar appearance */
2226
+ .flatpickr-calendar.open {
2227
+ animation: flatpickr-slideDown 0.2s ease-out;
2228
+ }
2229
+
2230
+ @keyframes flatpickr-slideDown {
2231
+ from {
2232
+ opacity: 0;
2233
+ transform: translateY(-10px) scale(0.98);
2234
+ }
2235
+ to {
2236
+ opacity: 1;
2237
+ transform: translateY(0) scale(1);
2238
+ }
2239
+ }
2240
+
2241
+ /* Small alert styling */
2242
+ .alert-sm {
2243
+ padding: 0.5rem;
2244
+ font-size: 0.875rem;
2245
+ }
2246
+
2247
+ /* Responsive adjustments - hide on smaller screens */
2248
+ @media (max-width: 991.98px) {
2249
+ .floating-creation-filter {
2250
+ display: none !important;
2251
+ }
2252
+ }
2253
+
2254
+ /* Flatpickr positioning and responsive adjustments */
2255
+ .flatpickr-calendar.arrowTop:before,
2256
+ .flatpickr-calendar.arrowTop:after {
2257
+ border-bottom-color: var(--bs-border-color);
2258
+ }
2259
+
2260
+ .flatpickr-calendar.arrowBottom:before,
2261
+ .flatpickr-calendar.arrowBottom:after {
2262
+ border-top-color: var(--bs-border-color);
2263
+ }
2264
+
2265
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:before,
2266
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:after {
2267
+ border-bottom-color: var(--bs-border-color-translucent);
2268
+ }
2269
+
2270
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:before,
2271
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:after {
2272
+ border-top-color: var(--bs-border-color-translucent);
2273
+ }
2274
+
2275
+ /* Ensure calendar stays within viewport on mobile */
2276
+ @media (max-width: 576px) {
2277
+ .flatpickr-calendar {
2278
+ max-width: calc(100vw - 20px);
2279
+ font-size: 14px;
2280
+ }
2281
+
2282
+ .flatpickr-day {
2283
+ height: 35px;
2284
+ line-height: 35px;
2285
+ }
2286
+
2287
+ .flatpickr-time input {
2288
+ font-size: 14px;
2289
+ }
2290
+ }
2291
+
2292
+ /* Improved accessibility */
2293
+ .flatpickr-calendar {
2294
+ font-family: inherit;
2295
+ }
2296
+
2297
+ .flatpickr-day[aria-label] {
2298
+ position: relative;
2299
+ }
2300
+
2301
+ /* Smooth scroll behavior for time inputs */
2302
+ .flatpickr-time input {
2303
+ scroll-behavior: smooth;
2304
+ }
2305
+
2306
+ /* Enhanced visual feedback for interactive elements */
2307
+ .flatpickr-prev-month,
2308
+ .flatpickr-next-month {
2309
+ border-radius: 4px;
2310
+ padding: 4px;
2311
+ margin: 2px;
2312
+ }
2313
+
2314
+ .flatpickr-prev-month:hover,
2315
+ .flatpickr-next-month:hover {
2316
+ background: rgba(var(--bs-primary-rgb), 0.1);
2317
+ }
2318
+
2319
+ /* Relationship count styling */
2320
+ .relationship-count .badge {
2321
+ min-width: 80px;
2322
+ display: inline-flex;
2323
+ align-items: center;
2324
+ justify-content: center;
2325
+ }
2326
+
2327
+ .relationship-count .spinner-border-sm {
2328
+ width: 0.875rem;
2329
+ height: 0.875rem;
2330
+ }
2331
+ </style>
2332
+
2333
+ <script>
2334
+ document.addEventListener('DOMContentLoaded', function() {
2335
+ // Initialize Flatpickr date range picker
2336
+ const dateRangeInput = document.getElementById('floatingCreationFilterRange');
2337
+ const startHidden = document.getElementById('creation_filter_start');
2338
+ const endHidden = document.getElementById('creation_filter_end');
2339
+
2340
+ if (dateRangeInput && typeof flatpickr !== 'undefined') {
2341
+ console.log('Flatpickr library loaded, initializing date range picker');
2342
+ // Store the Flatpickr instance in a variable accessible to all handlers
2343
+ let fp;
2344
+
2345
+ // Function to initialize Flatpickr
2346
+ function initializeFlatpickr(theme) {
2347
+ // Determine theme based on current document theme or passed parameter
2348
+ const currentTheme = theme || (document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light');
2349
+
2350
+ const config = {
2351
+ mode: 'range',
2352
+ enableTime: true,
2353
+ dateFormat: 'Y-m-d H:i',
2354
+ time_24hr: true,
2355
+ allowInput: false,
2356
+ clickOpens: true,
2357
+ theme: currentTheme,
2358
+ animate: true,
2359
+ position: 'auto',
2360
+ static: false,
2361
+ appendTo: document.body, // Ensure it renders above other elements
2362
+ locale: {
2363
+ rangeSeparator: ' to ',
2364
+ weekdays: {
2365
+ shorthand: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
2366
+ longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
2367
+ },
2368
+ months: {
2369
+ shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
2370
+ longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
2371
+ }
2372
+ },
2373
+ onOpen: function(selectedDates, dateStr, instance) {
2374
+ // Add a slight delay to apply theme-specific styling after calendar opens
2375
+ setTimeout(() => {
2376
+ const calendar = instance.calendarContainer;
2377
+ if (calendar) {
2378
+ // Apply theme-specific class for additional styling control
2379
+ calendar.classList.add(`flatpickr-${currentTheme}`);
2380
+
2381
+ // Ensure proper z-index for offcanvas overlay
2382
+ calendar.style.zIndex = '1070';
2383
+
2384
+ // Add elegant entrance animation
2385
+ calendar.classList.add('open');
2386
+ }
2387
+ }, 10);
2388
+ },
2389
+ onClose: function(selectedDates, dateStr, instance) {
2390
+ const calendar = instance.calendarContainer;
2391
+ if (calendar) {
2392
+ calendar.classList.remove('open');
2393
+ }
2394
+ },
2395
+ onChange: function(selectedDates, dateStr, instance) {
2396
+ console.log('Date range changed:', selectedDates);
2397
+
2398
+ if (selectedDates.length === 2) {
2399
+ // Format dates for hidden inputs (Rails expects ISO format)
2400
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2401
+ endHidden.value = selectedDates[1].toISOString().slice(0, 16);
2402
+
2403
+ // Update display with elegant formatting
2404
+ const formatOptions = {
2405
+ year: 'numeric',
2406
+ month: 'short',
2407
+ day: 'numeric',
2408
+ hour: '2-digit',
2409
+ minute: '2-digit',
2410
+ hour12: false
2411
+ };
2412
+
2413
+ const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2414
+ const endFormatted = selectedDates[1].toLocaleDateString('en-US', formatOptions);
2415
+ dateRangeInput.value = `${startFormatted} to ${endFormatted}`;
2416
+
2417
+ } else if (selectedDates.length === 1) {
2418
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2419
+ endHidden.value = '';
2420
+
2421
+ const formatOptions = {
2422
+ year: 'numeric',
2423
+ month: 'short',
2424
+ day: 'numeric',
2425
+ hour: '2-digit',
2426
+ minute: '2-digit',
2427
+ hour12: false
2428
+ };
2429
+
2430
+ const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2431
+ dateRangeInput.value = `${startFormatted} (select end date)`;
2432
+
2433
+ } else {
2434
+ startHidden.value = '';
2435
+ endHidden.value = '';
2436
+ dateRangeInput.value = '';
2437
+ }
2438
+ }
2439
+ };
2440
+
2441
+ return flatpickr(dateRangeInput, config);
2442
+ }
2443
+
2444
+ // Initialize date range picker
2445
+ fp = initializeFlatpickr();
2446
+
2447
+ // Set initial values if they exist
2448
+ if (startHidden.value || endHidden.value) {
2449
+ const dates = [];
2450
+ if (startHidden.value) {
2451
+ dates.push(new Date(startHidden.value));
2452
+ }
2453
+ if (endHidden.value) {
2454
+ dates.push(new Date(endHidden.value));
2455
+ }
2456
+ fp.setDate(dates);
2457
+ }
2458
+
2459
+ // Preset button functionality
2460
+ const presetButtons = document.querySelectorAll('.preset-btn');
2461
+ presetButtons.forEach(button => {
2462
+ button.addEventListener('click', function(event) {
2463
+ event.preventDefault(); // Prevent any form submission
2464
+
2465
+ const preset = this.getAttribute('data-preset');
2466
+ const now = new Date();
2467
+ let startDate, endDate;
2468
+
2469
+ console.log('Preset button clicked:', preset); // Debug log
2470
+
2471
+ switch (preset) {
2472
+ case 'lastminute':
2473
+ startDate = new Date(now);
2474
+ startDate.setMinutes(startDate.getMinutes() - 1);
2475
+ endDate = new Date(now);
2476
+ break;
2477
+ case 'last5minutes':
2478
+ startDate = new Date(now);
2479
+ startDate.setMinutes(startDate.getMinutes() - 5);
2480
+ endDate = new Date(now);
2481
+ break;
2482
+ case 'today':
2483
+ startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
2484
+ endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
2485
+ break;
2486
+ case 'yesterday':
2487
+ const yesterday = new Date(now);
2488
+ yesterday.setDate(yesterday.getDate() - 1);
2489
+ startDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 0, 0, 0);
2490
+ endDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59);
2491
+ break;
2492
+ case 'last7days':
2493
+ startDate = new Date(now);
2494
+ startDate.setDate(startDate.getDate() - 7);
2495
+ startDate.setHours(0, 0, 0, 0);
2496
+ endDate = new Date(now);
2497
+ endDate.setHours(23, 59, 59, 999);
2498
+ break;
2499
+ case 'last30days':
2500
+ startDate = new Date(now);
2501
+ startDate.setDate(startDate.getDate() - 30);
2502
+ startDate.setHours(0, 0, 0, 0);
2503
+ endDate = new Date(now);
2504
+ endDate.setHours(23, 59, 59, 999);
2505
+ break;
2506
+ case 'thismonth':
2507
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
2508
+ endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
2509
+ break;
2510
+ }
2511
+
2512
+ if (startDate && endDate && fp) {
2513
+ console.log('Setting dates:', startDate, endDate); // Debug log
2514
+ fp.setDate([startDate, endDate]);
2515
+
2516
+ // Also update the hidden inputs directly as a fallback
2517
+ startHidden.value = startDate.toISOString().slice(0, 16);
2518
+ endHidden.value = endDate.toISOString().slice(0, 16);
2519
+
2520
+ // Update the display value
2521
+ const formattedStart = startDate.toLocaleDateString() + ' ' + startDate.toLocaleTimeString();
2522
+ const formattedEnd = endDate.toLocaleDateString() + ' ' + endDate.toLocaleTimeString();
2523
+ dateRangeInput.value = formattedStart + ' to ' + formattedEnd;
2524
+ } else {
2525
+ console.error('Failed to set dates - startDate:', startDate, 'endDate:', endDate, 'fp:', fp);
2526
+ }
2527
+ });
2528
+ });
2529
+
2530
+ // Listen for theme changes and update Flatpickr theme
2531
+ document.addEventListener('dbviewerThemeChanged', function(e) {
2532
+ const newTheme = e.detail.theme === 'dark' ? 'dark' : 'light';
2533
+ console.log('Theme changed to:', newTheme);
2534
+
2535
+ // Destroy and recreate with new theme
2536
+ if (fp) {
2537
+ const currentDates = fp.selectedDates;
2538
+ fp.destroy();
2539
+ fp = initializeFlatpickr(newTheme);
2540
+
2541
+ // Restore previous values if they existed
2542
+ if (currentDates && currentDates.length > 0) {
2543
+ fp.setDate(currentDates);
2544
+ }
2545
+ }
2546
+ });
2547
+
2548
+ // Also listen for direct data-bs-theme attribute changes using MutationObserver
2549
+ const themeObserver = new MutationObserver(function(mutations) {
2550
+ mutations.forEach(function(mutation) {
2551
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-bs-theme') {
2552
+ const newTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light';
2553
+ console.log('Theme attribute changed to:', newTheme);
2554
+
2555
+ if (fp) {
2556
+ const currentDates = fp.selectedDates;
2557
+ fp.destroy();
2558
+ fp = initializeFlatpickr(newTheme);
2559
+
2560
+ // Restore previous values if they existed
2561
+ if (currentDates && currentDates.length > 0) {
2562
+ fp.setDate(currentDates);
2563
+ }
2564
+ }
2565
+ }
2566
+ });
2567
+ });
2568
+
2569
+ // Start observing theme changes
2570
+ themeObserver.observe(document.documentElement, {
2571
+ attributes: true,
2572
+ attributeFilter: ['data-bs-theme']
2573
+ });
2574
+ } else {
2575
+ console.error('Date range picker initialization failed:', {
2576
+ dateRangeInput: !!dateRangeInput,
2577
+ flatpickr: typeof flatpickr !== 'undefined'
2578
+ });
2579
+ }
2580
+
2581
+ // Close offcanvas after form submission
2582
+ const form = document.getElementById('floatingCreationFilterForm');
2583
+ if (form) {
2584
+ form.addEventListener('submit', function() {
2585
+ const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('creationFilterOffcanvas'));
2586
+ if (offcanvas) {
2587
+ setTimeout(() => {
2588
+ offcanvas.hide();
2589
+ }, 100);
2590
+ }
2591
+ });
2592
+ }
2593
+ });
2594
+ </script>
2595
+ <% end %>