## APP HR
Aplikasi untuk Human Resources seperti pendataan karyawan, penggajian, absensi, cuti dlll

Plugin yang di gunakan:
- Role & Permission menggunakan `spatie/laravel-permission`
- Untuk pencatatan menggunakan `owen-it/laravel-auditing`
- Untuk Icon `owenvoke/blade-fontawesome` and `blade-ui-kit/blade-icons` (Bisa di tambah icon lain jika di butuhkan)
- Untuk kebutuhan import dan export data `maatwebsite/excel`
- Kebutuhan developer `barryvdh/laravel-debugbar`

Jika ada script `javascript` berulang silahkan tulis di `resources/js/script.js`
Jika ada costume css silahkan langsung tulis di `tailwind.config.js`
Buat `componen` jika ada html yang sering berulang agar lebih mudah hanya memperbaiki di satu tempat 

Untuk lingkungan dev:
```
npm run dev
```
```
php artisan serve
```

Untuk lingkungan prod:
```
npm run build
```

## Menambahkan file css dan js jika di perlukan
Daftarkan file di dalam `vite.config.js` ini bertujuan untuk bisa di build dengan cara `npm run build`
Jangan lupa tambahkan juga di `resources\views\layouts\app.blade.php` pada bagian `@vite([ ... ])`

---

## *Role-Based Access Control* (RBAC)
Command ini akan insert/create permission ke table permission berdasarkan config dan datatable yang ada.
```bash
php artisan app:fresh-permission
```
### **1. Command untuk Generate Policy Semua Model**

Command ini akan memindai semua model yang ada di direktori `app/Models` dan otomatis membuat file policy serta permission terkait untuk setiap model.

#### **Sintaks**

```bash
php artisan app:regenerate-policies
```

#### **Deskripsi**
- **Fungsi**: Mengenerate ulang policy dan permissions untuk semua model di aplikasi.
- **Output**: 
  - File policy untuk setiap model di direktori yang ditentukan.
  - Permissions untuk setiap model yang ditambahkan ke sistem (menggunakan library `spatie/laravel-permission`).

#### **Contoh Penggunaan**

Jalankan command berikut di terminal:

```bash
php artisan app:regenerate-policies
```

#### **Output**
- File policy baru akan dibuat di direktori yang ditentukan.
- Permissions baru akan tercatat di database.

---

### **2. Command untuk Generate Policy per Model**

Command ini digunakan untuk mengenerate policy dan permissions untuk satu model tertentu. Anda perlu memberikan nama model sebagai argumen.

#### **Sintaks**

```bash
php artisan app:generate-policies {model}
```

#### **Deskripsi**
- **Fungsi**: Membuat file policy dan permissions hanya untuk model yang spesifik.
- **Parameter**:
  - `{model}`: Nama model (tanpa ekstensi `.php`) untuk mana policy akan dihasilkan.
- **Output**: 
  - File policy untuk model yang dipilih.
  - Permissions terkait untuk model tersebut ditambahkan ke sistem.

#### **Contoh Penggunaan**

Jalankan command berikut dengan nama model:

```bash
php artisan app:generate-policies User
```

#### **Output**
- File policy untuk model `User` akan dibuat di direktori yang ditentukan.
- Permissions seperti `create_user`, `edit_user`, dll., akan ditambahkan ke database.

---

### **Konfigurasi yang Digunakan**
Command-command ini bergantung pada beberapa konfigurasi di file `config/rbac.php`:

1. **`rbac.generator.policy_namespace`**  
   Namespace yang digunakan untuk policy yang dihasilkan. Contoh: `App\Policies`.

2. **`rbac.generator.policy_directory`**  
   Direktori tempat policy akan disimpan. Contoh: `Policies`.

3. **`rbac.permission_prefixes.resource`**  
   Array yang berisi prefix permission untuk model. Contoh:  
   ```php
   'resource' => ['create', 'read', 'update', 'delete']
   ```

---

## **Menambahkan audit di model**
Extend plugin audit di model
```php
    use OwenIt\Auditing\Contracts\Auditable;
    class {Nama Model yang di audit} extends Model implements Auditable
    {
        use HasFactory, \OwenIt\Auditing\Auditable;
    }
```

Cara view Audit

```php 
use App\Models\Reimbursement;
$reimbursement = Reimbursement::find(1);
$audits = $reimbursement->audits;

foreach ($audits as $audit) {
    echo "Old Values: " . json_encode($audit->old_values);
    echo "New Values: " . json_encode($audit->new_values);
    echo "User ID: " . $audit->user_id;
}
```
## **Catatan Penting**
- Policy template berasal dari file stub di direktori `stubs/Policy.stub`.
- Permissions diatur menggunakan library `spatie/laravel-permission`.

## JS
`window.namaFunsi` Or `window.namaVariabel` ini menunjukan bahwa Fungsi atau Variabel bisa di akses secara dimanapun karena `window` adalah object global.

## **Menggunakan format currency**
Tambahkan class `amountIDR` pada element input dengan type text.
Menggunakan package `npm install autonumeric`.

```JS
window.initAutoNumeric = function () {
    document.querySelectorAll('.amountIDR').forEach(element => {
        new AutoNumeric(element, {
            currencySymbol: '', // Tidak ada simbol mata uang
            decimalCharacter: ',',
            digitGroupSeparator: '.',
            currencySymbolPlacement: 'p',
            decimalPlaces: 0,
            minimumValue: '0'
        });
    });
};
```
Penggunaan di HTML cukup gunakan attribute `class="amountIDR"` agar nominal currency sesuai dengan format yang sudah di tetapkan.

## **Menambahkan filter tanggal di datatabel**
```JS
window.initDataTable = function (selector = "#search-table", multiSelect = true, rowNavigation = false) {
    const tableElement = document.querySelector(selector);

    if (tableElement && !tableElement.classList.contains('datatable-initialized')) {
        let table = new DataTable(tableElement, {
            responsive: true, // Mengaktifkan fitur responsif
            paging: true, // Aktifkan paginasi
            pageLength: 10, // Jumlah item per halaman
            perPageSelect: [10, 25, 50, 100], // Pilihan jumlah item per halaman
            searching: true, // Aktifkan pencarian
            ordering: true, // Aktifkan sorting
            rowRender: (row, tr, _index) => {
                if (!tr.attributes) {
                    tr.attributes = {};
                }
                if (!tr.attributes.class) {
                    tr.attributes.class = "";
                }
                if (row.selected) {
                    tr.attributes.class += " selected";
                } else {
                    tr.attributes.class = tr.attributes.class.replace(" selected", "");
                }
                return tr;
            },
            template: (options, dom) => `
                <div class='${options.classes.top}'>
                    <div class='flex justify-between sm:flex-row sm:items-center rtl:space-x-reverse w-full sm:w-auto'>

                    <!-- Date Range Picker -->
                    <div class="flex items-center">
                        <div class="relative block w-full">
                            <input type="text" id="dateRangePicker" name="dateRangePicker"
                                class='datatable-input'
                                data-date-range="${new URL(window.location.href).searchParams.get('date_range') || ''}"
                                placeholder="Pilih tanggal..." value="${new URL(window.location.href).searchParams.get('date_range') || ''}" readonly />
                            <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
                                📅
                            </div>
                        </div>
                    </div>

                        ${options.paging && options.perPageSelect ? `
                            <div class='${options.classes.dropdown} flex'>
                                <label>
                                    <select class='${options.classes.selector}'></select> ${options.labels.perPage}
                                </label>
                            </div>` : ""
                        }
                    </div>

                    ${options.searchable ? `
                        <div class='${options.classes.search}'>
                            <input class='${options.classes.input}' placeholder='${options.labels.placeholder}' type='search' title='${options.labels.searchTitle}' ${(dom.id ? ` aria-controls='${dom.id}'` : "")}>
                        </div>` : ""
                    }
                </div>
                <div class='${options.classes.container}' ${options.scrollY.length ? ` style='height: ${options.scrollY}; overflow-Y: auto;'` : ""}></div>
                <div class='${options.classes.bottom}'>
                    ${options.paging ? `<div class='${options.classes.info}'></div>` : ""}
                    <nav class='${options.classes.pagination}'></nav>
                </div>
            `
        });

        // Tandai agar tidak diinisialisasi dua kali
        tableElement.classList.add('datatable-initialized');

        // Mark all rows as unselected initially
        table.data.data.forEach(data => {
            data.selected = false;
        });

        // Event handler untuk pemilihan baris
        table.on("datatable.selectrow", (rowIndex, event) => {
            event.preventDefault();
            const row = table.data.data[rowIndex];
            if (row.selected) {
                row.selected = false;
            } else {
                if (!multiSelect) {
                    table.data.data.forEach(data => {
                        data.selected = false;
                    });
                }
                row.selected = true;
            }
            table.update();
        });

        // Nonaktifkan navigasi baris jika di perangkat mobile
        const isMobile = window.matchMedia("(any-pointer:coarse)").matches;
        if (isMobile) {
            rowNavigation = false;
        }
    }
};
```
Pada bagian ini ` template: (options, dom) => ...` ini adalah tempat menambahkan filter tanggal pada tabel ketika di buat

penggunaan di html nya di taro di bagian `script`
```JS
initDataTable("#search-table");
initDataTable("#selection-table", true, false); // Multi-Select aktif, Row Navigation non-aktif
```
Penggunaan di HTML nya cukup menggunakan `id="search-table"` or `id="selection-table"`
[  ] Untuk `selection-table` ini belum fix tapi sudah bisa select satu persatu tiap row table tpi belum ada action lainnya.

## **Control Modal Approve, Delete & Etc**
```JS
document.addEventListener("click", function(event) {
    let approveBtn = event.target.closest("[data-approve]");
    let deleteBtn = event.target.closest("[data-delete]");
    let etcBtn = event.target.closest("[data-etc]");

    if (approveBtn) {
        let approvedId = approveBtn.dataset.approve;
        let data= approveBtn.dataset.detail;
        let approveUrl = approveBtn.dataset.urlApprove;
        let rejectUrl = approveBtn.dataset.urlReject;

        // Alpine.store("selectedApproved", approvedId);
        console.log("Data: ", data);
        console.log("Approve ID:", approvedId);
        console.log("Approve URL:", approveUrl);
        console.log("Reject URL:", rejectUrl);

        // Update action URL untuk form dalam modal
        document.querySelector("#view-detail-approve").textContent = data;
        document.querySelector("#approve-form").setAttribute("action", approveUrl);
        document.querySelector("#reject-form").setAttribute("action", rejectUrl);

        // Buka modal approve
        window.dispatchEvent(new CustomEvent("open-modal", { detail: "approve-modal" }));
    }

    if (deleteBtn) {
        let deletedId = deleteBtn.dataset.delete;
        let data= deleteBtn.dataset.detail;
        let deleteUrl = deleteBtn.dataset.urlDelete;

        // Alpine.store("selectedDeleted", deletedId);
        console.log("Data: ", data);
        console.log("Delete ID:", deletedId);
        console.log("Delete URL:", deleteUrl);

        // Update action URL untuk form dalam modal
        document.querySelector("#view-detail-delete").textContent = data;
        document.querySelector("#delete-form").setAttribute("action", deleteUrl);

        // Buka modal delete
        window.dispatchEvent(new CustomEvent("open-modal", { detail: "delete-modal" }));
    }

    if (etcBtn) {
        window.dispatchEvent(new CustomEvent("open-modal", { detail: "etc-modal" }));
    }
});
```
Modal Approve: Ini berisi approve dan reject data
Modal Delete: Ini berisi confimation delete data
Modal Etc: Ini bisa di modif sesuka hati sesuai dengan kebutuhan kamu (Example ada di Add Employee: **Bulk Employee**)

## **Menggunakan flatpickr**
```JS
flatpickr("#timepicker", {
    enableTime: true,
    noCalendar: true,
    dateFormat: "H:i",
    time_24hr: true
});
```
Untuk input jam

## **Menggunakan daterangepicker**
```JS
window.setupDateRangePicker = function(id) {
    $(id).daterangepicker({
        autoUpdateInput: false,
        ranges: {
            'Today': [moment(), moment()],
            'This Month': [moment().startOf('month'), moment().endOf('month')],
            'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')],
            'This Year': [moment().startOf('year'), moment().endOf('year')],
            'Last Year': [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')]
        },
        locale: {
            format: 'YYYY-MM-DD',
            cancelLabel: 'Clear',
            applyLabel: "Apply",
            fromLabel: "From",
            toLabel: "To",
            customRangeLabel: "Custom"
        }
    });

    $(id).on('apply.daterangepicker', function (ev, picker) {
        let startDate = picker.startDate.format('YYYY-MM-DD');
        let endDate = picker.endDate.format('YYYY-MM-DD');
        $(this).val(startDate + ' - ' + endDate);

        // Redirect ke halaman dengan parameter filter tanggal
        let url = new URL(window.location.href);
        url.searchParams.set('date_range', startDate + ' - ' + endDate);
        window.location.href = url.toString();
    });

    $(id).on('cancel.daterangepicker', function () {
        $(this).val('');

        // Redirect ke halaman tanpa filter tanggal
        let url = new URL(window.location.href);
        url.searchParams.delete('date_range');
        window.location.href = url.toString();
    });
}
```
Kode ini di tunjukan untuk search data tabel di setiap tabel yang di tampilkan. tinggal sesuaikan di controller setiap data yang di sajikan.

```JS

$('[id^="datepicker-"]').each(function () {
    let $this = $(this);
    let type = $this.attr('id').split('-')[1]; // Ambil bagian setelah "datepicker-"

    // Tentukan minDate berdasarkan tipe yang ditemukan dalam ID
    let minDate;
    switch (type) {
        case 'day':
            minDate = moment().startOf('day'); // Hari ini
            break;
        case 'week':
            minDate = moment().subtract(7, 'days').startOf('day'); // 7 hari sebelumnya
            break;
        case '2weeks':
            minDate = moment().subtract(2, 'weeks').startOf('day'); // 2 minggu sebelumnya
            break;
        case 'month':
            minDate = moment().subtract(1, 'months').startOf('day'); // 1 bulan sebelumnya
            break;
        default:
            minDate = moment().startOf('day'); // Default ke hari ini jika tidak ada match
    }

    // Inisialisasi DateRangePicker
    $this.daterangepicker({
        singleDatePicker: true,
        minDate: minDate,
        minYear: moment().year(),
        maxYear: parseInt(moment().format('YYYY'), 10),
        autoApply: true,
        locale: {
            format: 'YYYY-MM-DD',
        }
    });
});
```
Example Penggunaan di html:
``` HTML
<input type="text" id="datepicker-day" />
<input type="text" id="datepicker-week" />
<input type="text" id="datepicker-2weeks" />
<input type="text" id="datepicker-month" />
```
