upload project
This commit is contained in:
commit
faadf63050
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=sas
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
FROM php:8.2-fpm as php
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
zip \
|
||||
unzip \
|
||||
libfreetype6-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libpng-dev \
|
||||
libzip-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure and install PHP extensions
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
gd \
|
||||
pdo_mysql \
|
||||
zip \
|
||||
opcache
|
||||
|
||||
# Install Composer
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Install PHP dependencies
|
||||
RUN composer install --optimize-autoloader --no-scripts --no-interaction
|
||||
|
||||
# Set permissions
|
||||
RUN chown -R www-data:www-data /app && chmod -R 755 /app
|
||||
|
||||
# Expose port
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["php-fpm"]
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
|
||||
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace App\Data;
|
||||
|
||||
use App\Enums\NotificationType;
|
||||
|
||||
class NotificationCreateTemplate
|
||||
{
|
||||
public const LEVEL_ALL = 'all';
|
||||
public const LEVEL_CLASS = 'class';
|
||||
public const LEVEL_STUDENT = 'student';
|
||||
|
||||
public function __construct(
|
||||
public readonly string $title,
|
||||
public readonly string $body,
|
||||
public readonly NotificationType $type,
|
||||
public readonly string $level,
|
||||
public readonly ?int $senderId = null,
|
||||
public readonly ?int $receiverId = null,
|
||||
public readonly ?int $classId = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function forAll(
|
||||
string $title,
|
||||
string $body,
|
||||
NotificationType|string $type,
|
||||
?int $senderId = null,
|
||||
): self {
|
||||
return new self(
|
||||
title: $title,
|
||||
body: $body,
|
||||
type: self::normalizeType($type),
|
||||
level: self::LEVEL_ALL,
|
||||
senderId: $senderId,
|
||||
);
|
||||
}
|
||||
|
||||
public static function forClass(
|
||||
int $classId,
|
||||
string $title,
|
||||
string $body,
|
||||
NotificationType|string $type,
|
||||
?int $senderId = null,
|
||||
): self {
|
||||
return new self(
|
||||
title: $title,
|
||||
body: $body,
|
||||
type: self::normalizeType($type),
|
||||
level: self::LEVEL_CLASS,
|
||||
senderId: $senderId,
|
||||
classId: $classId,
|
||||
);
|
||||
}
|
||||
|
||||
public static function forStudent(
|
||||
int $receiverId,
|
||||
string $title,
|
||||
string $body,
|
||||
NotificationType|string $type,
|
||||
?int $senderId = null,
|
||||
): self {
|
||||
return new self(
|
||||
title: $title,
|
||||
body: $body,
|
||||
type: self::normalizeType($type),
|
||||
level: self::LEVEL_STUDENT,
|
||||
senderId: $senderId,
|
||||
receiverId: $receiverId,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'type' => $this->type->value,
|
||||
'send_to' => $this->level,
|
||||
'sender_id' => $this->senderId,
|
||||
'receiver_id' => $this->receiverId,
|
||||
'class_id' => $this->classId,
|
||||
];
|
||||
}
|
||||
|
||||
private static function normalizeType(NotificationType|string $type): NotificationType
|
||||
{
|
||||
if ($type instanceof NotificationType) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
return NotificationType::from($type);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace App\Data;
|
||||
|
||||
class NotificationTemplate
|
||||
{
|
||||
public const TARGET_TOKEN = 'token';
|
||||
public const TARGET_TOPIC = 'topic';
|
||||
|
||||
public function __construct(
|
||||
public readonly string $targetType = self::TARGET_TOKEN,
|
||||
public readonly ?string $targetValue = null,
|
||||
public readonly string $title = 'Judul Notifikasi',
|
||||
public readonly string $body = 'Isi notifikasi',
|
||||
public readonly array $data = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public static function excelAliases(): array
|
||||
{
|
||||
return [
|
||||
'target_type' => ['target_type', 'target', 'send_type'],
|
||||
'target_value' => ['target_value', 'target_to', 'send_to'],
|
||||
'token' => ['token', 'fcm_token'],
|
||||
'topic' => ['topic', 'fcm_topic'],
|
||||
'title' => ['title', 'judul'],
|
||||
'body' => ['body', 'isi'],
|
||||
'data' => ['data', 'payload'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function excelColumns(): array
|
||||
{
|
||||
return ['target_type', 'target_value', 'title', 'body', 'data'];
|
||||
}
|
||||
|
||||
public static function example(): array
|
||||
{
|
||||
return [
|
||||
'target_type' => self::TARGET_TOKEN,
|
||||
'target_value' => 'fcm_token_device',
|
||||
'title' => 'Judul Notifikasi',
|
||||
'body' => 'Isi notifikasi',
|
||||
'data' => [
|
||||
'type' => 'announcement',
|
||||
'source' => 'excel-import',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function topicExample(): array
|
||||
{
|
||||
return [
|
||||
'target_type' => self::TARGET_TOPIC,
|
||||
'target_value' => 'student_notifications',
|
||||
'title' => 'Pengumuman Kelas',
|
||||
'body' => 'Ada update jadwal hari ini',
|
||||
'data' => [
|
||||
'type' => 'class_announcement',
|
||||
'source' => 'excel-import',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function forToken(string $token, string $title, string $body, array $data = []): self
|
||||
{
|
||||
return new self(
|
||||
targetType: self::TARGET_TOKEN,
|
||||
targetValue: $token,
|
||||
title: $title,
|
||||
body: $body,
|
||||
data: $data,
|
||||
);
|
||||
}
|
||||
|
||||
public static function forTopic(string $topic, string $title, string $body, array $data = []): self
|
||||
{
|
||||
return new self(
|
||||
targetType: self::TARGET_TOPIC,
|
||||
targetValue: $topic,
|
||||
title: $title,
|
||||
body: $body,
|
||||
data: $data,
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromArray(array $attributes): self
|
||||
{
|
||||
$targetType = self::normalizeTargetType(
|
||||
self::pickValue($attributes, self::excelAliases()['target_type'])
|
||||
);
|
||||
|
||||
$targetValue = self::pickValue($attributes, self::excelAliases()['target_value']);
|
||||
|
||||
if (!$targetValue) {
|
||||
$targetValue = $targetType === self::TARGET_TOPIC
|
||||
? self::pickValue($attributes, self::excelAliases()['topic'])
|
||||
: self::pickValue($attributes, self::excelAliases()['token']);
|
||||
}
|
||||
|
||||
if (!$targetValue && self::pickValue($attributes, self::excelAliases()['topic'])) {
|
||||
$targetType = self::TARGET_TOPIC;
|
||||
$targetValue = self::pickValue($attributes, self::excelAliases()['topic']);
|
||||
}
|
||||
|
||||
return new self(
|
||||
targetType: $targetType,
|
||||
targetValue: $targetValue,
|
||||
title: self::pickValue($attributes, self::excelAliases()['title']) ?? 'Judul Notifikasi',
|
||||
body: self::pickValue($attributes, self::excelAliases()['body']) ?? 'Isi notifikasi',
|
||||
data: self::normalizeData(self::pickValue($attributes, self::excelAliases()['data'])),
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'target_type' => $this->targetType,
|
||||
'target_value' => $this->targetValue,
|
||||
'token' => $this->isToken() ? $this->targetValue : null,
|
||||
'topic' => $this->isTopic() ? $this->targetValue : null,
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'data' => $this->data,
|
||||
];
|
||||
}
|
||||
|
||||
public function isToken(): bool
|
||||
{
|
||||
return $this->targetType === self::TARGET_TOKEN;
|
||||
}
|
||||
|
||||
public function isTopic(): bool
|
||||
{
|
||||
return $this->targetType === self::TARGET_TOPIC;
|
||||
}
|
||||
|
||||
public function target(): ?string
|
||||
{
|
||||
return $this->targetValue;
|
||||
}
|
||||
|
||||
private static function pickValue(array $attributes, array $keys): mixed
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
if (array_key_exists($key, $attributes)) {
|
||||
return $attributes[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function normalizeData(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : ['raw' => $value];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function normalizeTargetType(mixed $value): string
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return self::TARGET_TOKEN;
|
||||
}
|
||||
|
||||
$normalized = strtolower(trim($value));
|
||||
|
||||
return match ($normalized) {
|
||||
'topic', 'fcm_topic' => self::TARGET_TOPIC,
|
||||
default => self::TARGET_TOKEN,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum NotificationType: string
|
||||
{
|
||||
case AnnouncementAcademic = 'announcement_academic'; // pengumuman akademis
|
||||
case AnnouncementGeneral = 'announcement_general'; // pengumuman umum
|
||||
case LostAndFound = 'lost_and_found'; // kehilangan dan penemuan
|
||||
case EmergencyInfo = 'emergency_info'; // info darurat
|
||||
case ClassCancelled = 'class_cancelled'; // kelas dibatalkan
|
||||
case AnnouncementForClass = 'announcement_for_class'; // pengumuman untuk kelas tertentu
|
||||
case Assignment = 'assignment'; // tugas atau pekerjaan rumah
|
||||
case Permission = 'permission'; // izin (misal izin tidak masuk) (dari siswa ke sistem).
|
||||
case PermissionAccepted = 'permission_accepted'; // izin disetujui
|
||||
case PermissionRejected = 'permission_rejected'; // izin ditolak
|
||||
case AttendanceViolation = 'attendance_violation'; // pelanggaran kehadiran (misal terlambat, bolos, dll)
|
||||
case PersonalNote = 'personal_note'; // catatan pribadi (misal guru memberikan catatan khusus untuk siswa tertentu)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
if (!function_exists('getDistanceInMeters')) {
|
||||
function getDistanceInMeters($lat1, $lon1, $lat2, $lon2) {
|
||||
$earthRadius = 6371000; // radius (meter)
|
||||
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLon = deg2rad($lon2 - $lon1);
|
||||
|
||||
$a = sin($dLat/2) * sin($dLat/2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLon/2) * sin($dLon/2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
|
||||
return $earthRadius * $c;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,923 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AttendanceHistory;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Student;
|
||||
use App\Models\Subject;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
|
||||
class AttendanceHistoryController extends Controller
|
||||
{
|
||||
public function absensiMapel($classId, Request $request)
|
||||
{
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
|
||||
// Ambil semua siswa di kelas
|
||||
$allStudents = Student::where('id_class', $classId)->get();
|
||||
|
||||
// Ambil daftar mapel dari jadwal kelas
|
||||
$mapelList = Schedule::with('subject')
|
||||
->where('id_class', $classId)
|
||||
->distinct('id_subject')
|
||||
->get()
|
||||
->map(function($schedule) {
|
||||
return $schedule->subject;
|
||||
})
|
||||
->unique('id')
|
||||
->sortBy('name')
|
||||
->values();
|
||||
|
||||
// Default ke hari ini jika tidak ada filter
|
||||
$tanggal = $request->get('tanggal', now()->format('Y-m-d'));
|
||||
$subjectId = $request->get('subject_id');
|
||||
|
||||
$absensi = collect();
|
||||
$belumAbsen = $allStudents;
|
||||
|
||||
// Jika ada subject filter, ambil data berdasarkan tanggal + day_of_week
|
||||
if ($subjectId) {
|
||||
// Parse tanggal untuk ambil day of week
|
||||
$date = \Carbon\Carbon::createFromFormat('Y-m-d', $tanggal);
|
||||
|
||||
// Map hari ke format Indonesia
|
||||
$dayMap = [
|
||||
0 => 'Minggu',
|
||||
1 => 'Senin',
|
||||
2 => 'Selasa',
|
||||
3 => 'Rabu',
|
||||
4 => 'Kamis',
|
||||
5 => 'Jumat',
|
||||
6 => 'Sabtu'
|
||||
];
|
||||
|
||||
$dayName = $dayMap[$date->dayOfWeek] ?? '';
|
||||
|
||||
// Get schedule IDs untuk subject dan day ini
|
||||
$scheduleIds = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $subjectId)
|
||||
->where('day_of_week', $dayName)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Get attendance history untuk tanggal dan schedules spesifik
|
||||
if (!empty($scheduleIds)) {
|
||||
$absensi = AttendanceHistory::with(['student', 'class'])
|
||||
->whereIn('id_schedule', $scheduleIds)
|
||||
->where(function($q) use ($tanggal) {
|
||||
$q->whereDate('created_at', $tanggal)
|
||||
->orWhereDate('attendance_date', $tanggal);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
// Ambil siswa yang sudah absen untuk tanggal + day ini
|
||||
$sudahAbsenIds = $absensi->pluck('id_student')->unique();
|
||||
|
||||
// Siswa yang belum absen untuk tanggal + day ini
|
||||
$belumAbsen = $allStudents->whereNotIn('id', $sudahAbsenIds)->values();
|
||||
}
|
||||
|
||||
return view('pages.absensi.absensi_mapel', [
|
||||
'absensi' => $absensi,
|
||||
'kelas' => $kelas,
|
||||
'belumAbsen' => $belumAbsen,
|
||||
'mapelList' => $mapelList,
|
||||
'allStudents' => $allStudents,
|
||||
'selectedTanggal' => $tanggal,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getAbsensiByMapel($classId, $subjectId, Request $request)
|
||||
{
|
||||
try {
|
||||
// Get schedules untuk subject ini di kelas ini
|
||||
$scheduleIds = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $subjectId)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Build query untuk attendance history
|
||||
$query = AttendanceHistory::with(['student', 'class']);
|
||||
|
||||
if (!empty($scheduleIds)) {
|
||||
$query->whereIn('id_schedule', $scheduleIds);
|
||||
}
|
||||
|
||||
// Apply date filters jika ada
|
||||
$bulan = $request->get('bulan');
|
||||
$tahun = $request->get('tahun');
|
||||
|
||||
if ($bulan && $tahun) {
|
||||
$query->whereMonth('created_at', $bulan)
|
||||
->whereYear('created_at', $tahun);
|
||||
} elseif ($bulan) {
|
||||
$query->whereMonth('created_at', $bulan);
|
||||
} elseif ($tahun) {
|
||||
$query->whereYear('created_at', $tahun);
|
||||
}
|
||||
|
||||
// Get attendance history
|
||||
$absensi = $query->get();
|
||||
|
||||
// Get all students
|
||||
$allStudents = Student::where('id_class', $classId)->get();
|
||||
|
||||
// Ambil id siswa yang sudah absen untuk mapel ini
|
||||
$sudahAbsenIds = $absensi->pluck('id_student')->unique();
|
||||
|
||||
// Siswa yang belum absen untuk mapel ini
|
||||
$belumAbsen = $allStudents->whereNotIn('id', $sudahAbsenIds)->values();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'absensi' => $absensi,
|
||||
'belumAbsen' => $belumAbsen,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getSchedules($classId, $subjectId)
|
||||
{
|
||||
try {
|
||||
// Get all schedules untuk subject ini di kelas ini
|
||||
$schedules = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $subjectId)
|
||||
->get()
|
||||
->map(function($schedule) {
|
||||
return [
|
||||
'id' => $schedule->id,
|
||||
'day' => $schedule->day_of_week ?? 'N/A',
|
||||
'time' => ($schedule->start_time ? \Carbon\Carbon::parse($schedule->start_time)->format('H:i') : '') . ' - ' .
|
||||
($schedule->end_time ? \Carbon\Carbon::parse($schedule->end_time)->format('H:i') : '')
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'schedules' => $schedules,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAbsensiByMapelAndDate($classId, $subjectId, $tanggal)
|
||||
{
|
||||
try {
|
||||
// Parse tanggal
|
||||
$date = \Carbon\Carbon::createFromFormat('Y-m-d', $tanggal);
|
||||
|
||||
// Map hari ke format Indonesia
|
||||
$dayMap = [
|
||||
0 => 'Minggu',
|
||||
1 => 'Senin',
|
||||
2 => 'Selasa',
|
||||
3 => 'Rabu',
|
||||
4 => 'Kamis',
|
||||
5 => 'Jumat',
|
||||
6 => 'Sabtu'
|
||||
];
|
||||
|
||||
$dayName = $dayMap[$date->dayOfWeek] ?? '';
|
||||
|
||||
// Get schedules untuk subject ini yang sesuai dengan hari ini
|
||||
$scheduleIds = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $subjectId)
|
||||
->where('day_of_week', $dayName)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Get attendance history untuk tanggal spesifik dan schedules tersebut
|
||||
$absensi = collect();
|
||||
if (!empty($scheduleIds)) {
|
||||
$absensi = AttendanceHistory::with(['student', 'class'])
|
||||
->whereIn('id_schedule', $scheduleIds)
|
||||
->where(function($q) use ($tanggal) {
|
||||
$q->whereDate('created_at', $tanggal)
|
||||
->orWhereDate('attendance_date', $tanggal);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
// Get all students
|
||||
$allStudents = Student::where('id_class', $classId)->get();
|
||||
|
||||
// Siswa yang sudah absen untuk tanggal dan schedule ini
|
||||
$sudahAbsenIds = $absensi->pluck('id_student')->unique();
|
||||
|
||||
// Siswa yang belum absen
|
||||
$belumAbsen = $allStudents->whereNotIn('id', $sudahAbsenIds)->values();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'absensi' => $absensi,
|
||||
'belumAbsen' => $belumAbsen,
|
||||
'scheduleIds' => $scheduleIds,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function editStatus(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
|
||||
// Jika $id adalah 'null', create baru
|
||||
if ($id === 'null') {
|
||||
$request->validate([
|
||||
'id_student' => 'required|integer',
|
||||
'id_class' => 'required|integer',
|
||||
'id_schedule' => 'required|integer',
|
||||
]);
|
||||
|
||||
// Create attendance record dengan id_schedule dari request
|
||||
$currentDate = now()->format('Y-m-d');
|
||||
AttendanceHistory::create([
|
||||
'id_student' => $request->id_student,
|
||||
'id_class' => $request->id_class,
|
||||
'status' => $request->status,
|
||||
'period_number' => 1,
|
||||
'id_schedule' => $request->id_schedule,
|
||||
'coordinate' => config('coordinate.coordinate', '-7.604032330848524, 112.10176449791652'),
|
||||
'attendance_date' => $currentDate,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Data absensi berhasil ditambahkan.']);
|
||||
} else {
|
||||
// Update existing
|
||||
$absensi = AttendanceHistory::findOrFail($id);
|
||||
$currentDate = now()->format('Y-m-d');
|
||||
$absensi->update([
|
||||
'status' => $request->status,
|
||||
'attendance_date' => $currentDate,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Status absensi berhasil diupdate.']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export absensi mapel ke Excel - Format Grid per Periode (seperti standar absensi)
|
||||
*/
|
||||
public function exportExcel($classId, Request $request)
|
||||
{
|
||||
try {
|
||||
// Get kelas info
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
$kelasName = $kelas->name ?? 'Kelas';
|
||||
|
||||
// Get filter info
|
||||
$mapelId = $request->get('mapel');
|
||||
$bulan = $request->get('bulan');
|
||||
$tahun = $request->get('tahun');
|
||||
|
||||
// Default ke bulan & tahun sekarang jika kedua-duanya kosong
|
||||
if (empty($bulan) && empty($tahun)) {
|
||||
$bulan = date('m');
|
||||
$tahun = date('Y');
|
||||
} elseif ($bulan && empty($tahun)) {
|
||||
$tahun = date('Y');
|
||||
}
|
||||
|
||||
// Jika bulan kosong tapi tahun ada, lakukan export tahunan
|
||||
if (empty($bulan) && $tahun) {
|
||||
return $this->exportExcelYearly($classId, $mapelId, $tahun);
|
||||
}
|
||||
|
||||
if (empty($bulan)) {
|
||||
$bulan = date('m');
|
||||
}
|
||||
|
||||
// Get mapel/subject name
|
||||
$mapelName = 'Mapel';
|
||||
if ($mapelId) {
|
||||
$subject = Subject::find($mapelId);
|
||||
$mapelName = $subject ? $subject->name : 'Mapel';
|
||||
}
|
||||
|
||||
// Get all students
|
||||
$students = Student::where('id_class', $classId)->orderBy('name', 'ASC')->get();
|
||||
if ($students->isEmpty()) {
|
||||
return response()->json(['error' => 'Tidak ada siswa di kelas'], 400);
|
||||
}
|
||||
|
||||
// Get schedules untuk mapel ini
|
||||
$scheduleIds = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $mapelId)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Generate filename
|
||||
$month_name = $this->getMonthName($bulan);
|
||||
$filename = 'Absensi_Mapel_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $mapelName) . '_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . '.xlsx';
|
||||
|
||||
// Create spreadsheet
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Absensi Mapel');
|
||||
|
||||
// Row 1: Title (Yellow background)
|
||||
$title = 'ABSENSI ' . strtoupper($mapelName) . ' ' . strtoupper($kelasName);
|
||||
$sheet->setCellValue('A1', $title);
|
||||
$sheet->mergeCells('A1:AE1');
|
||||
$sheet->getRowDimension(1)->setRowHeight(25);
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 14, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Row 2: Period info
|
||||
$periodText = 'PERIODE ' . strtoupper($month_name) . ' ' . $tahun;
|
||||
$sheet->setCellValue('A2', $periodText);
|
||||
$sheet->mergeCells('A2:AE2');
|
||||
$sheet->getRowDimension(2)->setRowHeight(20);
|
||||
$sheet->getStyle('A2')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 12, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Row 3: Empty
|
||||
$sheet->getRowDimension(3)->setRowHeight(10);
|
||||
|
||||
// Row 4: Headers
|
||||
$sheet->setCellValue('A4', 'Nama');
|
||||
$sheet->setCellValue('B4', 'Jabatan');
|
||||
|
||||
// Days columns (1-31)
|
||||
$daysInMonth = \Carbon\Carbon::createFromDate($tahun, $bulan, 1)->daysInMonth;
|
||||
for ($day = 1; $day <= 31; $day++) {
|
||||
$colLetter = $this->getColumnLetter($day + 2); // +2 untuk offset dari kolom A (Nama) dan B (Jabatan)
|
||||
if ($day <= $daysInMonth) {
|
||||
$sheet->setCellValue($colLetter . '4', $day);
|
||||
} else {
|
||||
$sheet->setCellValue($colLetter . '4', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Summary columns
|
||||
$jumlahCol = $this->getColumnLetter(35); // Column after day 31
|
||||
$keteranganCol = $this->getColumnLetter(36);
|
||||
$sheet->setCellValue($jumlahCol . '4', 'Jumlah\nHari Kerja');
|
||||
$sheet->setCellValue($keteranganCol . '4', 'Keterangan');
|
||||
|
||||
// Style headers (Row 4) - Yellow background
|
||||
$headerRange = 'A4:' . $keteranganCol . '4';
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 11, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
]);
|
||||
$sheet->getRowDimension(4)->setRowHeight(30);
|
||||
|
||||
// Get attendance data untuk bulan+tahun ini
|
||||
$attendanceData = [];
|
||||
if (!empty($scheduleIds)) {
|
||||
$attendance = AttendanceHistory::where(function($q) use ($bulan, $tahun) {
|
||||
$q->where(function($qq) use ($bulan, $tahun) {
|
||||
$qq->whereMonth('created_at', $bulan)->whereYear('created_at', $tahun);
|
||||
})->orWhere(function($qq) use ($bulan, $tahun) {
|
||||
$qq->whereMonth('attendance_date', $bulan)->whereYear('attendance_date', $tahun);
|
||||
});
|
||||
})->whereIn('id_schedule', $scheduleIds)->get();
|
||||
|
||||
foreach ($attendance as $att) {
|
||||
$date = $att->attendance_date ? \Carbon\Carbon::parse($att->attendance_date) : \Carbon\Carbon::parse($att->created_at);
|
||||
$key = $att->id_student . '_' . $date->day;
|
||||
$attendanceData[$key] = $att->status;
|
||||
}
|
||||
}
|
||||
|
||||
// Add student rows
|
||||
$dataRow = 5;
|
||||
foreach ($students as $student) {
|
||||
$sheet->setCellValue('A' . $dataRow, $student->name);
|
||||
$sheet->setCellValue('B' . $dataRow, $student->classroom ? $student->classroom->name : '-');
|
||||
|
||||
// Add attendance status for each day
|
||||
for ($day = 1; $day <= 31; $day++) {
|
||||
$colLetter = $this->getColumnLetter($day + 2);
|
||||
$key = $student->id . '_' . $day;
|
||||
$status = isset($attendanceData[$key]) ? $attendanceData[$key] : '';
|
||||
|
||||
// Convert status to letter code
|
||||
$statusCode = '';
|
||||
if ($status) {
|
||||
switch($status) {
|
||||
case 'hadir':
|
||||
$statusCode = 'H';
|
||||
break;
|
||||
case 'izin':
|
||||
$statusCode = 'I';
|
||||
break;
|
||||
case 'sakit':
|
||||
$statusCode = 'S';
|
||||
break;
|
||||
case 'alpha':
|
||||
$statusCode = 'A';
|
||||
break;
|
||||
default:
|
||||
$statusCode = '';
|
||||
}
|
||||
}
|
||||
|
||||
$sheet->setCellValue($colLetter . $dataRow, $statusCode);
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
// Borders untuk semua kolom
|
||||
$dataRange = 'A' . $dataRow . ':' . $keteranganCol . $dataRow;
|
||||
$sheet->getStyle($dataRange)->applyFromArray([
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
$dataRow++;
|
||||
}
|
||||
|
||||
// Add legend
|
||||
$legendRow = $dataRow + 2;
|
||||
$sheet->setCellValue('A' . $legendRow, 'Keterangan:');
|
||||
$sheet->getStyle('A' . $legendRow)->applyFromArray(['font' => ['bold' => true, 'size' => 11]]);
|
||||
|
||||
$legendRow++;
|
||||
$legends = [
|
||||
'H' => 'Hadir',
|
||||
'I' => 'Izin',
|
||||
'S' => 'Sakit',
|
||||
'A' => 'Alpha'
|
||||
];
|
||||
|
||||
foreach ($legends as $code => $meaning) {
|
||||
$sheet->setCellValue('A' . $legendRow, $code);
|
||||
$sheet->setCellValue('B' . $legendRow, $meaning);
|
||||
$sheet->getStyle('A' . $legendRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$legendRow++;
|
||||
}
|
||||
|
||||
// Set column widths
|
||||
$sheet->getColumnDimension('A')->setWidth(20);
|
||||
$sheet->getColumnDimension('B')->setWidth(15);
|
||||
|
||||
// Day columns
|
||||
for ($day = 1; $day <= 31; $day++) {
|
||||
$colLetter = $this->getColumnLetter($day + 2);
|
||||
$sheet->getColumnDimension($colLetter)->setWidth(5);
|
||||
}
|
||||
|
||||
$sheet->getColumnDimension($jumlahCol)->setWidth(12);
|
||||
$sheet->getColumnDimension($keteranganCol)->setWidth(15);
|
||||
|
||||
// Save and download
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$tempPath = storage_path('app/temp/' . $filename);
|
||||
@mkdir(dirname($tempPath), 0755, true);
|
||||
$writer->save($tempPath);
|
||||
|
||||
return response()->download($tempPath)->deleteFileAfterSend(true);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get absensi mapel rows for export sesuai dengan filter
|
||||
* Export semua siswa di kelas dengan status absensi mereka (jika ada)
|
||||
*/
|
||||
private function getAbsensiMapelRowsForExport($classId, $mapelId, $tanggal = null, $bulan = null, $tahun = null)
|
||||
{
|
||||
// Get kelas
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
|
||||
// Get semua siswa di kelas (PENTING: ambil SEMUA siswa)
|
||||
$students = Student::where('id_class', $classId)
|
||||
->orderBy('name', 'ASC')
|
||||
->get();
|
||||
|
||||
if ($students->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get schedules untuk mapel ini
|
||||
$scheduleIds = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $mapelId)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Build query untuk attendance history - ambil SEMUA attendance untuk mapel ini
|
||||
$query = AttendanceHistory::with(['student', 'class']);
|
||||
|
||||
// Filter by schedule if available
|
||||
if (!empty($scheduleIds)) {
|
||||
$query->whereIn('id_schedule', $scheduleIds);
|
||||
} else {
|
||||
// Jika tidak ada schedule, kembalikan semua siswa dengan keterangan "-"
|
||||
return $this->buildEmptyAttendanceRows($students);
|
||||
}
|
||||
|
||||
// Apply date filters - PENTING: include semua attendance data sesuai filter
|
||||
if ($tanggal) {
|
||||
// Untuk tanggal spesifik
|
||||
$query->where(function($q) use ($tanggal) {
|
||||
$q->whereDate('created_at', $tanggal)
|
||||
->orWhereDate('attendance_date', $tanggal);
|
||||
});
|
||||
} elseif ($bulan && $tahun) {
|
||||
// Untuk bulan+tahun spesifik
|
||||
$query->where(function($q) use ($bulan, $tahun) {
|
||||
$q->where(function($qq) use ($bulan, $tahun) {
|
||||
$qq->whereMonth('created_at', $bulan)
|
||||
->whereYear('created_at', $tahun);
|
||||
})->orWhere(function($qq) use ($bulan, $tahun) {
|
||||
$qq->whereMonth('attendance_date', $bulan)
|
||||
->whereYear('attendance_date', $tahun);
|
||||
});
|
||||
});
|
||||
} elseif ($bulan) {
|
||||
// Untuk bulan spesifik
|
||||
$query->where(function($q) use ($bulan) {
|
||||
$q->whereMonth('created_at', $bulan)
|
||||
->orWhereMonth('attendance_date', $bulan);
|
||||
});
|
||||
} elseif ($tahun) {
|
||||
// Untuk tahun spesifik
|
||||
$query->where(function($q) use ($tahun) {
|
||||
$q->whereYear('created_at', $tahun)
|
||||
->orWhereYear('attendance_date', $tahun);
|
||||
});
|
||||
}
|
||||
|
||||
// Get attendance records
|
||||
$absensi = $query->get();
|
||||
|
||||
// Build rows untuk export - PENTING: include SEMUA students
|
||||
$rows = [];
|
||||
$no = 1;
|
||||
|
||||
foreach ($students as $student) {
|
||||
// Cari attendance record untuk student ini
|
||||
// Jika ada multiple records, ambil yang paling recent
|
||||
$absensiItem = $absensi->where('id_student', $student->id)->sortByDesc('created_at')->first();
|
||||
|
||||
$keterangan = '-';
|
||||
$attendanceDate = '-';
|
||||
$jamPertemuan = '-';
|
||||
|
||||
if ($absensiItem) {
|
||||
// Tentukan keterangan berdasarkan status
|
||||
switch($absensiItem->status) {
|
||||
case 'hadir':
|
||||
$keterangan = 'Hadir';
|
||||
break;
|
||||
case 'izin':
|
||||
$keterangan = 'Izin';
|
||||
break;
|
||||
case 'sakit':
|
||||
$keterangan = 'Sakit';
|
||||
break;
|
||||
case 'alpha':
|
||||
$keterangan = 'Alpha';
|
||||
break;
|
||||
case 'dispen':
|
||||
$keterangan = 'Dispen';
|
||||
break;
|
||||
default:
|
||||
$keterangan = ucfirst($absensiItem->status);
|
||||
}
|
||||
|
||||
// Format tanggal dan jam dari attendance record
|
||||
$attendanceDate = $absensiItem->attendance_date
|
||||
? \Carbon\Carbon::parse($absensiItem->attendance_date)->format('d/m/Y')
|
||||
: ($absensiItem->created_at ? $absensiItem->created_at->format('d/m/Y') : '-');
|
||||
|
||||
$jamPertemuan = $absensiItem->created_at ? $absensiItem->created_at->format('H:i') : '-';
|
||||
}
|
||||
|
||||
// Add row untuk student ini (terlepas ada attendance atau tidak)
|
||||
$rows[] = [
|
||||
'no' => $no,
|
||||
'student_name' => $student->name,
|
||||
'nisn' => $student->nisn ?? '-',
|
||||
'attendance_date' => $attendanceDate,
|
||||
'jam_pertemuan' => $jamPertemuan,
|
||||
'keterangan' => $keterangan,
|
||||
];
|
||||
|
||||
$no++;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Build empty attendance rows jika tidak ada schedule
|
||||
*/
|
||||
private function buildEmptyAttendanceRows($students)
|
||||
{
|
||||
$rows = [];
|
||||
$no = 1;
|
||||
|
||||
foreach ($students as $student) {
|
||||
$rows[] = [
|
||||
'no' => $no,
|
||||
'student_name' => $student->name,
|
||||
'nisn' => $student->nisn ?? '-',
|
||||
'attendance_date' => '-',
|
||||
'jam_pertemuan' => '-',
|
||||
'keterangan' => '-',
|
||||
];
|
||||
$no++;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Convert column number to letter
|
||||
*/
|
||||
private function getColumnLetter($col)
|
||||
{
|
||||
$letter = '';
|
||||
while ($col > 0) {
|
||||
$col--;
|
||||
$letter = chr(65 + ($col % 26)) . $letter;
|
||||
$col = intdiv($col, 26);
|
||||
}
|
||||
return $letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get month name in Indonesian
|
||||
*/
|
||||
private function getMonthName($bulan)
|
||||
{
|
||||
$months = [
|
||||
'01' => 'Januari', '02' => 'Februari', '03' => 'Maret',
|
||||
'04' => 'April', '05' => 'Mei', '06' => 'Juni',
|
||||
'07' => 'Juli', '08' => 'Agustus', '09' => 'September',
|
||||
'10' => 'Oktober', '11' => 'November', '12' => 'Desember'
|
||||
];
|
||||
return $months[$bulan] ?? 'Bulan';
|
||||
}
|
||||
|
||||
/**
|
||||
* Export absensi mapel ke Excel - Format Tahunan (12 Bulan)
|
||||
*/
|
||||
private function exportExcelYearly($classId, $mapelId, $tahun)
|
||||
{
|
||||
// Get kelas info
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
$kelasName = $kelas->name ?? 'Kelas';
|
||||
|
||||
// Get mapel/subject name
|
||||
$mapelName = 'Mapel';
|
||||
if ($mapelId) {
|
||||
$subject = Subject::find($mapelId);
|
||||
$mapelName = $subject ? $subject->name : 'Mapel';
|
||||
}
|
||||
|
||||
// Get all students
|
||||
$students = Student::where('id_class', $classId)->orderBy('name', 'ASC')->get();
|
||||
if ($students->isEmpty()) {
|
||||
return response()->json(['error' => 'Tidak ada siswa di kelas'], 400);
|
||||
}
|
||||
|
||||
// Get schedules untuk mapel ini
|
||||
$scheduleIds = Schedule::where('id_class', $classId)
|
||||
->where('id_subject', $mapelId)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
// Generate filename
|
||||
$filename = 'Laporan_Tahunan_Absensi_Mapel_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $mapelName) . '_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . '_' . $tahun . '.xlsx';
|
||||
|
||||
// Create spreadsheet
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Laporan Tahunan');
|
||||
|
||||
// Row 1: Title (Yellow background)
|
||||
$title = 'LAPORAN TAHUNAN ABSENSI ' . strtoupper($mapelName) . ' ' . strtoupper($kelasName);
|
||||
$sheet->setCellValue('A1', $title);
|
||||
$sheet->mergeCells('A1:R1');
|
||||
$sheet->getRowDimension(1)->setRowHeight(25);
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 14, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Row 2: Period info
|
||||
$periodText = 'TAHUN ' . $tahun;
|
||||
$sheet->setCellValue('A2', $periodText);
|
||||
$sheet->mergeCells('A2:R2');
|
||||
$sheet->getRowDimension(2)->setRowHeight(20);
|
||||
$sheet->getStyle('A2')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 12, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Row 3: Empty
|
||||
$sheet->getRowDimension(3)->setRowHeight(10);
|
||||
|
||||
// Row 4: Headers
|
||||
$sheet->setCellValue('A4', 'Nama');
|
||||
$sheet->setCellValue('B4', 'Jabatan/Kelas');
|
||||
|
||||
// Month Names
|
||||
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agt', 'Sep', 'Okt', 'Nov', 'Des'];
|
||||
foreach ($months as $idx => $monthName) {
|
||||
$colLetter = $this->getColumnLetter($idx + 3); // C = 3
|
||||
$sheet->setCellValue($colLetter . '4', $monthName);
|
||||
}
|
||||
|
||||
// Totals Headers
|
||||
$sheet->setCellValue('O4', "Total\nHadir");
|
||||
$sheet->setCellValue('P4', "Total\nIzin");
|
||||
$sheet->setCellValue('Q4', "Total\nSakit");
|
||||
$sheet->setCellValue('R4', "Total\nAlpha");
|
||||
|
||||
// Style headers (Row 4) - Yellow background
|
||||
$headerRange = 'A4:R4';
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 11, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
]);
|
||||
$sheet->getRowDimension(4)->setRowHeight(30);
|
||||
|
||||
// Fetch attendance data for the whole year
|
||||
$attendanceData = [];
|
||||
if (!empty($scheduleIds)) {
|
||||
$attendance = AttendanceHistory::whereIn('id_schedule', $scheduleIds)
|
||||
->where(function($q) use ($tahun) {
|
||||
$q->whereYear('created_at', $tahun)
|
||||
->orWhereYear('attendance_date', $tahun);
|
||||
})->get();
|
||||
|
||||
foreach ($attendance as $att) {
|
||||
$date = $att->attendance_date ? \Carbon\Carbon::parse($att->attendance_date) : \Carbon\Carbon::parse($att->created_at);
|
||||
$month = $date->month;
|
||||
$studentId = $att->id_student;
|
||||
$status = $att->status;
|
||||
|
||||
if (!isset($attendanceData[$studentId])) {
|
||||
$attendanceData[$studentId] = [];
|
||||
}
|
||||
if (!isset($attendanceData[$studentId][$month])) {
|
||||
$attendanceData[$studentId][$month] = [
|
||||
'hadir' => 0,
|
||||
'izin' => 0,
|
||||
'sakit' => 0,
|
||||
'alpha' => 0,
|
||||
'dispen' => 0
|
||||
];
|
||||
}
|
||||
if (in_array($status, ['hadir', 'izin', 'sakit', 'alpha', 'dispen'])) {
|
||||
$attendanceData[$studentId][$month][$status]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add student rows
|
||||
$dataRow = 5;
|
||||
foreach ($students as $student) {
|
||||
$sheet->setCellValue('A' . $dataRow, $student->name);
|
||||
$sheet->setCellValue('B' . $dataRow, $kelasName);
|
||||
|
||||
$totalHadir = 0;
|
||||
$totalIzin = 0;
|
||||
$totalSakit = 0;
|
||||
$totalAlpha = 0;
|
||||
|
||||
// Populate months (C to N)
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$colLetter = $this->getColumnLetter($month + 2); // C = 3
|
||||
|
||||
$h = 0; $i = 0; $s = 0; $a = 0;
|
||||
if (isset($attendanceData[$student->id][$month])) {
|
||||
$h = $attendanceData[$student->id][$month]['hadir'] ?? 0;
|
||||
$i = $attendanceData[$student->id][$month]['izin'] ?? 0;
|
||||
$s = $attendanceData[$student->id][$month]['sakit'] ?? 0;
|
||||
$a = (($attendanceData[$student->id][$month]['alpha'] ?? 0) + ($attendanceData[$student->id][$month]['dispen'] ?? 0));
|
||||
}
|
||||
|
||||
$totalHadir += $h;
|
||||
$totalIzin += $i;
|
||||
$totalSakit += $s;
|
||||
$totalAlpha += $a;
|
||||
|
||||
if ($h > 0 || $i > 0 || $s > 0 || $a > 0) {
|
||||
$displayText = "H:{$h}\nI:{$i}\nS:{$s}\nA:{$a}";
|
||||
$sheet->setCellValue($colLetter . $dataRow, $displayText);
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setWrapText(true);
|
||||
} else {
|
||||
$sheet->setCellValue($colLetter . $dataRow, '-');
|
||||
}
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
// Populate totals
|
||||
$sheet->setCellValue('O' . $dataRow, $totalHadir);
|
||||
$sheet->setCellValue('P' . $dataRow, $totalIzin);
|
||||
$sheet->setCellValue('Q' . $dataRow, $totalSakit);
|
||||
$sheet->setCellValue('R' . $dataRow, $totalAlpha);
|
||||
|
||||
for ($c = 15; $c <= 18; $c++) {
|
||||
$colLetter = $this->getColumnLetter($c);
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
// Border styling
|
||||
$dataRange = 'A' . $dataRow . ':R' . $dataRow;
|
||||
$sheet->getStyle($dataRange)->applyFromArray([
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Set height for dynamic wrapped text
|
||||
$sheet->getRowDimension($dataRow)->setRowHeight(55);
|
||||
|
||||
$dataRow++;
|
||||
}
|
||||
|
||||
// Add legend
|
||||
$legendRow = $dataRow + 2;
|
||||
$sheet->setCellValue('A' . $legendRow, 'Keterangan format bulanan:');
|
||||
$sheet->getStyle('A' . $legendRow)->applyFromArray(['font' => ['bold' => true, 'size' => 11]]);
|
||||
|
||||
$legendRow++;
|
||||
$legends = [
|
||||
'H' => 'Hadir',
|
||||
'I' => 'Izin',
|
||||
'S' => 'Sakit',
|
||||
'A' => 'Alpha / Dispen'
|
||||
];
|
||||
|
||||
foreach ($legends as $code => $meaning) {
|
||||
$sheet->setCellValue('A' . $legendRow, $code);
|
||||
$sheet->setCellValue('B' . $legendRow, $meaning);
|
||||
$sheet->getStyle('A' . $legendRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$legendRow++;
|
||||
}
|
||||
|
||||
// Set column widths
|
||||
$sheet->getColumnDimension('A')->setWidth(25);
|
||||
$sheet->getColumnDimension('B')->setWidth(15);
|
||||
|
||||
// Month columns width
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$colLetter = $this->getColumnLetter($month + 2);
|
||||
$sheet->getColumnDimension($colLetter)->setWidth(10);
|
||||
}
|
||||
|
||||
$sheet->getColumnDimension('O')->setWidth(12);
|
||||
$sheet->getColumnDimension('P')->setWidth(12);
|
||||
$sheet->getColumnDimension('Q')->setWidth(12);
|
||||
$sheet->getColumnDimension('R')->setWidth(12);
|
||||
|
||||
// Save and download
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$tempPath = storage_path('app/temp/' . $filename);
|
||||
@mkdir(dirname($tempPath), 0755, true);
|
||||
$writer->save($tempPath);
|
||||
|
||||
return response()->download($tempPath)->deleteFileAfterSend(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,676 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Student;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Teacher;
|
||||
use App\Models\Subject;
|
||||
use App\Mail\SendResetPasswordLink;
|
||||
use Faker\Provider\Image as ProviderImage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use Intervention\Image\Image as InterventionImage;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
if (Auth::attempt($credentials)) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->route('dashboard.index');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'Email atau password salah.',
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
public function showForgotPassword()
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
public function sendResetLink(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
// Cek apakah email terdaftar
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (!$user) {
|
||||
return back()->withErrors(['email' => 'Email tidak ditemukan.'])->withInput($request->only('email'));
|
||||
}
|
||||
|
||||
// Generate token random
|
||||
$token = \Illuminate\Support\Str::random(60);
|
||||
|
||||
// Simpan token ke database dengan hashing
|
||||
DB::table('password_reset_tokens')->updateOrInsert(
|
||||
['email' => $request->email],
|
||||
[
|
||||
'token' => Hash::make($token),
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// Kirim email dengan link reset password dan token
|
||||
try {
|
||||
Mail::to($request->email)->send(new SendResetPasswordLink($request->email, $token));
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['email' => 'Gagal mengirim email. Silakan coba lagi.'])->withInput($request->only('email'));
|
||||
}
|
||||
|
||||
return redirect()->route('password.verify.form', [
|
||||
'email' => $request->email,
|
||||
])->with('status', 'Email reset password telah dikirim. Silakan cek email Anda untuk token. Token berlaku selama 60 menit.');
|
||||
}
|
||||
|
||||
public function showVerifyToken(Request $request)
|
||||
{
|
||||
if (!$request->filled('email')) {
|
||||
return redirect()->route('password.request');
|
||||
}
|
||||
|
||||
// Cek apakah ada token untuk email ini yang masih berlaku
|
||||
$resetToken = DB::table('password_reset_tokens')
|
||||
->where('email', $request->email)
|
||||
->where('created_at', '>=', now()->subMinutes(60))
|
||||
->first();
|
||||
|
||||
if (!$resetToken) {
|
||||
return redirect()->route('password.request')
|
||||
->withErrors(['email' => 'Token tidak ditemukan atau sudah kadaluarsa.']);
|
||||
}
|
||||
|
||||
$email = $request->email;
|
||||
return view('auth.verify-token', compact('email'));
|
||||
}
|
||||
|
||||
public function verifyToken(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'token' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
// Cek token di database
|
||||
$resetToken = DB::table('password_reset_tokens')
|
||||
->where('email', $request->email)
|
||||
->where('created_at', '>=', now()->subMinutes(60))
|
||||
->first();
|
||||
|
||||
if (!$resetToken) {
|
||||
return back()->withErrors(['email' => 'Token tidak ditemukan atau sudah kadaluarsa.']);
|
||||
}
|
||||
|
||||
// Verifikasi token
|
||||
if (!Hash::check($request->token, $resetToken->token)) {
|
||||
return back()->withErrors(['token' => 'Token tidak sesuai.'])->withInput();
|
||||
}
|
||||
|
||||
// Token valid, arahkan ke form confirm password
|
||||
return redirect()->route('password.confirm.form', [
|
||||
'email' => $request->email,
|
||||
])->with('status', 'Token terverifikasi. Silakan masukkan password baru.');
|
||||
}
|
||||
|
||||
public function showConfirmPassword(Request $request)
|
||||
{
|
||||
if (!$request->filled('email')) {
|
||||
return redirect()->route('password.request')
|
||||
->withErrors(['email' => 'Link konfirmasi password tidak valid.']);
|
||||
}
|
||||
|
||||
$email = $request->email;
|
||||
return view('auth.confirm-password', compact('email'));
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', 'min:8'],
|
||||
], [
|
||||
'password.confirmed' => 'Konfirmasi password tidak sesuai.',
|
||||
'password.min' => 'Password minimal 8 karakter.',
|
||||
]);
|
||||
|
||||
// Cek user
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
if (!$user) {
|
||||
return back()->withErrors(['email' => 'Email tidak ditemukan.']);
|
||||
}
|
||||
|
||||
// Jika user adalah admin (forgot password langsung ke confirm password)
|
||||
$isAdmin = $user->role === 'admin';
|
||||
|
||||
// Jika user bukan admin, cek token (dari verify token flow)
|
||||
if (!$isAdmin) {
|
||||
$resetToken = DB::table('password_reset_tokens')
|
||||
->where('email', $request->email)
|
||||
->where('created_at', '>=', now()->subMinutes(60))
|
||||
->first();
|
||||
|
||||
if (!$resetToken) {
|
||||
return back()->withErrors(['email' => 'Session reset password telah kadaluarsa. Silakan lakukan forgot password lagi.']);
|
||||
}
|
||||
}
|
||||
|
||||
// Update password
|
||||
$user->password = Hash::make($request->password);
|
||||
$user->save();
|
||||
|
||||
// Hapus token dari database jika ada
|
||||
DB::table('password_reset_tokens')
|
||||
->where('email', $request->email)
|
||||
->delete();
|
||||
|
||||
$message = $isAdmin ? 'Password admin berhasil diperbarui. Silakan login.' : 'Password berhasil direset. Silakan login dengan password baru Anda.';
|
||||
return redirect()->route('login')->with('success', $message);
|
||||
}
|
||||
|
||||
public function showSetting()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return view('pages.setting.setting', compact('user'));
|
||||
}
|
||||
|
||||
public function updateSetting(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
if ($request->input('form_type') === 'profile') {
|
||||
return $this->updateSettingProfile($request, $user);
|
||||
}
|
||||
|
||||
if ($request->input('form_type') === 'password') {
|
||||
return $this->updateSettingPassword($request, $user);
|
||||
}
|
||||
|
||||
return back()->withErrors(['form' => 'Form tidak valid.']);
|
||||
}
|
||||
|
||||
public function deleteProfilePicture()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
if (empty($user->profile_picture)) {
|
||||
return back()->with('success', 'Foto profil sudah kosong.');
|
||||
}
|
||||
|
||||
$rolePath = $user->role ?? 'others';
|
||||
if (!empty($user->profile_picture) && Storage::disk('public')->exists('profile_pictures/' . $rolePath . '/' . $user->profile_picture)) {
|
||||
Storage::disk('public')->delete('profile_pictures/' . $rolePath . '/' . $user->profile_picture);
|
||||
}
|
||||
|
||||
$user->profile_picture = null;
|
||||
$user->save();
|
||||
|
||||
return back()->with('success', 'Foto profil berhasil dihapus.');
|
||||
}
|
||||
|
||||
private function updateSettingProfile(Request $request, User $user)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email|max:255|unique:users,email,' . $user->id,
|
||||
'profile_picture' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:2048',
|
||||
]);
|
||||
|
||||
$user->email = $request->email;
|
||||
|
||||
if ($request->hasFile('profile_picture')) {
|
||||
$rolePath = $user->role ?? 'others';
|
||||
if (!empty($user->profile_picture) && Storage::disk('public')->exists('profile_pictures/' . $rolePath . '/' . $user->profile_picture)) {
|
||||
Storage::disk('public')->delete('profile_pictures/' . $rolePath . '/' . $user->profile_picture);
|
||||
}
|
||||
|
||||
$image = $request->file('profile_picture');
|
||||
$imageName = $image->hashName();
|
||||
Storage::disk('public')->putFileAs('profile_pictures/' . $rolePath, $image, $imageName);
|
||||
$user->profile_picture = $imageName;
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
return back()->with('success', 'Email dan gambar profil berhasil diperbarui.');
|
||||
}
|
||||
|
||||
private function updateSettingPassword(Request $request, User $user)
|
||||
{
|
||||
$request->validate([
|
||||
'current_password' => 'required|string',
|
||||
'new_password' => 'required|string|min:8|confirmed',
|
||||
], [
|
||||
'new_password.confirmed' => 'Konfirmasi password baru tidak cocok.',
|
||||
]);
|
||||
|
||||
if (!Hash::check($request->current_password, $user->password)) {
|
||||
return back()->withErrors([
|
||||
'current_password' => 'Password saat ini tidak sesuai.',
|
||||
]);
|
||||
}
|
||||
|
||||
$user->password = Hash::make($request->new_password);
|
||||
$user->save();
|
||||
|
||||
return back()->with('success', 'Password berhasil diperbarui.');
|
||||
}
|
||||
|
||||
// start register student
|
||||
public function registerStudent(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6',
|
||||
'nisn' => 'required|digits:10|unique:students,nisn',
|
||||
'id_class' => 'required|integer',
|
||||
'entry_year' => 'required|integer',
|
||||
'profile_picture' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
'pictures' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Proses upload gambar jika ada
|
||||
$imageName = null;
|
||||
if ($request->hasFile('profile_picture')) {
|
||||
$image = $request->file('profile_picture');
|
||||
$imageName = $image->hashName();
|
||||
Storage::disk('public')->putFileAs('profile_pictures/student', $image, $imageName);
|
||||
}
|
||||
|
||||
// Proses 3 foto webcam
|
||||
$picturesNames = [];
|
||||
if ($request->filled('pictures')) {
|
||||
$photosData = $request->pictures;
|
||||
$photoArray = explode('|||', $photosData); // Split dengan |||
|
||||
|
||||
|
||||
foreach ($photoArray as $index => $photo) {
|
||||
if (!empty($photo)) {
|
||||
$imageData = $photo;
|
||||
|
||||
if (strpos($imageData, ',') !== false) {
|
||||
list($type, $imageData) = explode(',', $imageData);
|
||||
$imageData = base64_decode($imageData);
|
||||
} else {
|
||||
$imageData = base64_decode($imageData);
|
||||
}
|
||||
|
||||
if ($imageData !== false) {
|
||||
$picturesName = 'webcam_' . time() . '_' . ($index + 1) . '_' . uniqid() . '.png';
|
||||
Storage::disk('public')->put('photo-webcam/' . $picturesName, $imageData);
|
||||
$picturesNames[] = $picturesName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'student',
|
||||
'profile_picture' => $imageName,
|
||||
]);
|
||||
|
||||
$user->student()->create([
|
||||
'id_user' => $user->id,
|
||||
'name' => $request->name,
|
||||
'nisn' => $request->nisn,
|
||||
'id_class' => $request->id_class,
|
||||
'entry_year' => $request->entry_year,
|
||||
'pictures' => !empty($picturesNames) ? implode(',', $picturesNames) : null,
|
||||
]);
|
||||
|
||||
// Redirect kembali ke identitas siswa dengan pesan sukses
|
||||
return redirect()->route('akun.indentitas_siswa')->with('success', 'Data siswa berhasil ditambahkan.');
|
||||
} catch (ValidationException $e) {
|
||||
return redirect()->back()
|
||||
->withErrors($e->errors())
|
||||
->withInput()
|
||||
->with('open_modal', 'tambah');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->with('error', 'Registrasi siswa gagal. Silakan coba lagi.')
|
||||
->withInput()
|
||||
->with('open_modal', 'tambah');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function identitasSiswa()
|
||||
{
|
||||
$students = Student::with(['user', 'class'])->get();
|
||||
$classes = Classes::all();
|
||||
return view('pages.akun.indentitas_siswa', compact('students', 'classes'));
|
||||
}
|
||||
|
||||
public function destroyStudent($id)
|
||||
{
|
||||
$student = Student::findOrFail($id);
|
||||
|
||||
// Hapus semua foto webcam jika ada
|
||||
if ($student->pictures) {
|
||||
$photos = explode(',', $student->pictures);
|
||||
foreach ($photos as $photo) {
|
||||
if (!empty($photo)) {
|
||||
if (Storage::disk('public')->exists('photo-webcam/' . $photo)) {
|
||||
Storage::disk('public')->delete('photo-webcam/' . $photo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus user terkait (akan otomatis hapus student jika foreign key cascade)
|
||||
if ($student->user) {
|
||||
$student->user->delete();
|
||||
} else {
|
||||
$student->delete();
|
||||
}
|
||||
return redirect()->route('akun.indentitas_siswa')->with('success', 'Data siswa berhasil dihapus.');
|
||||
}
|
||||
|
||||
public function updateStudent(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
// Jika massal (hanya update kelas)
|
||||
if ($request->has('id_class') && !$request->has('name')) {
|
||||
$student = Student::findOrFail($id);
|
||||
$student->id_class = $request->id_class;
|
||||
$student->save();
|
||||
return redirect()->route('akun.indentitas_siswa')->with('success', 'Kelas siswa berhasil diupdate.');
|
||||
}
|
||||
|
||||
// Update lengkap (dari modal edit)
|
||||
$student = Student::findOrFail($id);
|
||||
$user = $student->user;
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users,email,' . $user->id,
|
||||
'nisn' => 'required|digits:10|unique:students,nisn,' . $student->id,
|
||||
'id_class' => 'required|integer',
|
||||
'entry_year' => 'required|integer',
|
||||
'profile_picture' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
'pictures_edit' => 'nullable|string',
|
||||
'delete_webcam_photo' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$user->email = $request->email;
|
||||
if ($request->hasFile('profile_picture')) {
|
||||
// Hapus foto lama jika ada
|
||||
if ($user->profile_picture && Storage::disk('public')->exists('profile_pictures/student/' . $user->profile_picture)) {
|
||||
Storage::disk('public')->delete('profile_pictures/student/' . $user->profile_picture);
|
||||
}
|
||||
|
||||
$image = $request->file('profile_picture');
|
||||
$imageName = $image->hashName();
|
||||
Storage::disk('public')->putFileAs('profile_pictures/student', $image, $imageName);
|
||||
$user->profile_picture = $imageName;
|
||||
}
|
||||
$user->save();
|
||||
|
||||
$student->name = $request->name;
|
||||
$student->nisn = $request->nisn;
|
||||
$student->id_class = $request->id_class;
|
||||
$student->entry_year = $request->entry_year;
|
||||
|
||||
// Handle hapus foto webcam lama
|
||||
if ($request->filled('delete_webcam_photo') && $request->delete_webcam_photo == '1') {
|
||||
if ($student->pictures) {
|
||||
$photos = explode(',', $student->pictures);
|
||||
foreach ($photos as $photo) {
|
||||
if (!empty($photo) && Storage::disk('public')->exists('photo-webcam/' . $photo)) {
|
||||
Storage::disk('public')->delete('photo-webcam/' . $photo);
|
||||
}
|
||||
}
|
||||
}
|
||||
$student->pictures = null;
|
||||
}
|
||||
|
||||
// Handle tambah foto webcam baru dari edit (3 foto)
|
||||
if ($request->filled('pictures_edit')) {
|
||||
$photosData = $request->pictures_edit;
|
||||
$photoArray = explode('|||', $photosData); // Split dengan |||
|
||||
|
||||
// Hapus foto lama dulu sebelum menyimpan yang baru
|
||||
if ($student->pictures) {
|
||||
$oldPhotos = explode(',', $student->pictures);
|
||||
foreach ($oldPhotos as $photo) {
|
||||
if (!empty($photo) && Storage::disk('public')->exists('photo-webcam/' . $photo)) {
|
||||
Storage::disk('public')->delete('photo-webcam/' . $photo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$picturesNames = [];
|
||||
foreach ($photoArray as $index => $photo) {
|
||||
if (!empty($photo)) {
|
||||
$imageData = $photo;
|
||||
|
||||
if (strpos($imageData, ',') !== false) {
|
||||
list($type, $imageData) = explode(',', $imageData);
|
||||
$imageData = base64_decode($imageData);
|
||||
} else {
|
||||
$imageData = base64_decode($imageData);
|
||||
}
|
||||
|
||||
if ($imageData !== false) {
|
||||
$picturesName = 'webcam_' . time() . '_' . ($index + 1) . '_' . uniqid() . '.png';
|
||||
Storage::disk('public')->put('photo-webcam/' . $picturesName, $imageData);
|
||||
$picturesNames[] = $picturesName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($picturesNames)) {
|
||||
$student->pictures = implode(',', $picturesNames);
|
||||
}
|
||||
}
|
||||
|
||||
$student->save();
|
||||
|
||||
return redirect()->route('akun.indentitas_siswa')->with('success', 'Data siswa berhasil diupdate.');
|
||||
} catch (ValidationException $e) {
|
||||
return redirect()->back()
|
||||
->withErrors($e->errors())
|
||||
->withInput()
|
||||
->with('open_modal', 'edit')
|
||||
->with('edit_id', $id);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->with('error', 'Update siswa gagal. Silakan coba lagi.')
|
||||
->withInput()
|
||||
->with('open_modal', 'edit')
|
||||
->with('edit_id', $id);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateStudentClass(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'id_class' => 'required|integer',
|
||||
]);
|
||||
|
||||
$student = Student::findOrFail($id);
|
||||
$student->id_class = $request->id_class;
|
||||
$student->save();
|
||||
|
||||
return redirect()->route('akun.indentitas_siswa')->with('success', 'Kelas siswa berhasil diupdate.');
|
||||
}
|
||||
|
||||
// end register student
|
||||
|
||||
|
||||
// start register teacher
|
||||
public function registerTeacher(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6',
|
||||
'nip' => 'required|digits:18|unique:teachers,nip',
|
||||
'subjects' => 'required|array|min:1',
|
||||
'subjects.*' => 'integer|exists:subjects,id',
|
||||
'profile_picture' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
]);
|
||||
|
||||
$imageName = null;
|
||||
if ($request->hasFile('profile_picture')) {
|
||||
$image = $request->file('profile_picture');
|
||||
$imageName = $image->hashName();
|
||||
Storage::disk('public')->putFileAs('profile_pictures/teacher', $image, $imageName);
|
||||
}
|
||||
|
||||
// Ambil nama semua subject yang dipilih
|
||||
$subjectNames = Subject::whereIn('id', $request->subjects)->pluck('code')->toArray();
|
||||
$subjectString = implode(', ', $subjectNames);
|
||||
|
||||
$user = User::create([
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => 'teacher',
|
||||
'profile_picture' => $imageName,
|
||||
]);
|
||||
|
||||
$teacher = new Teacher();
|
||||
$teacher->id_user = $user->id;
|
||||
$teacher->name = $request->name;
|
||||
$teacher->nip = $request->nip;
|
||||
$teacher->subject = $subjectString;
|
||||
$teacher->save();
|
||||
|
||||
return redirect()->route('akun.indentitas_guru')->with('success', 'Data guru berhasil ditambahkan.');
|
||||
} catch (ValidationException $e) {
|
||||
return redirect()->back()
|
||||
->withErrors($e->errors())
|
||||
->withInput()
|
||||
->with('open_modal', 'tambah');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->with('error', 'Registrasi guru gagal: ' . $e->getMessage())
|
||||
->withInput()
|
||||
->with('open_modal', 'tambah');
|
||||
}
|
||||
}
|
||||
|
||||
public function identitasGuru()
|
||||
{
|
||||
$teachers = Teacher::with('user')->get(); // Hapus 'subjects'
|
||||
$subjects = Subject::all();
|
||||
|
||||
|
||||
|
||||
return view('pages.akun.indentitas_guru', compact('teachers', 'subjects'));
|
||||
}
|
||||
|
||||
// Update teacher
|
||||
|
||||
|
||||
public function updateTeacher(Request $request, $id)
|
||||
{
|
||||
try {
|
||||
$teacher = Teacher::findOrFail($id);
|
||||
$user = $teacher->user;
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users,email,' . $user->id,
|
||||
'nip' => 'required|string|max:50|unique:teachers,nip,' . $teacher->id,
|
||||
'subjects' => 'required|array|min:1',
|
||||
'subjects.*' => 'integer|exists:subjects,id',
|
||||
'profile_picture' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
|
||||
]);
|
||||
|
||||
$user->email = $request->email;
|
||||
if ($request->hasFile('profile_picture')) {
|
||||
if ($user->profile_picture && Storage::disk('public')->exists('profile_pictures/teacher/' . $user->profile_picture)) {
|
||||
Storage::disk('public')->delete('profile_pictures/teacher/' . $user->profile_picture);
|
||||
}
|
||||
|
||||
$image = $request->file('profile_picture');
|
||||
$imageName = $image->hashName();
|
||||
Storage::disk('public')->putFileAs('profile_pictures/teacher', $image, $imageName);
|
||||
$user->profile_picture = $imageName;
|
||||
}
|
||||
$user->save();
|
||||
|
||||
// Ambil nama semua subject yang dipilih
|
||||
$subjectNames = Subject::whereIn('id', $request->subjects)->pluck('code')->toArray();
|
||||
$subjectString = implode(', ', $subjectNames);
|
||||
|
||||
$teacher->name = $request->name;
|
||||
$teacher->nip = $request->nip;
|
||||
$teacher->subject = $subjectString;
|
||||
$teacher->save();
|
||||
|
||||
return redirect()->route('akun.indentitas_guru')->with('success', 'Data guru berhasil diupdate.');
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return redirect()->back()
|
||||
->withErrors($e->errors())
|
||||
->withInput()
|
||||
->with('open_modal', 'edit')
|
||||
->with('edit_id', $id);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->with('error', 'Update guru gagal. Silakan coba lagi.')
|
||||
->withInput()
|
||||
->with('open_modal', 'edit')
|
||||
->with('edit_id', $id);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyTeacher($id)
|
||||
{
|
||||
$teacher = Teacher::findOrFail($id);
|
||||
// Hapus user terkait (akan otomatis hapus teacher jika foreign key cascade)
|
||||
if ($teacher->user) {
|
||||
$teacher->user->delete();
|
||||
} else {
|
||||
$teacher->delete();
|
||||
}
|
||||
return redirect()->route('akun.indentitas_guru')->with('success', 'Data guru berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Classes;
|
||||
use App\Models\AttendanceHistoryDaily;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClassController extends Controller
|
||||
{
|
||||
public function harian()
|
||||
{
|
||||
$classes = Classes::all();
|
||||
return view('pages.kelas.kelas_absensi', compact('classes'));
|
||||
}
|
||||
|
||||
|
||||
public function mapel()
|
||||
{
|
||||
$classes = Classes::all();
|
||||
return view('pages.kelas.kelas_mapel', compact('classes'));
|
||||
}
|
||||
|
||||
public function kelas_laporkan()
|
||||
{
|
||||
$classes = Classes::all();
|
||||
return view('pages.kelas.kelas_laporkan', compact('classes'));
|
||||
}
|
||||
|
||||
// Proses tambah kelas
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'major' => 'required|string|max:255',
|
||||
'grade' => 'required|integer|in:10,11,12',
|
||||
'code' => 'required|string|max:50',
|
||||
], [
|
||||
'grade.required' => 'Tingkat kelas harus diisi.',
|
||||
'grade.integer' => 'Tingkat kelas harus berupa angka.',
|
||||
'grade.in' => 'Tingkat kelas hanya boleh 10, 11, atau 12.',
|
||||
]);
|
||||
|
||||
Classes::create([
|
||||
'name' => $request->name,
|
||||
'major' => $request->major,
|
||||
'grade' => $request->grade,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
return redirect()->route('kelas.absensi')->with('success', 'Kelas berhasil ditambahkan.');
|
||||
return redirect()->route('kelas.mapel')->with('success', 'Kelas berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'major' => 'required|string|max:255',
|
||||
'grade' => 'required|integer|in:10,11,12',
|
||||
'code' => 'required|string|max:50',
|
||||
], [
|
||||
'grade.required' => 'Tingkat kelas harus diisi.',
|
||||
'grade.integer' => 'Tingkat kelas harus berupa angka.',
|
||||
'grade.in' => 'Tingkat kelas hanya boleh 10, 11, atau 12.',
|
||||
]);
|
||||
|
||||
$kelas = Classes::findOrFail($id);
|
||||
$kelas->update([
|
||||
'name' => $request->name,
|
||||
'major' => $request->major,
|
||||
'grade' => $request->grade,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'Kelas berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$kelas = Classes::findOrFail($id);
|
||||
$kelas->delete();
|
||||
|
||||
return redirect()->route('kelas.absensi')->with('success', 'Kelas berhasil dihapus.');
|
||||
return redirect()->route('kelas.mapel')->with('success', 'Kelas berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Classes;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Student;
|
||||
use App\Models\ClassModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$serverTime = now()->format('H:i:s');
|
||||
$year = now()->year;
|
||||
|
||||
// Statistik siswa
|
||||
$totalSiswa = Student::count();
|
||||
|
||||
// Get class IDs by grade dari database
|
||||
$class10Ids = Classes::where('grade', 10)->pluck('id');
|
||||
$class11Ids = Classes::where('grade', 11)->pluck('id');
|
||||
$class12Ids = Classes::where('grade', 12)->pluck('id');
|
||||
|
||||
// Count students in each grade
|
||||
$siswaKelas10 = Student::whereIn('id_class', $class10Ids)->count();
|
||||
$siswaKelas11 = Student::whereIn('id_class', $class11Ids)->count();
|
||||
$siswaKelas12 = Student::whereIn('id_class', $class12Ids)->count();
|
||||
|
||||
// Ambil data absensi harian per hari untuk semua bulan dalam tahun berjalan
|
||||
$allMonthlyStats = [];
|
||||
for ($m = 1; $m <= 12; $m++) {
|
||||
$allMonthlyStats[$m] = DB::table('attendance_history_dailys')
|
||||
->select(DB::raw('DAY(created_at) as day'), DB::raw('COUNT(*) as total'))
|
||||
->whereYear('created_at', $year)
|
||||
->whereMonth('created_at', $m)
|
||||
->groupBy(DB::raw('DAY(created_at)'))
|
||||
->orderBy('day')
|
||||
->pluck('total', 'day')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$month = now()->month;
|
||||
$monthlyStats = $allMonthlyStats[$month];
|
||||
|
||||
return view('pages.dashboard.dashboard', compact(
|
||||
'serverTime',
|
||||
'totalSiswa',
|
||||
'siswaKelas10',
|
||||
'siswaKelas11',
|
||||
'siswaKelas12',
|
||||
'monthlyStats',
|
||||
'allMonthlyStats'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,658 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AttendanceHistoryDaily;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Student;
|
||||
use App\Traits\ExcelExportTrait;
|
||||
use Illuminate\Support\Collection;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
|
||||
class HistoryDailyController extends Controller
|
||||
{
|
||||
use ExcelExportTrait;
|
||||
public function absensiHarian($classId, Request $request)
|
||||
{
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
|
||||
// Ambil semua siswa di kelas
|
||||
$students = Student::where('id_class', $classId)->get();
|
||||
|
||||
// Determine filter parameters
|
||||
$tanggal = $request->get('tanggal');
|
||||
$bulan = $request->get('bulan');
|
||||
$tahun = $request->get('tahun');
|
||||
|
||||
// Default ke hari ini hanya jika tidak ada filter apapun
|
||||
if (!$tanggal && !$bulan && !$tahun) {
|
||||
$tanggal = now()->format('Y-m-d');
|
||||
}
|
||||
|
||||
// Build query untuk fetch attendance records
|
||||
$query = AttendanceHistoryDaily::with(['student', 'class'])
|
||||
->where('id_class', $classId);
|
||||
|
||||
// Jika tanggal diset, filter by exact date
|
||||
if ($tanggal && !$bulan && !$tahun) {
|
||||
$query->whereDate('created_at', $tanggal);
|
||||
}
|
||||
// Jika bulan dan tahun keduanya diset, filter by month AND year
|
||||
else if ($bulan && $tahun) {
|
||||
$query->whereMonth('created_at', $bulan)
|
||||
->whereYear('created_at', $tahun);
|
||||
}
|
||||
// Jika hanya bulan diset, filter by month
|
||||
else if ($bulan) {
|
||||
$query->whereMonth('created_at', $bulan);
|
||||
}
|
||||
// Jika hanya tahun diset, filter by year
|
||||
else if ($tahun) {
|
||||
$query->whereYear('created_at', $tahun);
|
||||
}
|
||||
|
||||
$absensiFromDB = $query->orderBy('created_at', 'desc')->get();
|
||||
|
||||
// Dapatkan semua hari UNIK dari records
|
||||
$uniqueDates = $absensiFromDB
|
||||
->map(function($item) { return $item->created_at->format('Y-m-d'); })
|
||||
->unique()
|
||||
->values()
|
||||
->sort()
|
||||
->reverse() // Terbaru dulu
|
||||
->values();
|
||||
|
||||
// Jika filter tanggal spesifik dan tidak ada data, tambahkan tanggal tersebut
|
||||
if ($tanggal && !$bulan && !$tahun && !$uniqueDates->contains($tanggal)) {
|
||||
$uniqueDates = collect([$tanggal])->merge($uniqueDates);
|
||||
}
|
||||
|
||||
// Build result: untuk SETIAP HARI, tampilkan SEMUA SISWA
|
||||
$result = [];
|
||||
foreach ($uniqueDates as $date) {
|
||||
foreach ($students as $student) {
|
||||
// Cari record di database untuk student + date ini
|
||||
$record = $absensiFromDB
|
||||
->where('id_student', $student->id)
|
||||
->filter(function($item) use ($date) {
|
||||
return $item->created_at->format('Y-m-d') === $date;
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($record) {
|
||||
// Ada record di database
|
||||
$result[] = (object)[
|
||||
'id' => $record->id,
|
||||
'student' => $record->student,
|
||||
'class' => $record->class,
|
||||
'status' => $record->status,
|
||||
'created_at' => $record->created_at,
|
||||
'picture' => $record->picture,
|
||||
];
|
||||
} else {
|
||||
// Dummy record: siswa belum absen di hari ini
|
||||
// Set created_at ke hari tersebut 00:00:00 untuk filter bekerja dengan baik
|
||||
$dummyDateTime = \Carbon\Carbon::parse($date . ' 00:00:00');
|
||||
|
||||
$result[] = (object)[
|
||||
'id' => null,
|
||||
'student' => $student,
|
||||
'class' => $kelas,
|
||||
'status' => 'in progress',
|
||||
'created_at' => $dummyDateTime,
|
||||
'picture' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group data by tanggal (created_at)
|
||||
$groupedByDate = collect();
|
||||
foreach ($result as $item) {
|
||||
$dateKey = $item->created_at->format('Y-m-d');
|
||||
|
||||
if (!$groupedByDate->has($dateKey)) {
|
||||
$groupedByDate->put($dateKey, []);
|
||||
}
|
||||
$items = $groupedByDate->get($dateKey);
|
||||
$items[] = $item;
|
||||
$groupedByDate->put($dateKey, $items);
|
||||
}
|
||||
|
||||
// Urutkan by tanggal (terbaru dulu)
|
||||
$groupedArray = $groupedByDate->toArray();
|
||||
krsort($groupedArray);
|
||||
|
||||
// Get today's date untuk default filter
|
||||
$today = now()->format('Y-m-d');
|
||||
|
||||
return view('pages.absensi.absensi_harian', [
|
||||
'absensi' => collect($result),
|
||||
'groupedByDate' => $groupedArray,
|
||||
'today' => $today,
|
||||
'kelas' => $kelas,
|
||||
'selectedTanggal' => $tanggal,
|
||||
'selectedBulan' => $bulan,
|
||||
'selectedTahun' => $tahun
|
||||
]);
|
||||
}
|
||||
|
||||
public function editStatus(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => 'required|string'
|
||||
]);
|
||||
|
||||
// Jika $id adalah 'null', artinya create baru (belum ada di database)
|
||||
if ($id === 'null') {
|
||||
$request->validate([
|
||||
'id_student' => 'required|exists:students,id',
|
||||
'id_class' => 'required|exists:clases,id',
|
||||
]);
|
||||
|
||||
try {
|
||||
AttendanceHistoryDaily::create([
|
||||
'id_student' => $request->id_student,
|
||||
'id_class' => $request->id_class,
|
||||
'status' => $request->status,
|
||||
'picture' => $request->picture ?? '',
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Data absensi berhasil ditambahkan.']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Error: ' . $e->getMessage()], 500);
|
||||
}
|
||||
} else {
|
||||
// Update existing
|
||||
$absensi = AttendanceHistoryDaily::findOrFail($id);
|
||||
$absensi->status = $request->status;
|
||||
$absensi->save();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Status absensi berhasil diupdate.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function exportExcel($classId, Request $request)
|
||||
{
|
||||
// Get kelas info
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
$kelasName = $kelas->name ?? 'Kelas';
|
||||
|
||||
$tanggal = $request->get('tanggal');
|
||||
$bulan = $request->get('bulan');
|
||||
$tahun = $request->get('tahun');
|
||||
|
||||
// Jika bulan kosong tapi tahun ada, lakukan export tahunan
|
||||
if (empty($tanggal) && empty($bulan) && $tahun) {
|
||||
return $this->exportExcelYearly($classId, $tahun);
|
||||
}
|
||||
|
||||
// Get data absensi harian
|
||||
$rows = $this->getAbsensiHarianRowsForExport($classId, $request);
|
||||
|
||||
// Build filename suffix
|
||||
if ($tanggal) {
|
||||
$suffixTanggal = '_' . preg_replace('/[^0-9\-]/', '', $tanggal);
|
||||
} else if ($bulan && $tahun) {
|
||||
$suffixTanggal = '_' . $tahun . '-' . $bulan;
|
||||
} else if ($bulan) {
|
||||
$suffixTanggal = '_Bulan' . $bulan;
|
||||
} else if ($tahun) {
|
||||
$suffixTanggal = '_' . $tahun;
|
||||
} else {
|
||||
$suffixTanggal = '_' . now()->format('Y-m-d');
|
||||
}
|
||||
|
||||
$filename = 'Rekap_Absensi_Harian_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . $suffixTanggal;
|
||||
|
||||
// Create spreadsheet
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Absensi Harian');
|
||||
|
||||
// Insert 2 rows untuk title dan subtitle
|
||||
$sheet->insertNewRowBefore(1, 2);
|
||||
|
||||
// Title and subtitle
|
||||
$title = 'REKAP ABSENSI HARIAN - ' . strtoupper($kelasName);
|
||||
$subtitle = 'Diekspor pada: ' . now()->timezone('Asia/Jakarta')->format('d M Y H:i:s') . ' WIB';
|
||||
|
||||
$sheet->setCellValue('A1', $title);
|
||||
$sheet->setCellValue('A2', $subtitle);
|
||||
|
||||
// Merge cells untuk title dan subtitle
|
||||
$sheet->mergeCells('A1:F1');
|
||||
$sheet->mergeCells('A2:F2');
|
||||
|
||||
// Set row heights
|
||||
$sheet->getRowDimension(1)->setRowHeight(24);
|
||||
$sheet->getRowDimension(2)->setRowHeight(18);
|
||||
|
||||
// Style title (Row 1)
|
||||
$sheet->getStyle('A1:G1')->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 14,
|
||||
'color' => ['rgb' => '1F2937'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Style subtitle (Row 2)
|
||||
$sheet->getStyle('A2:G2')->applyFromArray([
|
||||
'font' => [
|
||||
'size' => 10,
|
||||
'color' => ['rgb' => '6B7280'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Add headers at row 3
|
||||
$headers = ['No', 'Nama Siswa', 'NISN', 'Kelas', 'Status', 'Waktu'];
|
||||
foreach ($headers as $col => $header) {
|
||||
$sheet->setCellValue($this->getColumnLetter($col + 1) . '3', $header);
|
||||
}
|
||||
|
||||
// Style headers (Row 3)
|
||||
$headerRange = 'A3:F3';
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'color' => ['rgb' => 'FFFFFF'],
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => '365CF5'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
'wrapText' => true,
|
||||
],
|
||||
]);
|
||||
$sheet->getRowDimension(3)->setRowHeight(20);
|
||||
|
||||
// Add data rows starting from row 4
|
||||
$dataStartRow = 4;
|
||||
foreach ($rows as $index => $row) {
|
||||
$currentRow = $dataStartRow + $index;
|
||||
$sheet->setCellValue('A' . $currentRow, $row['no']);
|
||||
$sheet->setCellValue('B' . $currentRow, $row['student_name']);
|
||||
$sheet->setCellValue('C' . $currentRow, $row['nisn']);
|
||||
$sheet->setCellValue('D' . $currentRow, $row['class_name']);
|
||||
$sheet->setCellValue('E' . $currentRow, $row['status']);
|
||||
$sheet->setCellValue('F' . $currentRow, $row['waktu']);
|
||||
|
||||
// Zebra rows (alternate background color)
|
||||
if (($index % 2) === 1) {
|
||||
$sheet->getStyle('A' . $currentRow . ':F' . $currentRow)->applyFromArray([
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'F4F7FF'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get highest row
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
|
||||
// Apply borders to all data
|
||||
$tableRange = 'A3:F' . $highestRow;
|
||||
$sheet->getStyle($tableRange)->applyFromArray([
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => 'D1D5DB'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Center align specific columns
|
||||
$sheet->getStyle('A4:A' . $highestRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle('D4:D' . $highestRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle('E4:E' . $highestRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
|
||||
// Set column widths
|
||||
$sheet->getColumnDimension('A')->setWidth(6);
|
||||
$sheet->getColumnDimension('B')->setWidth(28);
|
||||
$sheet->getColumnDimension('C')->setWidth(16);
|
||||
$sheet->getColumnDimension('D')->setWidth(14);
|
||||
$sheet->getColumnDimension('E')->setWidth(16);
|
||||
$sheet->getColumnDimension('F')->setWidth(20);
|
||||
|
||||
// Freeze top 3 rows (title + subtitle + header)
|
||||
$sheet->freezePane('A4');
|
||||
|
||||
// Save and download
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$tempPath = storage_path('app/temp/' . $filename . '.xlsx');
|
||||
@mkdir(dirname($tempPath), 0755, true);
|
||||
$writer->save($tempPath);
|
||||
|
||||
return response()->download($tempPath)->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export absensi harian ke Excel - Format Tahunan (12 Bulan)
|
||||
*/
|
||||
private function exportExcelYearly($classId, $tahun)
|
||||
{
|
||||
// Get kelas info
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
$kelasName = $kelas->name ?? 'Kelas';
|
||||
|
||||
// Get all students
|
||||
$students = Student::where('id_class', $classId)->orderBy('name', 'ASC')->get();
|
||||
if ($students->isEmpty()) {
|
||||
return response()->json(['error' => 'Tidak ada siswa di kelas'], 400);
|
||||
}
|
||||
|
||||
// Generate filename
|
||||
$filename = 'Laporan_Tahunan_Absensi_Harian_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $kelasName) . '_' . $tahun . '.xlsx';
|
||||
|
||||
// Create spreadsheet
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Laporan Tahunan');
|
||||
|
||||
// Row 1: Title (Yellow background)
|
||||
$title = 'LAPORAN TAHUNAN ABSENSI HARIAN ' . strtoupper($kelasName);
|
||||
$sheet->setCellValue('A1', $title);
|
||||
$sheet->mergeCells('A1:Q1');
|
||||
$sheet->getRowDimension(1)->setRowHeight(25);
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 14, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Row 2: Period info
|
||||
$periodText = 'TAHUN ' . $tahun;
|
||||
$sheet->setCellValue('A2', $periodText);
|
||||
$sheet->mergeCells('A2:Q2');
|
||||
$sheet->getRowDimension(2)->setRowHeight(20);
|
||||
$sheet->getStyle('A2')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 12, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Row 3: Empty
|
||||
$sheet->getRowDimension(3)->setRowHeight(10);
|
||||
|
||||
// Row 4: Headers
|
||||
$sheet->setCellValue('A4', 'Nama');
|
||||
$sheet->setCellValue('B4', 'Jabatan/Kelas');
|
||||
|
||||
// Month Names
|
||||
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agt', 'Sep', 'Okt', 'Nov', 'Des'];
|
||||
foreach ($months as $idx => $monthName) {
|
||||
$colLetter = $this->getColumnLetter($idx + 3); // C = 3
|
||||
$sheet->setCellValue($colLetter . '4', $monthName);
|
||||
}
|
||||
|
||||
// Totals Headers
|
||||
$sheet->setCellValue('O4', "Total\nTepat Waktu");
|
||||
$sheet->setCellValue('P4', "Total\nTerlambat");
|
||||
$sheet->setCellValue('Q4', "Total\nIzin");
|
||||
|
||||
// Style headers (Row 4) - Yellow background
|
||||
$headerRange = 'A4:Q4';
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 11, 'color' => ['rgb' => '000000']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'FFCC00']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
]);
|
||||
$sheet->getRowDimension(4)->setRowHeight(30);
|
||||
|
||||
// Fetch attendance data for the whole year
|
||||
$attendanceData = [];
|
||||
$attendance = AttendanceHistoryDaily::where('id_class', $classId)
|
||||
->whereYear('created_at', $tahun)
|
||||
->get();
|
||||
|
||||
foreach ($attendance as $att) {
|
||||
$date = \Carbon\Carbon::parse($att->created_at);
|
||||
$month = $date->month;
|
||||
$studentId = $att->id_student;
|
||||
$status = $att->status;
|
||||
|
||||
if (!isset($attendanceData[$studentId])) {
|
||||
$attendanceData[$studentId] = [];
|
||||
}
|
||||
if (!isset($attendanceData[$studentId][$month])) {
|
||||
$attendanceData[$studentId][$month] = [
|
||||
'tepat_waktu' => 0,
|
||||
'terlambat' => 0,
|
||||
'izin' => 0
|
||||
];
|
||||
}
|
||||
if (in_array($status, ['tepat_waktu', 'terlambat', 'izin'])) {
|
||||
$attendanceData[$studentId][$month][$status]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add student rows
|
||||
$dataRow = 5;
|
||||
foreach ($students as $student) {
|
||||
$sheet->setCellValue('A' . $dataRow, $student->name);
|
||||
$sheet->setCellValue('B' . $dataRow, $kelasName);
|
||||
|
||||
$totalTepatWaktu = 0;
|
||||
$totalTerlambat = 0;
|
||||
$totalIzin = 0;
|
||||
|
||||
// Populate months (C to N)
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$colLetter = $this->getColumnLetter($month + 2); // C = 3
|
||||
|
||||
$tw = 0; $tl = 0; $i = 0;
|
||||
if (isset($attendanceData[$student->id][$month])) {
|
||||
$tw = $attendanceData[$student->id][$month]['tepat_waktu'] ?? 0;
|
||||
$tl = $attendanceData[$student->id][$month]['terlambat'] ?? 0;
|
||||
$i = $attendanceData[$student->id][$month]['izin'] ?? 0;
|
||||
}
|
||||
|
||||
$totalTepatWaktu += $tw;
|
||||
$totalTerlambat += $tl;
|
||||
$totalIzin += $i;
|
||||
|
||||
if ($tw > 0 || $tl > 0 || $i > 0) {
|
||||
$displayText = "TW:{$tw}\nTL:{$tl}\nI:{$i}";
|
||||
$sheet->setCellValue($colLetter . $dataRow, $displayText);
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setWrapText(true);
|
||||
} else {
|
||||
$sheet->setCellValue($colLetter . $dataRow, '-');
|
||||
}
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
// Populate totals
|
||||
$sheet->setCellValue('O' . $dataRow, $totalTepatWaktu);
|
||||
$sheet->setCellValue('P' . $dataRow, $totalTerlambat);
|
||||
$sheet->setCellValue('Q' . $dataRow, $totalIzin);
|
||||
|
||||
for ($c = 15; $c <= 17; $c++) {
|
||||
$colLetter = $this->getColumnLetter($c);
|
||||
$sheet->getStyle($colLetter . $dataRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
}
|
||||
|
||||
// Border styling
|
||||
$dataRange = 'A' . $dataRow . ':Q' . $dataRow;
|
||||
$sheet->getStyle($dataRange)->applyFromArray([
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
|
||||
// Set height for dynamic wrapped text
|
||||
$sheet->getRowDimension($dataRow)->setRowHeight(45);
|
||||
|
||||
$dataRow++;
|
||||
}
|
||||
|
||||
// Add legend
|
||||
$legendRow = $dataRow + 2;
|
||||
$sheet->setCellValue('A' . $legendRow, 'Keterangan format bulanan:');
|
||||
$sheet->getStyle('A' . $legendRow)->applyFromArray(['font' => ['bold' => true, 'size' => 11]]);
|
||||
|
||||
$legendRow++;
|
||||
$legends = [
|
||||
'TW' => 'Tepat Waktu',
|
||||
'TL' => 'Terlambat',
|
||||
'I' => 'Izin'
|
||||
];
|
||||
|
||||
foreach ($legends as $code => $meaning) {
|
||||
$sheet->setCellValue('A' . $legendRow, $code);
|
||||
$sheet->setCellValue('B' . $legendRow, $meaning);
|
||||
$sheet->getStyle('A' . $legendRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$legendRow++;
|
||||
}
|
||||
|
||||
// Set column widths
|
||||
$sheet->getColumnDimension('A')->setWidth(25);
|
||||
$sheet->getColumnDimension('B')->setWidth(15);
|
||||
|
||||
// Month columns width
|
||||
for ($month = 1; $month <= 12; $month++) {
|
||||
$colLetter = $this->getColumnLetter($month + 2);
|
||||
$sheet->getColumnDimension($colLetter)->setWidth(10);
|
||||
}
|
||||
|
||||
$sheet->getColumnDimension('O')->setWidth(15);
|
||||
$sheet->getColumnDimension('P')->setWidth(12);
|
||||
$sheet->getColumnDimension('Q')->setWidth(12);
|
||||
|
||||
// Save and download
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$tempPath = storage_path('app/temp/' . $filename);
|
||||
@mkdir(dirname($tempPath), 0755, true);
|
||||
$writer->save($tempPath);
|
||||
|
||||
return response()->download($tempPath)->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert column number to letter (1->A, 2->B, etc)
|
||||
*/
|
||||
private function getColumnLetter($col)
|
||||
{
|
||||
$letter = '';
|
||||
while ($col > 0) {
|
||||
$col--;
|
||||
$letter = chr(65 + ($col % 26)) . $letter;
|
||||
$col = intdiv($col, 26);
|
||||
}
|
||||
return $letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method untuk prepare data export absensi harian
|
||||
*/
|
||||
private function getAbsensiHarianRowsForExport($classId, Request $request)
|
||||
{
|
||||
// Get kelas
|
||||
$kelas = Classes::findOrFail($classId);
|
||||
|
||||
// Get semua siswa di kelas
|
||||
$students = Student::where('id_class', $classId)->get();
|
||||
|
||||
// Determine filter parameters
|
||||
$tanggal = $request->get('tanggal');
|
||||
$bulan = $request->get('bulan');
|
||||
$tahun = $request->get('tahun');
|
||||
|
||||
// Default ke hari ini hanya jika tidak ada filter apapun
|
||||
if (!$tanggal && !$bulan && !$tahun) {
|
||||
$tanggal = now()->format('Y-m-d');
|
||||
}
|
||||
|
||||
// Build query untuk fetch attendance records
|
||||
$query = AttendanceHistoryDaily::with(['student', 'class'])
|
||||
->where('id_class', $classId);
|
||||
|
||||
// Jika tanggal diset, filter by exact date
|
||||
if ($tanggal && !$bulan && !$tahun) {
|
||||
$query->whereDate('created_at', $tanggal);
|
||||
}
|
||||
// Jika bulan dan tahun keduanya diset, filter by month AND year
|
||||
else if ($bulan && $tahun) {
|
||||
$query->whereMonth('created_at', $bulan)
|
||||
->whereYear('created_at', $tahun);
|
||||
}
|
||||
// Jika hanya bulan diset, filter by month
|
||||
else if ($bulan) {
|
||||
$query->whereMonth('created_at', $bulan);
|
||||
}
|
||||
// Jika hanya tahun diset, filter by year
|
||||
else if ($tahun) {
|
||||
$query->whereYear('created_at', $tahun);
|
||||
}
|
||||
|
||||
$absensi = $query->get();
|
||||
|
||||
// Get unique dates dari filtered data
|
||||
$uniqueDates = $absensi
|
||||
->map(function($item) { return $item->created_at->format('Y-m-d'); })
|
||||
->unique()
|
||||
->values()
|
||||
->sort()
|
||||
->reverse()
|
||||
->values();
|
||||
|
||||
// Build rows untuk export: untuk setiap hari, untuk setiap siswa
|
||||
$rows = [];
|
||||
$no = 1;
|
||||
|
||||
foreach ($uniqueDates as $date) {
|
||||
foreach ($students as $student) {
|
||||
// Cari record untuk student + date ini
|
||||
$record = $absensi
|
||||
->where('id_student', $student->id)
|
||||
->filter(function($item) use ($date) {
|
||||
return $item->created_at->format('Y-m-d') === $date;
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($record) {
|
||||
// Ada record di database
|
||||
$rows[] = [
|
||||
'no' => $no,
|
||||
'student_name' => $record->student->name ?? '-',
|
||||
'nisn' => $record->student->nisn ?? '-',
|
||||
'class_name' => $record->class->name ?? '-',
|
||||
'status' => ucfirst($record->status),
|
||||
'waktu' => $record->created_at->format('d M Y H:i:s'),
|
||||
];
|
||||
} else {
|
||||
// Dummy record: siswa belum absen di hari ini
|
||||
$rows[] = [
|
||||
'no' => $no,
|
||||
'student_name' => $student->name,
|
||||
'nisn' => $student->nisn ?? '-',
|
||||
'class_name' => $kelas->name,
|
||||
'status' => 'In Progress',
|
||||
'waktu' => '-',
|
||||
];
|
||||
}
|
||||
|
||||
$no++;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
|
||||
use App\Models\AcademicPeriod;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Subject;
|
||||
use App\Models\Teacher;
|
||||
use App\Services\QrLinkService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class JadwalController extends Controller
|
||||
{
|
||||
protected QrLinkService $qrLinkService;
|
||||
|
||||
public function __construct(QrLinkService $qrLinkService)
|
||||
{
|
||||
$this->qrLinkService = $qrLinkService;
|
||||
}
|
||||
public function index()
|
||||
{
|
||||
$kelas = Classes::all();
|
||||
$mapel = Subject::all();
|
||||
|
||||
// Join teachers dengan subjects berdasarkan kolom subject (code)
|
||||
$guru = Teacher::all()->map(function($teacher) {
|
||||
// Cari subject berdasarkan code yang ada di kolom subject teacher
|
||||
// Gunakan array_map('trim', ...) untuk menghilangkan spasi sebelum/sesudah koma
|
||||
$subjectCodes = array_map('trim', explode(',', $teacher->subject));
|
||||
$subjects = Subject::whereIn('code', $subjectCodes)->pluck('id')->toArray();
|
||||
$teacher->subject_ids = $subjects;
|
||||
return $teacher;
|
||||
});
|
||||
|
||||
$periodes = AcademicPeriod::orderBy('start_date', 'desc')->get();
|
||||
$periode_aktif = AcademicPeriod::where('is_active', true)->first();
|
||||
$jadwal = Schedule::with(['class', 'subject', 'teacher'])->get();
|
||||
return view('pages.jadwal.jadwal', compact('kelas', 'mapel', 'guru', 'periodes', 'periode_aktif', 'jadwal'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'day_of_week' => 'required|string',
|
||||
'period_start' => 'required|integer',
|
||||
'period_end' => 'required|integer',
|
||||
'start_time' => 'required|string',
|
||||
'end_time' => 'required|string',
|
||||
'code' => 'nullable|string',
|
||||
'id_class' => 'required|exists:clases,id',
|
||||
'id_subject' => 'required|exists:subjects,id',
|
||||
'id_teacher' => 'required|exists:teachers,id',
|
||||
'id_academic_periods' => 'nullable|exists:academic_periods,id',
|
||||
]);
|
||||
|
||||
// Hitung jam mulai dan jam selesai
|
||||
$startHour = 7 + ($validated['period_start'] - 1);
|
||||
$endHour = 7 + ($validated['period_end'] - 1);
|
||||
$start_time = sprintf('%02d:00', $startHour);
|
||||
$end_time = sprintf('%02d:00', $endHour);
|
||||
|
||||
// Ambil data untuk kode otomatis
|
||||
$class = Classes::find($validated['id_class']);
|
||||
$subject = Subject::find($validated['id_subject']);
|
||||
$period = $validated['id_academic_periods'] ?? '';
|
||||
$token = (string) random_int(1000, 9999);
|
||||
|
||||
$day = strtoupper(substr($validated['day_of_week'], 0, 3));
|
||||
$className = $class ? $class->name : '';
|
||||
$subjectCode = $subject ? $subject->code : '';
|
||||
$periodCode = $period ?: '0';
|
||||
|
||||
$autoCode = "{$className}-{$day}-{$validated['period_start']}-{$validated['period_end']}-{$subjectCode}-{$periodCode}-{$token}";
|
||||
|
||||
$code = $request->input('code') ?: $autoCode;
|
||||
|
||||
// Hash code sebelum simpan ke database
|
||||
$hashedCode = Crypt::encryptString($code);
|
||||
|
||||
Schedule::create([
|
||||
// make day of week to lowercase
|
||||
'day_of_week' => strtolower($validated['day_of_week']),
|
||||
'period_start' => $validated['period_start'],
|
||||
'period_end' => $validated['period_end'],
|
||||
'start_time' => $validated['start_time'],
|
||||
'end_time' => $validated['end_time'],
|
||||
'code' => $hashedCode,
|
||||
'id_class' => $validated['id_class'],
|
||||
'id_subject' => $validated['id_subject'],
|
||||
'id_teacher' => $validated['id_teacher'],
|
||||
'id_academic_periods' => $validated['id_academic_periods'] ?? null,
|
||||
]);
|
||||
|
||||
return redirect()->route('jadwal.index')->with('success', 'Jadwal berhasil ditambahkan!');
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'day_of_week' => 'required|string',
|
||||
'period_start' => 'required|integer',
|
||||
'period_end' => 'required|integer',
|
||||
'start_time' => 'required|string',
|
||||
'end_time' => 'required|string',
|
||||
'code' => 'required|string',
|
||||
'id_class' => 'required|exists:clases,id',
|
||||
'id_subject' => 'required|exists:subjects,id',
|
||||
'id_teacher' => 'required|exists:teachers,id',
|
||||
'id_academic_periods' => 'nullable|exists:academic_periods,id',
|
||||
]);
|
||||
|
||||
// Hitung jam mulai dan jam selesai
|
||||
$startHour = 7 + ($validated['period_start'] - 1);
|
||||
$endHour = 7 + ($validated['period_end'] - 1);
|
||||
$start_time = sprintf('%02d:00', $startHour);
|
||||
$end_time = sprintf('%02d:00', $endHour);
|
||||
|
||||
// Ambil schedule lama untuk cek perubahan dan avoid double encryption
|
||||
$jadwalLama = Schedule::findOrFail($id);
|
||||
|
||||
// Cek apakah ada perubahan di field-field yang mempengaruhi code
|
||||
$codeChanged = false;
|
||||
if ($jadwalLama->day_of_week != strtolower($validated['day_of_week']) ||
|
||||
$jadwalLama->period_start != $validated['period_start'] ||
|
||||
$jadwalLama->period_end != $validated['period_end'] ||
|
||||
$jadwalLama->id_subject != $validated['id_subject'] ||
|
||||
$jadwalLama->id_class != $validated['id_class'] ||
|
||||
$jadwalLama->id_academic_periods != $validated['id_academic_periods']) {
|
||||
$codeChanged = true;
|
||||
}
|
||||
|
||||
// Generate code baru jika ada perubahan, atau gunakan code lama
|
||||
if ($codeChanged) {
|
||||
// Generate code baru
|
||||
$class = Classes::find($validated['id_class']);
|
||||
$subject = Subject::find($validated['id_subject']);
|
||||
$period = $validated['id_academic_periods'] ?? '';
|
||||
$token = (string) random_int(1000, 9999);
|
||||
|
||||
$day = strtoupper(substr($validated['day_of_week'], 0, 3));
|
||||
$className = $class ? $class->name : '';
|
||||
$subjectCode = $subject ? $subject->code : '';
|
||||
$periodCode = $period ?: '0';
|
||||
|
||||
$autoCode = "{$className}-{$day}-{$validated['period_start']}-{$validated['period_end']}-{$subjectCode}-{$periodCode}-{$token}";
|
||||
$hashedCode = Crypt::encryptString($autoCode);
|
||||
} else {
|
||||
// Gunakan code lama tanpa encrypt ulang (avoid double encryption)
|
||||
$hashedCode = $jadwalLama->code;
|
||||
}
|
||||
|
||||
$jadwalLama->update([
|
||||
'day_of_week' => strtolower($validated['day_of_week']),
|
||||
'period_start' => $validated['period_start'],
|
||||
'period_end' => $validated['period_end'],
|
||||
'start_time' => $validated['start_time'],
|
||||
'end_time' => $validated['end_time'],
|
||||
'code' => $hashedCode,
|
||||
'id_class' => $validated['id_class'],
|
||||
'id_subject' => $validated['id_subject'],
|
||||
'id_teacher' => $validated['id_teacher'],
|
||||
'id_academic_periods' => $validated['id_academic_periods'] ?? null,
|
||||
]);
|
||||
|
||||
return redirect()->route('jadwal.index')->with('success', 'Jadwal berhasil diupdate!');
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$ids = $request->input('ids');
|
||||
if (!$ids) {
|
||||
return redirect()->route('jadwal.index')->with('error', 'Tidak ada jadwal yang dipilih untuk dihapus.');
|
||||
}
|
||||
// Ubah string menjadi array jika perlu
|
||||
if (is_string($ids)) {
|
||||
$ids = explode(',', $ids);
|
||||
}
|
||||
Schedule::destroy($ids);
|
||||
return redirect()->route('jadwal.index')->with('success', 'Jadwal berhasil dihapus!');
|
||||
}
|
||||
|
||||
public function showQr($id)
|
||||
{
|
||||
$jadwal = Schedule::findOrFail($id);
|
||||
return view('pages.jadwal.qr', compact('jadwal'));
|
||||
}
|
||||
|
||||
public function showPublicQr($token)
|
||||
{
|
||||
$tokenData = Cache::get("public_qr_link:$token");
|
||||
|
||||
if (!$tokenData) {
|
||||
return view('public.attendance.qr-error', [
|
||||
'status' => 'expired',
|
||||
'message' => 'Link QR sudah expired atau tidak valid. Silakan minta link baru dari guru.'
|
||||
]);
|
||||
}
|
||||
|
||||
$idSchedule = $tokenData['schedule_id'];
|
||||
$expiresAt = $tokenData['expires_at'];
|
||||
|
||||
$jadwal = Schedule::with(['class', 'subject', 'teacher'])->find($idSchedule);
|
||||
|
||||
if (!$jadwal) {
|
||||
return view('public.attendance.qr-error', [
|
||||
'status' => 'notfound',
|
||||
'message' => 'Data jadwal tidak ditemukan.'
|
||||
]);
|
||||
}
|
||||
|
||||
$publicUrl = route('jadwal.qr.public', ['token' => $token]);
|
||||
|
||||
return view('public.attendance.qr', compact('jadwal', 'publicUrl', 'expiresAt'));
|
||||
}
|
||||
|
||||
public function generateQrLink($id)
|
||||
{
|
||||
$result = $this->qrLinkService->generateLink($id);
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\NotificationType;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Notification;
|
||||
use App\Services\FirebaseMessagingService;
|
||||
use App\Services\NotificationHelperService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
|
||||
//* Dokumentasi Penggunaan Template Message untuk fitur Notifikasi
|
||||
//* Contoh penggunaan template untuk kirim ke topic kelas
|
||||
// $template = $this->notificationHelper->templateTugasKelas(
|
||||
// 'kelas_10_TKJ_1',
|
||||
// 'Tugas Baru: Matematika',
|
||||
// 'Tugas matematika untuk minggu ini sudah tersedia. Silakan cek aplikasi untuk detailnya.',
|
||||
// );
|
||||
// $result = $this->notificationHelper->send($template);
|
||||
|
||||
// * Contoh penggunaan template untuk kirim ke semua siswa (pengumuman umum)
|
||||
// $template = $this->notificationHelper->templatePengumuman(
|
||||
// 'Libur Sekolah',
|
||||
// 'Sekolah akan libur pada tanggal 25 Desember 2024 hingga 1 Januari 2025. Selamat berlibur!',
|
||||
// );
|
||||
// $result = $this->notificationHelper->send($template);
|
||||
|
||||
// * Contoh penggunaan template untuk kirim ke satu device (token)
|
||||
// $template = $this->notificationHelper->tokenTemplate(
|
||||
// 'fcm_token_device',
|
||||
// 'Pengumuman Pribadi',
|
||||
// 'Ini adalah pengumuman khusus untuk Anda. Harap cek aplikasi untuk detailnya.',
|
||||
// );
|
||||
// $result = $this->notificationHelper->send($template);
|
||||
|
||||
|
||||
//* Dokumentasi Penggunaan Data NotficationCreateTemplate untuk menyimpan ke database
|
||||
|
||||
// $payloadAll = $this->notificationHelper->createTemplateForAll(
|
||||
// 'Info Umum',
|
||||
// 'Sekolah libur besok',
|
||||
// NotificationType::AnnouncementGeneral,
|
||||
// auth()->id(),
|
||||
// );
|
||||
|
||||
// $payloadClass = $this->notificationHelper->createTemplateForClass(
|
||||
// 10,
|
||||
// 'Tugas Baru',
|
||||
// 'Kerjakan latihan halaman 12',
|
||||
// NotificationType::Assignment,
|
||||
// auth()->id(),
|
||||
// );
|
||||
|
||||
// $payloadStudent = $this->notificationHelper->createTemplateForStudent(
|
||||
// 25,
|
||||
// 'Catatan Personal',
|
||||
// 'Harap hubungi wali kelas',
|
||||
// NotificationType::PersonalNote,
|
||||
// auth()->id(),
|
||||
// );
|
||||
|
||||
public function index()
|
||||
{
|
||||
$notifications = Notification::with(['sender.teacher', 'sender.student', 'class'])
|
||||
->latest()
|
||||
->get();
|
||||
return view('pages.notifikasi.notifikasi', compact('notifications'));
|
||||
}
|
||||
|
||||
protected FirebaseMessagingService $fcm;
|
||||
protected NotificationHelperService $notificationHelper;
|
||||
|
||||
public function __construct(FirebaseMessagingService $fcm, NotificationHelperService $notificationHelper)
|
||||
{
|
||||
$this->fcm = $fcm;
|
||||
$this->notificationHelper = $notificationHelper;
|
||||
}
|
||||
|
||||
// Test kirim ke satu token
|
||||
public function sendToToken(Request $request)
|
||||
{
|
||||
$token = $request->input('token'); // FCM token dari mobile
|
||||
$title = $request->input('title', 'Judul Notifikasi');
|
||||
$body = $request->input('body', 'Isi notifikasi');
|
||||
|
||||
$data = $request->input('data', []); // optional payload
|
||||
|
||||
$this->fcm->sendToToken($token, $title, $body, $data);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
// Kirim ke topic
|
||||
public function sendToTopic(Request $request)
|
||||
{
|
||||
$topic = $request->input('topic'); // contoh: 'session_123'
|
||||
$title = $request->input('title', 'Judul Notifikasi');
|
||||
$body = $request->input('body', 'Isi notifikasi');
|
||||
|
||||
$data = $request->input('data', []);
|
||||
|
||||
$this->fcm->sendToTopic($topic, $title, $body, $data);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
// Contoh penggunaan template notifikasi untuk target topic kelas
|
||||
public function sendTopicTemplateExample(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
$template = $this->notificationHelper->templateAssignmentForClass(
|
||||
$request->input('topic', 'kelas_10_TKJ_1'),
|
||||
$request->input('title', 'Tugas Baru: Matematika'),
|
||||
$request->input('body', 'Tugas matematika untuk minggu ini silahkan kerjakan buku praktikum halaman 12-15'),
|
||||
);
|
||||
|
||||
$result = $this->notificationHelper->send($template);
|
||||
|
||||
$createTemplate = $this->notificationHelper->createTemplateForClass(
|
||||
1, // class_id, for testing only
|
||||
$template->title,
|
||||
$template->body,
|
||||
NotificationType::Assignment,
|
||||
1, // sender_id, bisa diganti dengan auth()->id() jika sudah ada sistem autentikasi
|
||||
);
|
||||
|
||||
// store to database
|
||||
Notification::create($createTemplate->toArray());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Topic template notification sent successfully',
|
||||
'template' => $template->toArray(),
|
||||
'create_template' => $createTemplate->toArray(),
|
||||
'result' => $result,
|
||||
]);
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Failed to send topic template notification',
|
||||
'message' => $th->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// notes untuk alvian:
|
||||
// $template = $this->notificationHelper->templatePermissionApproval(
|
||||
// $request->input('userid'), // receiver_id, contoh: user id siswa yang izin nya disetujui
|
||||
// // $request->input('title', 'Tugas Baru: Matematika'),
|
||||
// );
|
||||
|
||||
|
||||
// $result = $this->notificationHelper->send($template);
|
||||
|
||||
// $createTemplate = $this->notificationHelper->createTemplateForStudent(
|
||||
// $request->input('userid'), // receiver_id, contoh: user id siswa yang izin nya disetujui
|
||||
// $template->title,
|
||||
// $template->body,
|
||||
// NotificationType::PersonalNote,
|
||||
// 1, // sender_id, bisa diganti dengan auth()->id() jika sudah ada sistem autentikasi
|
||||
// );
|
||||
|
||||
// // store to database
|
||||
// Notification::create($createTemplate->toArray());
|
||||
|
||||
|
||||
// universal send (kirim ke semua student device, untuk pengumuman umum seperti libur, maintenance, dll)
|
||||
public function sendToAll(Request $request)
|
||||
{
|
||||
$title = $request->input('title', 'Judul Notifikasi');
|
||||
$body = $request->input('body', 'Isi notifikasi');
|
||||
|
||||
$data = $request->input('data', []);
|
||||
|
||||
// Kirim ke topic global untuk semua siswa
|
||||
$this->fcm->sendToTopic('student_notifications', $title, $body, $data);
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
|
||||
// send to class topic (announcement, homework, assignment, etc)
|
||||
public function sendToClassTopic(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
// user auth check
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$userId = $user->id;
|
||||
|
||||
$request->validate([
|
||||
'class_id' => 'required|integer',
|
||||
'type' => 'nullable|string',
|
||||
'title' => 'nullable|string',
|
||||
'body' => 'nullable|string',
|
||||
'data' => 'nullable',
|
||||
]);
|
||||
|
||||
$type = $request->input('type', 'announcement'); // default ke announcement
|
||||
|
||||
$classId = $request->input('class_id'); // contoh: '123'
|
||||
$title = $request->input('title', null);
|
||||
$body = $request->input('body', null);
|
||||
$data = $request->input('data', []);
|
||||
|
||||
// get fcm topic from database based on class_id
|
||||
$class = Classes::find($classId);
|
||||
if (!$class) {
|
||||
return response()->json(['error' => 'Class not found'], 404);
|
||||
}
|
||||
|
||||
$topic = $class->fcm_topic;
|
||||
if (!$topic) {
|
||||
return response()->json(['error' => 'No FCM topic assigned to this class'], 404);
|
||||
}
|
||||
|
||||
$template = null;
|
||||
|
||||
switch ($type) {
|
||||
case 'assignment':
|
||||
$t = $title;
|
||||
$b = $body ?? 'Ada tugas baru untuk kelas ini. Silakan cek aplikasi untuk detail.';
|
||||
$template = $this->notificationHelper->templateAssignmentForClass(
|
||||
$topic,
|
||||
$t,
|
||||
$b,
|
||||
$data,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'class_cancelled':
|
||||
// contoh template untuk pengumuman kelas dibatalkan
|
||||
$t = $title ?? 'Kelas Dibatalkan';
|
||||
$b = $body ?? '(pengirim tidak memberikan detail alasan pembatalan)';
|
||||
$template = $this->notificationHelper->templateClassCancelled(
|
||||
$topic,
|
||||
$b
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// default ke template pengumuman umum untuk kelas
|
||||
$t = $title ?? 'Pengumuman Kelas';
|
||||
$b = $body ?? 'Ada pengumuman untuk kelas ini.';
|
||||
$template = $this->notificationHelper->templateAnnouncementForClass(
|
||||
$topic,
|
||||
$t,
|
||||
$b,
|
||||
array_merge($data, ['type' => 'announcement_for_class']),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
return response()->json(['error' => 'Failed to create notification template'], 500);
|
||||
}
|
||||
|
||||
try {
|
||||
$resultMessaging = $this->notificationHelper->send($template);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send attendance violation notification', ['exception' => $e]);
|
||||
$resultMessaging = [
|
||||
'success' => false,
|
||||
'message' => 'Failed to send notification',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$createTemplate = $this->notificationHelper->createTemplateForClass(
|
||||
$classId,
|
||||
$template->title,
|
||||
$template->body,
|
||||
$type,
|
||||
$userId,
|
||||
);
|
||||
|
||||
// store to database
|
||||
Notification::create($createTemplate->toArray());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Notification sent to class topic successfully',
|
||||
'data' => [
|
||||
'messaging' => $resultMessaging,
|
||||
'create_template' => $createTemplate->toArray(),
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
//throw $th;
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Failed to send notification',
|
||||
'message' => $th->getMessage()], 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
Notification::create([
|
||||
'title' => 'Test',
|
||||
'body' => 'Test body',
|
||||
'sender_id' => Auth::id()
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Notifikasi dibuat');
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
try {
|
||||
// Auth user
|
||||
$user = request()->user();
|
||||
if (!$user) {
|
||||
return redirect()->back()->with('error', 'Unauthorized');
|
||||
}
|
||||
|
||||
// Find notification by id
|
||||
$notification = Notification::find($id);
|
||||
if (!$notification) {
|
||||
return redirect()->back()->with('error', 'Notifikasi tidak ditemukan');
|
||||
}
|
||||
|
||||
// Delete notification
|
||||
$notification->delete();
|
||||
|
||||
return redirect()->back()->with('success', 'Notifikasi berhasil dihapus');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AcademicPeriod;
|
||||
|
||||
class PeriodeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$periods = AcademicPeriod::orderByDesc('start_date')->get();
|
||||
return view('pages.periode.periode', compact('periods'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tahun_ajaran' => 'required|string|max:255',
|
||||
'semester' => 'required|string|in:ganjil,genap',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
]);
|
||||
|
||||
// Ubah hanya huruf pertama menjadi kapital (Ganjil/Genap)
|
||||
$semester = ucfirst(strtolower($request->semester));
|
||||
if ($semester === 'Ganjil') {
|
||||
$semester = 'Ganjil';
|
||||
} elseif ($semester === 'Genap') {
|
||||
$semester = 'Genap';
|
||||
}
|
||||
|
||||
$name = $semester . ' ' . $request->tahun_ajaran;
|
||||
|
||||
AcademicPeriod::create([
|
||||
'name' => $name,
|
||||
'start_date' => $request->start_date,
|
||||
'end_date' => $request->end_date,
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
return redirect()->route('periode.index')->with('success', 'Periode berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function activate($id)
|
||||
{
|
||||
// Nonaktifkan semua periode
|
||||
AcademicPeriod::query()->update(['is_active' => false]);
|
||||
// Aktifkan periode yang dipilih
|
||||
AcademicPeriod::where('id', $id)->update(['is_active' => true]);
|
||||
|
||||
return redirect()->route('periode.index')->with('success', 'Periode berhasil diaktifkan.');
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'tahun_ajaran' => 'required|string|max:255',
|
||||
'semester' => 'required|string|in:ganjil,genap',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
]);
|
||||
|
||||
$semester = ucfirst(strtolower($request->semester));
|
||||
$name = $semester . ' ' . $request->tahun_ajaran;
|
||||
|
||||
$period = AcademicPeriod::findOrFail($id);
|
||||
$period->name = $name;
|
||||
$period->start_date = $request->start_date;
|
||||
$period->end_date = $request->end_date;
|
||||
|
||||
// Jika is_active dicentang, aktifkan hanya periode ini
|
||||
if ($request->has('is_active')) {
|
||||
AcademicPeriod::query()->update(['is_active' => false]);
|
||||
$period->is_active = true;
|
||||
} else {
|
||||
$period->is_active = false;
|
||||
}
|
||||
|
||||
$period->save();
|
||||
|
||||
return redirect()->route('periode.index')->with('success', 'Periode berhasil diupdate.');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$period = AcademicPeriod::findOrFail($id);
|
||||
$period->delete();
|
||||
return redirect()->route('periode.index')->with('success', 'Periode berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\NotificationType;
|
||||
use App\Models\AttendanceHistory;
|
||||
use App\Models\AttendanceHistoryDaily;
|
||||
use App\Models\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Student;
|
||||
use App\Services\NotificationHelperService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
|
||||
protected NotificationHelperService $notificationHelper;
|
||||
public $latOfAttendance;
|
||||
public $lonOfAttendance;
|
||||
|
||||
public function __construct(NotificationHelperService $notificationHelper)
|
||||
{
|
||||
$this->notificationHelper = $notificationHelper;
|
||||
$this->initCoordinatesFromConfig();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function initCoordinatesFromConfig(): void
|
||||
{
|
||||
$coordinate = config('coordinate.coordinate', '-7.604032330848524, 112.10176449791652');
|
||||
[$lat, $lon] = array_map('trim', explode(',', $coordinate));
|
||||
$this->latOfAttendance = (float) $lat;
|
||||
$this->lonOfAttendance = (float) $lon;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$permissions = Permission::all();
|
||||
return view('pages.perizinan.perizinan', compact('permissions'));
|
||||
}
|
||||
|
||||
public function permissionAccept(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
try {
|
||||
// session-auth user for web controller
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return redirect()->back()->with('error', 'Unauthorized');
|
||||
}
|
||||
|
||||
// find permission by id
|
||||
$permission = Permission::find($id);
|
||||
if (!$permission) {
|
||||
return redirect()->back()->with('error', 'Data perizinan tidak ditemukan');
|
||||
}
|
||||
|
||||
// permission must be processed only, if already accepted or rejected, cannot be processed again.
|
||||
if ($permission->status === 'diterima') {
|
||||
return redirect()->back()->with('warning', 'Perizinan sudah diterima sebelumnya');
|
||||
}
|
||||
if ($permission->status === 'ditolak') {
|
||||
return redirect()->back()->with('warning', 'Perizinan sudah ditolak sebelumnya');
|
||||
}
|
||||
|
||||
$student = Student::find($permission->id_student);
|
||||
if (!$student) {
|
||||
return redirect()->back()->with('error', 'Data siswa tidak ditemukan');
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($permission, $user, $request, $student) {
|
||||
// update status to accepted
|
||||
$permission->status = 'diterima';
|
||||
$permission->approved_by = $user->id; // set approved by teacher id
|
||||
$permission->feedback = $request->input('feedback', null); // optional feedback message from teacher
|
||||
$permission->save();
|
||||
|
||||
// * create attendance record history to related date (only fill with schedule that has active academic period, and match with date permission).
|
||||
$permissionReason = $permission->reason; // contoh: 'sakit', 'izin', 'dispen', 'lainnya'
|
||||
if ($permissionReason === 'lainnya') {
|
||||
$permissionReason = 'izin';
|
||||
}
|
||||
|
||||
$permissionDate = Carbon::parse($permission->date_permission)->startOfDay();
|
||||
$permissionTimePeriod = (int) $permission->time_period;
|
||||
$createdSchoolDays = 0;
|
||||
$currentDate = $permissionDate->copy();
|
||||
|
||||
// Count time_period as school days; weekend days are skipped and replaced by the next weekday.
|
||||
while ($createdSchoolDays < $permissionTimePeriod) {
|
||||
if ($currentDate->isWeekend()) {
|
||||
$currentDate->addDay();
|
||||
continue;
|
||||
}
|
||||
|
||||
$dayName = strtolower($currentDate->locale('id')->dayName);
|
||||
$dayIndex = $currentDate->dayOfWeekIso; // 1..7
|
||||
|
||||
// search related schedule with day_of_week same and active academic period
|
||||
$schedules = Schedule::query()
|
||||
->where('id_class', $student->id_class)
|
||||
->whereHas('academicPeriods', function ($q) {
|
||||
$q->where('is_active', 1);
|
||||
})
|
||||
->where(function ($q) use ($dayName, $dayIndex) {
|
||||
$q->where('day_of_week', $dayName)
|
||||
->orWhere('day_of_week', (string) $dayIndex);
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($schedules as $schedule) {
|
||||
AttendanceHistory::updateOrCreate(
|
||||
[
|
||||
'id_schedule' => $schedule->id,
|
||||
'id_student' => $student->id,
|
||||
'attendance_date' => $currentDate->toDateString(),
|
||||
],
|
||||
[
|
||||
'id_class' => $schedule->id_class,
|
||||
'period_number' => $schedule->period_start,
|
||||
'coordinate' => $this->latOfAttendance . ', ' . $this->lonOfAttendance,
|
||||
'status' => $permissionReason,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// update attendancehistorydaily to 'izin'
|
||||
// Cari berdasarkan id_student, id_class, dan tanggal dari created_at
|
||||
// Jika ada, update. Jika tidak ada, create
|
||||
$attendanceDaily = AttendanceHistoryDaily::whereDate('created_at', $currentDate->toDateString())
|
||||
->where('id_student', $student->id)
|
||||
->where('id_class', $student->id_class)
|
||||
->first();
|
||||
|
||||
if ($attendanceDaily) {
|
||||
$attendanceDaily->update([
|
||||
'status' => 'izin',
|
||||
'picture' => '',
|
||||
]);
|
||||
} else {
|
||||
AttendanceHistoryDaily::create([
|
||||
'id_student' => $student->id,
|
||||
'id_class' => $student->id_class,
|
||||
'created_at' => $currentDate,
|
||||
'status' => 'izin',
|
||||
'picture' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
$createdSchoolDays++;
|
||||
$currentDate->addDay();
|
||||
}
|
||||
});
|
||||
|
||||
// notification zone: send notification to related student after permission approved
|
||||
$userStudent = $student->user;
|
||||
if (!$userStudent) {
|
||||
return redirect()->back()->with('error', 'User siswa tidak ditemukan untuk notifikasi');
|
||||
}
|
||||
|
||||
$fcmToken = $userStudent->device_token;
|
||||
$title = 'Perizinan Disetujui';
|
||||
$body = 'Permohonan izin Anda telah disetujui.';
|
||||
|
||||
// Push notification is optional; database notification must still be created.
|
||||
if (!empty($fcmToken)) {
|
||||
try {
|
||||
$template = $this->notificationHelper->templatePermissionApproval($fcmToken);
|
||||
$this->notificationHelper->send($template);
|
||||
$title = $template->title;
|
||||
$body = $template->body;
|
||||
} catch (\Throwable $th) {
|
||||
// Ignore push errors so approval flow and DB notification keep working.
|
||||
}
|
||||
}
|
||||
|
||||
$createTemplate = $this->notificationHelper->createTemplateForStudent(
|
||||
(int) $userStudent->id,
|
||||
$title,
|
||||
$body,
|
||||
NotificationType::Permission,
|
||||
(int) $user->id
|
||||
);
|
||||
|
||||
Notification::create($createTemplate->toArray());
|
||||
|
||||
return redirect()->back()->with('success', 'Izin diterima untuk siswa ' . $student->name);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function permissionReject(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
try {
|
||||
// session-auth user for web controller
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return redirect()->back()->with('error', 'Unauthorized');
|
||||
}
|
||||
|
||||
// find permission by id
|
||||
$permission = Permission::find($id);
|
||||
if (!$permission) {
|
||||
return redirect()->back()->with('error', 'Data perizinan tidak ditemukan');
|
||||
}
|
||||
|
||||
// permission must be processed only, if already accepted or rejected, cannot be processed again.
|
||||
if ($permission->status === 'diterima') {
|
||||
return redirect()->back()->with('warning', 'Perizinan sudah diterima sebelumnya');
|
||||
}
|
||||
if ($permission->status === 'ditolak') {
|
||||
return redirect()->back()->with('warning', 'Perizinan sudah ditolak sebelumnya');
|
||||
}
|
||||
|
||||
$student = Student::find($permission->id_student);
|
||||
if (!$student) {
|
||||
return redirect()->back()->with('error', 'Data siswa tidak ditemukan');
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($permission, $user, $request) {
|
||||
|
||||
// update status to rejected
|
||||
$permission->status = 'ditolak';
|
||||
$permission->approved_by = $user->id; // set approved by teacher id
|
||||
$permission->feedback = $request->input('feedback', null); // optional feedback message from teacher
|
||||
$permission->save();
|
||||
|
||||
});
|
||||
|
||||
// notification zone: send notification to related student after permission rejected
|
||||
|
||||
$userStudent = $student->user;
|
||||
if (!$userStudent) {
|
||||
return redirect()->back()->with('error', 'User siswa tidak ditemukan untuk notifikasi');
|
||||
}
|
||||
|
||||
$fcmToken = $userStudent->device_token;
|
||||
|
||||
// Push notification is optional; database notification must still be created.
|
||||
if (!empty($fcmToken)) {
|
||||
try {
|
||||
$template = $this->notificationHelper->templatePermissionRejection(
|
||||
$fcmToken,
|
||||
$request->input('feedback', null) // optional custom message from teacher feedback
|
||||
);
|
||||
$this->notificationHelper->send($template);
|
||||
$title = $template->title;
|
||||
$body = $template->body;
|
||||
} catch (\Throwable $th) {
|
||||
// Ignore push errors so rejection flow and DB notification keep working.
|
||||
$title = 'Perizinan Ditolak';
|
||||
$body = 'Permohonan izin Anda telah ditolak.';
|
||||
}
|
||||
} else {
|
||||
$title = 'Perizinan Ditolak';
|
||||
$body = 'Permohonan izin Anda telah ditolak.';
|
||||
}
|
||||
|
||||
$createTemplate = $this->notificationHelper->createTemplateForStudent(
|
||||
(int) $userStudent->id,
|
||||
$title,
|
||||
$body,
|
||||
NotificationType::Permission,
|
||||
(int) $user->id
|
||||
);
|
||||
|
||||
Notification::create($createTemplate->toArray());
|
||||
|
||||
return redirect()->back()->with('success', 'Izin ditolak untuk siswa ' . $student->name);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Report;
|
||||
use App\Models\AttendanceHistory;
|
||||
use App\Models\AttendanceHistoryDaily;
|
||||
use App\Models\Student;
|
||||
use App\Models\Notification;
|
||||
use App\Enums\NotificationType;
|
||||
use App\Services\NotificationHelperService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
protected NotificationHelperService $notificationHelper;
|
||||
public $latOfAttendance = -7.7811912;
|
||||
public $lonOfAttendance = 112.0315286;
|
||||
|
||||
public function __construct(NotificationHelperService $notificationHelper)
|
||||
{
|
||||
$this->notificationHelper = $notificationHelper;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$reports = Report::all();
|
||||
return view('pages.laporkan.laporkan', compact('reports'));
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, int $id)
|
||||
{
|
||||
try {
|
||||
// Auth user
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return redirect()->back()->with('error', 'Unauthorized');
|
||||
}
|
||||
|
||||
// Find report by id
|
||||
$report = Report::find($id);
|
||||
if (!$report) {
|
||||
return redirect()->back()->with('error', 'Data laporkan tidak ditemukan');
|
||||
}
|
||||
|
||||
// Validate status
|
||||
$validStatuses = ['hadir', 'izin', 'sakit', 'alpha', 'dispen', 'invalid'];
|
||||
$status = strtolower($request->input('status'));
|
||||
if (!in_array($status, $validStatuses)) {
|
||||
return redirect()->back()->with('error', 'Status tidak valid');
|
||||
}
|
||||
|
||||
// Update attendance history
|
||||
if ($report->id_attendance_history) {
|
||||
AttendanceHistory::where('id', $report->id_attendance_history)->update(['status' => $status]);
|
||||
}
|
||||
|
||||
// Update report marked_as_resolved = 1
|
||||
Report::where('id', $id)->update([
|
||||
'marked_as_resolved' => 1
|
||||
]);
|
||||
|
||||
// notification zone: send notification to related student after status updated
|
||||
$student = Student::find($report->id_student);
|
||||
if ($student) {
|
||||
$userStudent = $student->user;
|
||||
if ($userStudent) {
|
||||
$fcmToken = $userStudent->device_token;
|
||||
$title = 'Laporan Diperbarui';
|
||||
$body = 'Status laporan Anda telah diperbarui menjadi ' . ucfirst($status) . '.';
|
||||
|
||||
// Push notification is optional; database notification must still be created.
|
||||
if (!empty($fcmToken)) {
|
||||
try {
|
||||
$template = $this->notificationHelper->templateAttendanceViolation(
|
||||
$fcmToken,
|
||||
$request->input('description', null) // optional custom message from student about the disrepancy
|
||||
);
|
||||
$notificationResult = $this->notificationHelper->send($template);
|
||||
$createTemplate = $this->notificationHelper->createTemplateForStudent(
|
||||
(int) $userStudent->id,
|
||||
$template->title,
|
||||
$template->body,
|
||||
NotificationType::AttendanceViolation,
|
||||
// user id of system or admin, convert to int. (for example: 0)
|
||||
(int) $user->id
|
||||
);
|
||||
|
||||
Notification::create($createTemplate->toArray());
|
||||
} catch (\Throwable $th) {
|
||||
// Ignore push errors so update flow and DB notification keep working.
|
||||
}
|
||||
}
|
||||
|
||||
$createTemplate = $this->notificationHelper->createTemplateForStudent(
|
||||
(int) $userStudent->id,
|
||||
$title,
|
||||
$body,
|
||||
NotificationType::AttendanceViolation,
|
||||
(int) $user->id
|
||||
);
|
||||
|
||||
Notification::create($createTemplate->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
$message = 'Status laporan berhasil diperbarui menjadi ' . ucfirst($status);
|
||||
return redirect()->back()->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
$errorMessage = 'Terjadi kesalahan: ' . $e->getMessage();
|
||||
return redirect()->back()->with('error', $errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Subject;
|
||||
|
||||
class SubjectController extends Controller
|
||||
{
|
||||
|
||||
public function index ()
|
||||
{
|
||||
$subjects = Subject::all();
|
||||
return view('pages.mapel.mapel', compact('subjects'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:umum,jurusan',
|
||||
'code' => 'required|string|max:50|unique:subjects,code',
|
||||
]);
|
||||
|
||||
Subject::create([
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
return redirect()->route('mapel.index')->with('success', 'Mata pelajaran berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|in:umum,jurusan',
|
||||
'code' => 'required|string|max:50|unique:subjects,code,' . $id,
|
||||
]);
|
||||
|
||||
$subject = Subject::findOrFail($id);
|
||||
$subject->update([
|
||||
'name' => $request->name,
|
||||
'type' => $request->type,
|
||||
'code' => $request->code,
|
||||
]);
|
||||
|
||||
return redirect()->route('mapel.index')->with('success', 'Mata pelajaran berhasil diupdate.');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$subject = Subject::findOrFail($id);
|
||||
$subject->delete();
|
||||
return redirect()->route('mapel.index')->with('success', 'Mata pelajaran berhasil dihapus.');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AttendanceDailyController extends Controller
|
||||
{
|
||||
public function uploadPicture(Request $request)
|
||||
{
|
||||
$storagePath = $request->input('storage_path', 'daily-attendance');
|
||||
|
||||
// Accept both 'image' (from Flask) and 'file' (from other clients)
|
||||
$file = $request->file('image') ?? $request->file('file');
|
||||
|
||||
if (! $file || ! $file->isValid()) {
|
||||
return response()->json(["success" => false, "message" => "No valid file uploaded"], 422);
|
||||
}
|
||||
|
||||
$filename = Str::uuid() . '_' . time() . '.' . $file->getClientOriginalExtension();
|
||||
|
||||
$path = Storage::disk('public')->putFileAs(
|
||||
$storagePath,
|
||||
$file,
|
||||
$filename
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'path' => $path,
|
||||
'url' => asset('storage/' . $path),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Notification;
|
||||
use App\Services\FirebaseMessagingService;
|
||||
use App\Services\NotificationHelperService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class ClassController extends Controller
|
||||
{
|
||||
|
||||
protected FirebaseMessagingService $fcm;
|
||||
protected NotificationHelperService $notificationHelper;
|
||||
|
||||
// Ambil semua data kelas
|
||||
public function index()
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Student classes fetched successfully',
|
||||
'data' => Classes::all()],
|
||||
200);
|
||||
}
|
||||
|
||||
public function __construct(FirebaseMessagingService $fcm, NotificationHelperService $notificationHelper)
|
||||
{
|
||||
$this->fcm = $fcm;
|
||||
$this->notificationHelper = $notificationHelper;
|
||||
}
|
||||
|
||||
|
||||
// Ambil detail kelas berdasarkan id
|
||||
public function show($id)
|
||||
{
|
||||
$class = Classes::find($id);
|
||||
if (!$class) {
|
||||
return response()->json(['message' => 'Class not found'], 404);
|
||||
}
|
||||
return response()->json($class, 200);
|
||||
}
|
||||
|
||||
// get schedule for a specific class
|
||||
public function schedule(Request $request, $id)
|
||||
{
|
||||
// check Authorization header for Bearer token
|
||||
$authHeader = $request->header('Authorization');
|
||||
if (empty($authHeader) || !Str::startsWith($authHeader, 'Bearer ')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$token = trim(Str::after($authHeader, 'Bearer'));
|
||||
$tokenModel = PersonalAccessToken::findToken($token);
|
||||
if (!$tokenModel) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$class = Classes::find($id);
|
||||
if (!$class) {
|
||||
return response()->json(['message' => 'Class not found'], 404);
|
||||
}
|
||||
$schedules = $class->schedules()->whereHas('academicPeriods', function ($q) {
|
||||
$q->where('is_active', 1);
|
||||
})->with(['subject', 'teacher'])->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Class schedules fetched successfully',
|
||||
'data' => $schedules
|
||||
], 200);
|
||||
}
|
||||
|
||||
// get schedule for a specific class for a given day index (1=Senin,..7=Minggu)
|
||||
public function scheduleByDay(Request $request, $id, $dayindex)
|
||||
{
|
||||
// same auth check as schedule()
|
||||
$authHeader = $request->header('Authorization');
|
||||
if (empty($authHeader) || !Str::startsWith($authHeader, 'Bearer ')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$token = trim(Str::after($authHeader, 'Bearer'));
|
||||
$tokenModel = PersonalAccessToken::findToken($token);
|
||||
if (!$tokenModel) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$class = Classes::find($id);
|
||||
if (!$class) {
|
||||
return response()->json(['message' => 'Class not found'], 404);
|
||||
}
|
||||
|
||||
$dayIndex = (int) $dayindex;
|
||||
$dayNames = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
|
||||
$dayName = $dayNames[$dayIndex] ?? null;
|
||||
|
||||
$query = $class->schedules()->whereHas('academicPeriods', function ($q) {
|
||||
$q->where('is_active', 1);
|
||||
});
|
||||
|
||||
if ($dayName) {
|
||||
$query->where(function ($q) use ($dayIndex, $dayName) {
|
||||
$q->where('day_of_week', $dayName)->orWhere('day_of_week', (string) $dayIndex);
|
||||
});
|
||||
} else {
|
||||
$query->where('day_of_week', (string) $dayIndex);
|
||||
}
|
||||
|
||||
$schedules = $query->with(['subject', 'teacher'])->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Class schedules fetched successfully for day',
|
||||
'day_index' => $dayIndex,
|
||||
'data' => $schedules
|
||||
], 200);
|
||||
}
|
||||
|
||||
// teacher only
|
||||
public function studentClassInformation($id)
|
||||
{
|
||||
$class = Classes::where('id', $id)->with(['students'])->first();
|
||||
|
||||
if (!$class) {
|
||||
return response()->json(['message' => 'Class not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Class information fetched successfully',
|
||||
'data' => $class,
|
||||
], 200);
|
||||
}
|
||||
|
||||
// send to class topic (announcement, homework, assignment, etc)
|
||||
public function sendAnnouncementToClass(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
// user auth check
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$userId = $user->id;
|
||||
|
||||
$request->validate([
|
||||
'class_id' => 'required|integer',
|
||||
'type' => 'nullable|string',
|
||||
'title' => 'nullable|string',
|
||||
'body' => 'nullable|string',
|
||||
'data' => 'nullable',
|
||||
]);
|
||||
|
||||
$type = $request->input('type', 'announcement'); // default ke announcement
|
||||
|
||||
$classId = $request->input('class_id'); // contoh: '123'
|
||||
$title = $request->input('title', null);
|
||||
$body = $request->input('body', null);
|
||||
$data = $request->input('data', []);
|
||||
|
||||
// get fcm topic from database based on class_id
|
||||
$class = Classes::find($classId);
|
||||
if (!$class) {
|
||||
return response()->json(['error' => 'Class not found'], 404);
|
||||
}
|
||||
|
||||
$topic = $class->fcm_topic;
|
||||
if (!$topic) {
|
||||
return response()->json(['error' => 'No FCM topic assigned to this class'], 404);
|
||||
}
|
||||
|
||||
$template = null;
|
||||
|
||||
switch ($type) {
|
||||
case 'assignment':
|
||||
$t = $title;
|
||||
$b = $body ?? 'Ada tugas baru untuk kelas ini. Silakan cek aplikasi untuk detail.';
|
||||
$template = $this->notificationHelper->templateAssignmentForClass(
|
||||
$topic,
|
||||
$t,
|
||||
$b,
|
||||
$data,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'class_cancelled':
|
||||
// contoh template untuk pengumuman kelas dibatalkan
|
||||
$t = $title ?? 'Kelas Dibatalkan';
|
||||
$b = $body ?? '(pengirim tidak memberikan detail alasan pembatalan)';
|
||||
$template = $this->notificationHelper->templateClassCancelled(
|
||||
$topic,
|
||||
$b
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
// default ke template pengumuman umum untuk kelas
|
||||
$t = $title ?? 'Pengumuman Kelas';
|
||||
$b = $body ?? 'Ada pengumuman untuk kelas ini.';
|
||||
$template = $this->notificationHelper->templateAnnouncementForClass(
|
||||
$topic,
|
||||
$t,
|
||||
$b,
|
||||
array_merge($data, ['type' => 'announcement_for_class']),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
return response()->json(['error' => 'Failed to create notification template'], 500);
|
||||
}
|
||||
|
||||
$resultMessaging = $this->notificationHelper->send($template);
|
||||
|
||||
$createTemplate = $this->notificationHelper->createTemplateForClass(
|
||||
$classId,
|
||||
$template->title,
|
||||
$template->body,
|
||||
$type,
|
||||
$userId,
|
||||
);
|
||||
|
||||
// store to database
|
||||
Notification::create($createTemplate->toArray());
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Notification sent to class topic successfully',
|
||||
'data' => [
|
||||
'messaging' => $resultMessaging,
|
||||
'create_template' => $createTemplate->toArray(),
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
//throw $th;
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Failed to send notification',
|
||||
'message' => $th->getMessage()], 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AcademicPeriod;
|
||||
use App\Models\Schedule;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class ScheduleController extends Controller
|
||||
{
|
||||
public function scheduleTeacherPersonal(Request $request)
|
||||
{
|
||||
// check Authorization header for Bearer token
|
||||
$authHeader = $request->header('Authorization');
|
||||
if (empty($authHeader) || !Str::startsWith($authHeader, 'Bearer ')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$token = trim(Str::after($authHeader, 'Bearer'));
|
||||
$tokenModel = PersonalAccessToken::findToken($token);
|
||||
if (!$tokenModel) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// get teacher id by token
|
||||
$user = $tokenModel->tokenable;
|
||||
$teacher = $user?->teacher;
|
||||
if (!$teacher) {
|
||||
return response()->json(['success' => false, 'message' => 'Teacher not found'], 404);
|
||||
}
|
||||
$idTeacher = $teacher->id;
|
||||
|
||||
try {
|
||||
// find schedules by teacher_id directly
|
||||
$schedules = Schedule::where('id_teacher', $idTeacher)
|
||||
->whereHas('academicPeriods', function ($q) {
|
||||
$q->where('is_active', 1);
|
||||
})
|
||||
->with(['subject', 'teacher', 'class'])
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Schedules fetched successfully',
|
||||
'data' => $schedules
|
||||
], 200);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('scheduleTeacherPersonal error', ['exception' => $e]);
|
||||
return response()->json(['success' => false, 'message' => 'Server Error', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function scheduleTeacherPersonalByDay(Request $request) {
|
||||
// check Authorization header for Bearer token
|
||||
$authHeader = $request->header('Authorization');
|
||||
if (empty($authHeader) || !Str::startsWith($authHeader, 'Bearer ')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$token = trim(Str::after($authHeader, 'Bearer'));
|
||||
$tokenModel = PersonalAccessToken::findToken($token);
|
||||
if (!$tokenModel) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// get teacher id by token
|
||||
$user = $tokenModel->tokenable;
|
||||
$teacher = $user?->teacher;
|
||||
|
||||
if (!$teacher) {
|
||||
return response()->json(['success' => false, 'message' => 'Teacher not found'], 404);
|
||||
}
|
||||
|
||||
$idTeacher = $teacher->id;
|
||||
|
||||
try {
|
||||
// test only
|
||||
// $now = Carbon::parse('2026-05-09', timezone: 'Asia/Jakarta');
|
||||
// request time
|
||||
$now = Carbon::now('Asia/Jakarta');
|
||||
$dayOfWeek = strtolower($now->locale('id')->dayName); // get day name in Indonesian and convert to lowercase
|
||||
|
||||
// find schedules by teacher_id, where day_of_week matches current day, and academic period is active
|
||||
$schedules = Schedule::where('id_teacher', $idTeacher)
|
||||
->where('day_of_week', $dayOfWeek)
|
||||
->whereHas('academicPeriods', function ($q) {
|
||||
$q->where('is_active', 1);
|
||||
})
|
||||
->with(['subject', 'teacher', 'class'])
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Schedules fetched successfully',
|
||||
'data' => $schedules
|
||||
], 200);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('scheduleTeacherPersonalByDay error', ['exception' => $e]);
|
||||
return response()->json(['success' => false, 'message' => 'Server Error', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// get schedule by class and active academic period, for non personal schedule.
|
||||
public function scheduleByClass(Request $request, $classId)
|
||||
{
|
||||
// validate authorize user
|
||||
$authHeader = $request->header('Authorization');
|
||||
if (empty($authHeader) || !Str::startsWith($authHeader, 'Bearer ')) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$token = trim(Str::after($authHeader, 'Bearer'));
|
||||
$tokenModel = PersonalAccessToken::findToken($token);
|
||||
if (!$tokenModel) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// find schedules by class_id directly
|
||||
|
||||
try {
|
||||
//code...
|
||||
$schedules = Schedule::where('id_class', $classId)
|
||||
->whereHas('academicPeriods', function ($q) {
|
||||
$q->where('is_active', 1);
|
||||
})
|
||||
->with(['subject', 'teacher', 'class'])
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Schedules fetched successfully',
|
||||
'data' => $schedules
|
||||
], 200);
|
||||
} catch (\Throwable $th) {
|
||||
Log::error('scheduleByClass error', ['exception' => $th]);
|
||||
return response()->json(['success' => false, 'message' => 'Server Error', 'error' => $th->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchAcademicPeriodActive()
|
||||
{
|
||||
try {
|
||||
$activePeriod = AcademicPeriod::where('is_active', 1)->first();
|
||||
if (!$activePeriod) {
|
||||
return response()->json(['success' => false, 'message' => 'No active academic period found'], 404);
|
||||
}
|
||||
return response()->json(['success' => true, 'data' => $activePeriod], 200);
|
||||
} catch (\Throwable $th) {
|
||||
Log::error('fetchAcademicPeriodActive error', ['exception' => $th]);
|
||||
return response()->json(['success' => false, 'message' => 'Server Error', 'error' => $th->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Classes;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
|
||||
public function storeFcmToken(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'fcm_token' => 'required|string',
|
||||
// 'platform' => 'nullable|string',
|
||||
// device_id DIHAPUS
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$user->update([
|
||||
'device_token' => $validated['fcm_token'],
|
||||
// 'platform' => $validated['platform'] ?? 'android',
|
||||
// device_id tidak dipakai
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'FCM token stored successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getTopicFromClass(Request $request, $classId)
|
||||
{
|
||||
// request check Authorization header for Bearer token
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$class = Classes::find($classId);
|
||||
|
||||
if (!$class) {
|
||||
return response()->json(['success' => false, 'message' => 'Class not found'], 404);
|
||||
}
|
||||
|
||||
if (!$class->fcm_topic) {
|
||||
return response()->json(['success' => false, 'message' => 'Class does not have an FCM topic'], 400);
|
||||
}
|
||||
|
||||
// Logic untuk subscribe ke topic bisa ditambahkan di sini
|
||||
// Misalnya simpan di database atau langsung panggil service FCM
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "Subscribed to class topic: {$class->fcm_topic}",
|
||||
'data' => [
|
||||
'class_id' => $class->id,
|
||||
'class_name' => $class->name,
|
||||
'fcm_topic' => $class->fcm_topic,
|
||||
]
|
||||
], 200);
|
||||
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = User::where('email', $request->email)->first();
|
||||
|
||||
// cek apakah email user ditemukan
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Akun tidak ditemukan'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// cek apakah password benar
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Email atau password tidak sesuai'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// check if user is banned
|
||||
if ($user->is_banned) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Akun anda telah diblokir. Silakan hubungi administrator untuk informasi lebih lanjut.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
$this->syncTopicSubscribe($user);
|
||||
|
||||
$userData = $user->load('teacher', 'student.class');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Login successful',
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'user' => $userData
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Something went wrong',
|
||||
'error' => $e->getMessage() // bisa dihapus di production untuk keamanan
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function register(Request $request)
|
||||
{
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
//code...
|
||||
$messages = [
|
||||
'name.required' => 'Nama harus diisi',
|
||||
'name.max' => 'Nama maksimal :max karakter',
|
||||
'email.required' => 'Email harus diisi',
|
||||
'email.email' => 'Format email tidak valid',
|
||||
'email.unique' => 'Akun ini sudah terdaftar',
|
||||
'password.required' => 'Password harus diisi',
|
||||
'password.min' => 'Password minimal :min karakter',
|
||||
'role.required' => 'Role harus dipilih',
|
||||
'role.in' => 'Role tidak valid',
|
||||
];
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6',
|
||||
'role' => 'required|in:teacher,student',
|
||||
], $messages);
|
||||
|
||||
// buat akun user
|
||||
$user = User::create([
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'role' => $request->role,
|
||||
'profile_picture' => $request->profile_picture ?? null,
|
||||
]);
|
||||
|
||||
// buat record sesuai role
|
||||
if ($user->role == 'teacher') {
|
||||
$messagesTeacher = [
|
||||
'nip.required' => 'NIP harus diisi',
|
||||
'nip.max' => 'NIP maksimal :max karakter',
|
||||
'nip.unique' => 'NIP sudah pernah dipakai',
|
||||
'subject.required' => 'Mata pelajaran harus diisi',
|
||||
'subject.max' => 'Mata pelajaran maksimal :max karakter',
|
||||
];
|
||||
|
||||
$request->validate([
|
||||
'nip' => 'required|string|max:50|unique:teachers,nip',
|
||||
'subject' => 'required|string|max:100',
|
||||
], $messagesTeacher);
|
||||
|
||||
$user->teacher()->create([
|
||||
'id_user' => $user->id,
|
||||
'name' => $request->name,
|
||||
'nip' => $request->nip,
|
||||
'subject' => $request->subject,
|
||||
]);
|
||||
} else if ($user->role == 'student') {
|
||||
|
||||
$messagesStudent = [
|
||||
'nisn.required' => 'NISN harus diisi',
|
||||
'nisn.max' => 'NISN maksimal :max karakter',
|
||||
'nisn.unique' => 'Akun ini sudah terdaftar',
|
||||
'id_class.required' => 'Kelas harus dipilih',
|
||||
'id_class.integer' => 'ID kelas tidak valid',
|
||||
'entry_year.required' => 'Tahun masuk harus diisi',
|
||||
'entry_year.integer' => 'Tahun masuk tidak valid',
|
||||
];
|
||||
|
||||
$request->validate([
|
||||
'nisn' => 'required|string|max:50|unique:students,nisn',
|
||||
'id_class' => 'required|integer',
|
||||
'entry_year' => 'required|integer',
|
||||
], $messagesStudent);
|
||||
|
||||
$user->student()->create([
|
||||
'id_user' => $user->id,
|
||||
'name' => $request->name,
|
||||
'nisn' => $request->nisn,
|
||||
'id_class' => $request->id_class,
|
||||
'entry_year' => $request->entry_year,
|
||||
'pictures' => $request->pictures ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->syncTopicSubscribe($user);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$token = $user->createToken('auth_token')->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Anda berhasil register',
|
||||
'access_token' => $token,
|
||||
'token_type' => 'Bearer',
|
||||
'user' => $user->load('teacher', 'student')
|
||||
], 201);
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error',
|
||||
'errors' => $e->errors(), // detail error validasi
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Registration failed',
|
||||
'error' => $e->getMessage(), // optional, bisa di-hide kalau production
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function feedback(Request $request)
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'Halo student, ini feedback testing!',
|
||||
'user' => $request->user()->only(['id', 'name', 'email', 'role']),
|
||||
]);
|
||||
}
|
||||
|
||||
private function syncTopicSubscribe(User $user): ?string
|
||||
{
|
||||
$user->loadMissing('student.class');
|
||||
|
||||
if (!$user->student) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$topics = ['student_notifications'];
|
||||
|
||||
if ($topic = $user->student->class?->fcm_topic) {
|
||||
$topics[] = $topic;
|
||||
}
|
||||
|
||||
$topics = array_unique(array_filter($topics));
|
||||
|
||||
if (empty($topics)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$topicSubscribe = implode(', ', $topics);
|
||||
|
||||
if ($user->topic_subscribe !== $topicSubscribe) {
|
||||
$user->update(['topic_subscribe' => $topicSubscribe]);
|
||||
}
|
||||
|
||||
$user->topic_subscribe = $topicSubscribe;
|
||||
|
||||
return $topicSubscribe;
|
||||
}
|
||||
|
||||
// get student notification list data from notification table.
|
||||
// note filter: user_id = auth user id, or his class id.
|
||||
public function getStudentNotifications(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$student = $user->student;
|
||||
|
||||
if (!$user || !$student) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// validate query parameters
|
||||
$request->validate([
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date',
|
||||
]);
|
||||
|
||||
$classId = $user->student->id_class;
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = Carbon::parse($request->input('end_date'))->endOfDay();
|
||||
|
||||
// filter by class id and date range
|
||||
$notifications = DB::table('notifications')
|
||||
->where(function ($query) use ($user, $classId) {
|
||||
$query->where('receiver_id', $user->id)
|
||||
->orWhere('class_id', $classId)
|
||||
->orWhere(function ($q) {
|
||||
$q->whereNull('receiver_id')
|
||||
->whereNull('class_id');
|
||||
});
|
||||
})
|
||||
|
||||
->whereBetween('created_at', [$startDate, $endDate])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Notifications fetched successfully',
|
||||
'data' => $notifications,
|
||||
], 200);
|
||||
}
|
||||
|
||||
// get activity teacher history from notification table with related sender_id with the user.
|
||||
public function getTeacherActivity(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user || !$user->teacher) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// validate query parameters
|
||||
$request->validate([
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date',
|
||||
]);
|
||||
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = Carbon::parse($request->input('end_date'))->endOfDay();
|
||||
|
||||
// filter by sender_id and date range
|
||||
$activities = DB::table('notifications')
|
||||
->where('sender_id', $user->id)
|
||||
->whereBetween('created_at', [$startDate, $endDate])
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Activities fetched successfully',
|
||||
'data' => $activities,
|
||||
], 200);
|
||||
}
|
||||
|
||||
// update profile user, for all (teacher, student, admin)
|
||||
public function updateProfilePhoto(Request $request)
|
||||
{
|
||||
|
||||
// auth check
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// check user role for separated storage path, for example: profile_pictures/teacher, profile_pictures/student, etc.
|
||||
$rolePath = match ($user->role) {
|
||||
'teacher' => 'teacher',
|
||||
'student' => 'student',
|
||||
'admin' => 'admin',
|
||||
default => 'others',
|
||||
};
|
||||
|
||||
// input file check using request validation
|
||||
// nullable|file|mimes:jpg,jpeg,png|max:2048',
|
||||
$request->validate([
|
||||
'profile_picture' => 'required|file|mimes:jpg,jpeg,png|max:2048',
|
||||
]);
|
||||
|
||||
try {
|
||||
$file = $request->file('profile_picture');
|
||||
|
||||
// store to storage:disk public, with unique name
|
||||
$fileName = 'profile_' . $user->id . '_' . time() . '.' . $file->getClientOriginalExtension();
|
||||
|
||||
Storage::disk('public')->putFileAs('profile_pictures/' . $rolePath, $file, $fileName);
|
||||
|
||||
// update user profile picture path
|
||||
$user->update(['profile_picture' => $fileName]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profile picture updated successfully',
|
||||
'data' => [
|
||||
'profile_picture' => $fileName,
|
||||
'profile_picture_url' => asset('storage/profile_pictures/' . $rolePath . '/' . $fileName),
|
||||
]
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to update profile picture',
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RoleMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, $role): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthenticated',
|
||||
'errors' => 'Authentication required',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if ($user->role !== $role) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'you cannot continue this request cause role mismatch',
|
||||
'errors' => 'Unauthorized. Role mismatch'], 403);
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'register/*',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\AttendanceHistory;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WriteAttendanceJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public array $data;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Determine attendance time (prefer explicit attendance_time, then created_at, then now)
|
||||
$attendanceTime = null;
|
||||
if (!empty($this->data['attendance_time'])) {
|
||||
$attendanceTime = Carbon::parse($this->data['attendance_time']);
|
||||
} elseif (!empty($this->data['created_at'])) {
|
||||
$attendanceTime = Carbon::parse($this->data['created_at']);
|
||||
} else {
|
||||
$attendanceTime = Carbon::now();
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'id_student' => $this->data['id_student'],
|
||||
'id_schedule' => $this->data['id_schedule'],
|
||||
'attendance_date' => $attendanceTime->toDateString(),
|
||||
];
|
||||
|
||||
$values = array_merge($attributes, [
|
||||
'id_class' => $this->data['id_class'] ?? null,
|
||||
'status' => $this->data['status'] ?? 'hadir',
|
||||
'coordinate' => $this->data['coordinate'] ?? null,
|
||||
'period_number' => $this->data['period_number'] ?? null,
|
||||
'attendance_time' => $attendanceTime->toDateTimeString(),
|
||||
'created_at' => $this->data['created_at'] ?? $attendanceTime->toDateTimeString(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// firstOrCreate prevents duplicate rows and is idempotent.
|
||||
$attendance = AttendanceHistory::firstOrCreate($attributes, $values);
|
||||
|
||||
Log::info('WriteAttendanceJob: attendance written', ['id' => $attendance->id ?? null, 'attrs' => $attributes]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('WriteAttendanceJob failed', ['error' => $e->getMessage(), 'data' => $this->data]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SendResetPasswordLink extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $email;
|
||||
public $token;
|
||||
public $resetLink;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($email, $token)
|
||||
{
|
||||
$this->email = $email;
|
||||
$this->token = $token;
|
||||
// Generate link ke verify-token page dengan email dan token
|
||||
$this->resetLink = route('password.verify.form', ['email' => $email]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Reset Password SAS SMK Taruna Bakti Kertosono',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.reset-password',
|
||||
with: [
|
||||
'email' => $this->email,
|
||||
'token' => $this->token,
|
||||
'resetLink' => $this->resetLink,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AcademicPeriod extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'academic_periods';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'is_active',
|
||||
// 'semester', // Uncomment if you add semester column
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Student;
|
||||
use App\Models\Schedule;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class AttendanceHistory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'attendance_histories';
|
||||
|
||||
protected $fillable = ['period_number', 'status', 'id_student', 'id_schedule', 'coordinates', 'attendance_date', 'id_class', 'coordinate'];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'id_student');
|
||||
}
|
||||
|
||||
public function schedule()
|
||||
{
|
||||
return $this->belongsTo(Schedule::class, 'id_schedule');
|
||||
}
|
||||
|
||||
public function class()
|
||||
{
|
||||
return $this->belongsTo(Classes::class, 'id_class');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AttendanceHistoryDaily extends Model
|
||||
{
|
||||
protected $table = 'attendance_history_dailys';
|
||||
|
||||
protected $fillable = [
|
||||
'status',
|
||||
'picture',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'id_student',
|
||||
'id_class',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'id_student');
|
||||
}
|
||||
|
||||
public function class()
|
||||
{
|
||||
return $this->belongsTo(Classes::class, 'id_class');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Student;
|
||||
|
||||
class Classes extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'clases';
|
||||
|
||||
protected $fillable = ['name', 'major', 'grade', 'code', 'fcm_topic'];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function schedules()
|
||||
{
|
||||
return $this->hasMany(Schedule::class, 'id_class');
|
||||
}
|
||||
|
||||
public function students()
|
||||
{
|
||||
return $this->hasMany(Student::class, 'id_class');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
// table name
|
||||
protected $table = 'notifications';
|
||||
|
||||
protected $fillable = ["title", "body", "type", "send_to", "sender_id", "receiver_id", "class_id"];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function sender(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'sender_id');
|
||||
}
|
||||
|
||||
public function receiver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'receiver_id');
|
||||
}
|
||||
|
||||
public function class(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Classes::class, 'class_id');
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($notif) {
|
||||
try {
|
||||
$adminPhone = '62' . ltrim(env('FONNTE_PHONE'), '0');
|
||||
|
||||
Http::withHeaders([
|
||||
'Authorization' => env('FONNTE_TOKEN'),
|
||||
])->post('https://api.fonnte.com/send', [
|
||||
'phone' => $adminPhone,
|
||||
'message' =>
|
||||
"🔔 NOTIFIKASI BARU\n\n" .
|
||||
"Judul: {$notif->title}\n" .
|
||||
"Pesan: {$notif->body}",
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('WA Error: ' . $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'permissions';
|
||||
|
||||
protected $fillable = ['reason', 'information', 'evidence', 'status', 'feedback', 'date_permission', 'time_period', 'id_student', 'approved_by'];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
// relationship foreign key
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'id_student');
|
||||
}
|
||||
|
||||
public static function findById($id)
|
||||
{
|
||||
return self::with('student')->findOrFail($id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Report extends Model
|
||||
{
|
||||
protected $table = 'table_report_disrepancy';
|
||||
|
||||
protected $fillable = [
|
||||
'id_student',
|
||||
'student_name',
|
||||
'reported_by',
|
||||
'id_attendance_history',
|
||||
'id_class',
|
||||
'disrepancy_type',
|
||||
'description',
|
||||
'marked_as_resolved',
|
||||
'attendance_date',
|
||||
];
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'id_student');
|
||||
}
|
||||
|
||||
public function reporter()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reported_by');
|
||||
}
|
||||
|
||||
public function attendanceHistory()
|
||||
{
|
||||
return $this->belongsTo(AttendanceHistory::class, 'id_attendance_history');
|
||||
}
|
||||
|
||||
public function class()
|
||||
{
|
||||
return $this->belongsTo(Classes::class, 'id_class');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\AcademicPeriod;
|
||||
use App\Models\Classes;
|
||||
use App\Models\Subject;
|
||||
use App\Models\Teacher;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Schedule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'schedules';
|
||||
|
||||
protected $fillable = ['day_of_week', 'period_start', 'period_end', 'start_time', 'end_time', 'code', 'id_class', 'id_subject', 'id_teacher', 'id_academic_periods'];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function class()
|
||||
{
|
||||
return $this->belongsTo(Classes::class, 'id_class');
|
||||
}
|
||||
public function subject()
|
||||
{
|
||||
return $this->belongsTo(Subject::class, 'id_subject');
|
||||
}
|
||||
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(Teacher::class, 'id_teacher');
|
||||
}
|
||||
|
||||
public function academicPeriods()
|
||||
{
|
||||
return $this->belongsTo(AcademicPeriod::class, 'id_academic_periods');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Classes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class Student extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'students';
|
||||
|
||||
protected $fillable = ['id_user', 'id_class', 'name', 'nisn', 'entry_year', 'pictures'];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'id_user');
|
||||
}
|
||||
|
||||
public function class()
|
||||
{
|
||||
return $this->belongsTo(Classes::class, 'id_class');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class Subject extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'subjects';
|
||||
|
||||
protected $fillable = ['name', 'type', 'code'];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(Teacher::class, 'id_teacher');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Subject;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class Teacher extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
// table name
|
||||
protected $table = 'teachers';
|
||||
|
||||
protected $fillable = [
|
||||
'id_user',
|
||||
'name',
|
||||
'nip',
|
||||
'subject', /// kode pramudya.
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'id_user');
|
||||
}
|
||||
|
||||
public static function findById($id)
|
||||
{
|
||||
return self::with('user')->findOrFail($id);
|
||||
}
|
||||
|
||||
/// kode pramudya
|
||||
public function getSubjectObjectsAttribute()
|
||||
{
|
||||
$subjectField = $this->subject ?? ''; // stored as CSV subject codes like "MTK, IPA"
|
||||
$names = array_filter(array_map('trim', explode(',', $subjectField)));
|
||||
if (empty($names)) {
|
||||
return collect();
|
||||
}
|
||||
// Search by subject.code and return Subject models so we can show their names
|
||||
return Subject::whereIn('code', $names)->get();
|
||||
}
|
||||
|
||||
public function subjects()
|
||||
{
|
||||
return $this->hasMany(Subject::class, 'id_teacher');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Models\Student;
|
||||
use App\Models\Teacher;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'password',
|
||||
'role',
|
||||
'is_banned',
|
||||
'device_token',
|
||||
'device_id',
|
||||
'topic_subscribe',
|
||||
'profile_picture',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function teacher()
|
||||
{
|
||||
return $this->hasOne(Teacher::class, 'id_user');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->hasOne(Student::class, 'id_user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Nama tampilan: ambil dari relasi teacher/student, fallback ke email.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
if ($this->relationLoaded('teacher') ? $this->teacher : $this->teacher()->first()) {
|
||||
return $this->teacher->name;
|
||||
}
|
||||
if ($this->relationLoaded('student') ? $this->student : $this->student()->first()) {
|
||||
return $this->student->name;
|
||||
}
|
||||
return $this->email ?? 'Pengguna';
|
||||
}
|
||||
|
||||
/**
|
||||
* Label role dalam Bahasa Indonesia.
|
||||
*/
|
||||
public function getRoleLabelAttribute(): string
|
||||
{
|
||||
return match ($this->role) {
|
||||
'admin' => 'Admin',
|
||||
'teacher' => 'Guru',
|
||||
'student' => 'Siswa',
|
||||
default => ucfirst((string) $this->role),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Jobs\SendTelegramNotificationJob;
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(ClientInterface::class, Client::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
config(['app.locale' => 'id']);
|
||||
Carbon::setLocale('id');
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Kreait\Firebase\Messaging;
|
||||
use Kreait\Firebase\Messaging\CloudMessage;
|
||||
use Kreait\Firebase\Messaging\Notification;
|
||||
|
||||
class FirebaseMessagingService
|
||||
{
|
||||
protected Messaging $messaging;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->messaging = app('firebase.messaging');
|
||||
}
|
||||
|
||||
public function sendToToken(string $token, string $title, string $body, array $data = [])
|
||||
{
|
||||
$message = CloudMessage::withTarget('token', $token)
|
||||
->withNotification(Notification::create($title, $body))
|
||||
->withData($data);
|
||||
|
||||
return $this->messaging->send($message);
|
||||
}
|
||||
|
||||
public function sendToTopic(string $topic, string $title, string $body, array $data = [])
|
||||
{
|
||||
$message = CloudMessage::withTarget('topic', $topic)
|
||||
->withNotification(Notification::create($title, $body))
|
||||
->withData($data);
|
||||
|
||||
return $this->messaging->send($message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class FonteService
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Data\NotificationCreateTemplate;
|
||||
use App\Data\NotificationTemplate;
|
||||
use App\Enums\NotificationType;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class NotificationHelperService
|
||||
{
|
||||
public function __construct(protected FirebaseMessagingService $fcm)
|
||||
{
|
||||
}
|
||||
|
||||
public function template(array $attributes = []): NotificationTemplate
|
||||
{
|
||||
return NotificationTemplate::fromArray($attributes);
|
||||
}
|
||||
|
||||
public function createTemplateForAll(
|
||||
string $title,
|
||||
string $body,
|
||||
NotificationType|string $type,
|
||||
?int $senderId = null,
|
||||
): NotificationCreateTemplate {
|
||||
return NotificationCreateTemplate::forAll($title, $body, $type, $senderId);
|
||||
}
|
||||
|
||||
public function createTemplateForClass(
|
||||
int $classId,
|
||||
string $title,
|
||||
string $body,
|
||||
NotificationType|string $type,
|
||||
?int $senderId = null,
|
||||
): NotificationCreateTemplate {
|
||||
return NotificationCreateTemplate::forClass($classId, $title, $body, $type, $senderId);
|
||||
}
|
||||
|
||||
public function createTemplateForStudent(
|
||||
int $receiverId, // user id siswa
|
||||
string $title,
|
||||
string $body,
|
||||
NotificationType|string $type,
|
||||
?int $senderId = null,
|
||||
): NotificationCreateTemplate {
|
||||
return NotificationCreateTemplate::forStudent($receiverId, $title, $body, $type, $senderId);
|
||||
}
|
||||
|
||||
public function tokenTemplate(string $token, string $title, string $body, array $data = []): NotificationTemplate
|
||||
{
|
||||
return NotificationTemplate::forToken($token, $title, $body, $data);
|
||||
}
|
||||
|
||||
public function topicTemplate(string $topic, string $title, string $body, array $data = []): NotificationTemplate
|
||||
{
|
||||
return NotificationTemplate::forTopic($topic, $title, $body, $data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// template messaging general.
|
||||
public function templateAnnouncementForAll(string $judul, string $isi, string $topic = 'student_notifications', array $data = []): NotificationTemplate
|
||||
{
|
||||
return $this->topicTemplate(
|
||||
$topic,
|
||||
$judul,
|
||||
$isi,
|
||||
array_merge([
|
||||
'type' => 'announcement',
|
||||
'level' => 'all',
|
||||
], $data),
|
||||
);
|
||||
}
|
||||
|
||||
public function templateAnnouncementForClass(string $classTopic, string $judul, string $isi, array $data = []): NotificationTemplate
|
||||
{
|
||||
return $this->topicTemplate(
|
||||
$classTopic,
|
||||
$judul,
|
||||
$isi,
|
||||
array_merge([
|
||||
'type' => 'announcement',
|
||||
'level' => 'class',
|
||||
], $data),
|
||||
);
|
||||
}
|
||||
|
||||
public function templateAssignmentForClass(string $classTopic, string $judul = null, string $isi, array $data = []): NotificationTemplate
|
||||
{
|
||||
return $this->topicTemplate(
|
||||
$classTopic,
|
||||
$judul ?? 'Tugas Baru',
|
||||
$isi,
|
||||
array_merge([
|
||||
'type' => 'assignment',
|
||||
'level' => 'class',
|
||||
], $data),
|
||||
);
|
||||
}
|
||||
|
||||
public function templateTokenPersonal(string $token, string $judul, string $isi, array $data = []): NotificationTemplate
|
||||
{
|
||||
return $this->tokenTemplate(
|
||||
$token,
|
||||
$judul,
|
||||
$isi,
|
||||
array_merge([
|
||||
'level' => 'student',
|
||||
], $data),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// template messaging based on rules template data.
|
||||
public function templateAnnouncementAcademic($message): NotificationTemplate{
|
||||
return $this->topicTemplate(
|
||||
'student_notifications',
|
||||
'Pengumuman Akademik',
|
||||
$message,
|
||||
[
|
||||
'type' => 'announcement_academic',
|
||||
'level' => 'all',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templateAnnouncementGeneral($message): NotificationTemplate{
|
||||
return $this->topicTemplate(
|
||||
'student_notifications',
|
||||
'Pengumuman Umum',
|
||||
$message,
|
||||
[
|
||||
'type' => 'announcement_general',
|
||||
'level' => 'all',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templateLostAndFound($message): NotificationTemplate{
|
||||
return $this->topicTemplate(
|
||||
'student_notifications',
|
||||
'Info Kehilangan dan Penemuan',
|
||||
$message,
|
||||
[
|
||||
'type' => 'lost_and_found',
|
||||
'level' => 'all',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templateEmergencyInfo($message): NotificationTemplate{
|
||||
return $this->topicTemplate(
|
||||
'student_notifications',
|
||||
'Info Darurat',
|
||||
$message,
|
||||
[
|
||||
'type' => 'emergency_info',
|
||||
'level' => 'all',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templateClassCancelled($classTopic, $message): NotificationTemplate{
|
||||
return $this->topicTemplate(
|
||||
$classTopic,
|
||||
'Kelas Dibatalkan',
|
||||
$message,
|
||||
[
|
||||
'type' => 'class_cancelled',
|
||||
'level' => 'all',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templatePermissionRequest($studentTopic, $message): NotificationTemplate{
|
||||
return $this->tokenTemplate(
|
||||
$studentTopic,
|
||||
'Permohonan Izin',
|
||||
$message,
|
||||
[
|
||||
'type' => 'permission',
|
||||
'level' => 'student',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templatePermissionApproval($studentTopic, $message = null): NotificationTemplate{
|
||||
return $this->tokenTemplate(
|
||||
$studentTopic,
|
||||
'Perizinan Disetujui',
|
||||
$message ?? 'Permohonan izin Anda telah disetujui.',
|
||||
[
|
||||
'type' => 'permission_approval',
|
||||
'level' => 'student',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templatePermissionRejection($studentTopic, $message = null): NotificationTemplate{
|
||||
return $this->tokenTemplate(
|
||||
$studentTopic,
|
||||
'Perizinan Ditolak',
|
||||
$message ?? 'Permohonan izin Anda telah ditolak.',
|
||||
[
|
||||
'type' => 'permission_rejection',
|
||||
'level' => 'student',
|
||||
],);
|
||||
}
|
||||
|
||||
public function templateAttendanceViolation($studentTopic, $message = null): NotificationTemplate{
|
||||
return $this->tokenTemplate(
|
||||
$studentTopic,
|
||||
'Anda Melanggar Aturan Absensi',
|
||||
$message ?? 'Anda telah melanggar aturan absensi.',
|
||||
[
|
||||
'type' => 'attendance_violation',
|
||||
'level' => 'student',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function templatePersonalNote($studentTopic, $message): NotificationTemplate{
|
||||
return $this->tokenTemplate(
|
||||
$studentTopic,
|
||||
'Catatan Pribadi',
|
||||
$message,
|
||||
[
|
||||
'type' => 'personal_note',
|
||||
'level' => 'student',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function excelTemplate(): array
|
||||
{
|
||||
return [
|
||||
'columns' => NotificationTemplate::excelColumns(),
|
||||
'aliases' => NotificationTemplate::excelAliases(),
|
||||
'example' => NotificationTemplate::example(),
|
||||
'topic_example' => NotificationTemplate::topicExample(),
|
||||
];
|
||||
}
|
||||
|
||||
public function send(NotificationTemplate $template): mixed
|
||||
{
|
||||
if (!$template->target()) {
|
||||
throw new \InvalidArgumentException('Notification target is required.');
|
||||
}
|
||||
|
||||
if ($template->isTopic()) {
|
||||
return $this->fcm->sendToTopic(
|
||||
$template->target(),
|
||||
$template->title,
|
||||
$template->body,
|
||||
$template->data,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->fcm->sendToToken(
|
||||
$template->target(),
|
||||
$template->title,
|
||||
$template->body,
|
||||
$template->data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an uploaded Excel file (or path) and return an array of NotificationTemplate objects.
|
||||
*
|
||||
* Implementation TODO: use PhpSpreadsheet or similar to read the Excel file
|
||||
* and map columns to NotificationTemplate using the aliases defined there.
|
||||
*
|
||||
* @param UploadedFile|string $file
|
||||
* @return array<int, NotificationTemplate>
|
||||
* @throws \RuntimeException when not implemented
|
||||
*/
|
||||
public function parseExcelNotifications(UploadedFile|string $file): array
|
||||
{
|
||||
// TODO: implement parsing logic
|
||||
throw new \RuntimeException('NotificationHelperService::parseExcelNotifications not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a normalized notification template from a row array.
|
||||
*
|
||||
* @param array $row
|
||||
* @return NotificationTemplate
|
||||
*/
|
||||
public function buildPayload(array $row): NotificationTemplate
|
||||
{
|
||||
return $this->template($row);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Schedule;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class QrLinkService
|
||||
{
|
||||
/**
|
||||
* Generate a public QR link for a schedule
|
||||
*
|
||||
* @param int $scheduleId
|
||||
* @return array
|
||||
*/
|
||||
public function generateLink($scheduleId)
|
||||
{
|
||||
// Verify schedule exists
|
||||
$schedule = Schedule::findOrFail($scheduleId);
|
||||
|
||||
// Generate random token
|
||||
$token = Str::random(64);
|
||||
|
||||
// Create expiration time (10 minutes from now)
|
||||
$expiresAt = now()->addSeconds(600);
|
||||
|
||||
// Cache the schedule ID and expiration time
|
||||
Cache::put("public_qr_link:$token", [
|
||||
'schedule_id' => $scheduleId,
|
||||
'expires_at' => $expiresAt->toIso8601String()
|
||||
], $expiresAt);
|
||||
|
||||
// Generate the public URL
|
||||
$url = route('jadwal.qr.public', ['token' => $token]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'QR link generated',
|
||||
'data' => [
|
||||
'url' => $url,
|
||||
'token' => $token,
|
||||
'schedule_id' => $scheduleId,
|
||||
'expired_in_seconds' => 600,
|
||||
'expired_in_minutes' => 10
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the schedule ID and expiration from a token
|
||||
*
|
||||
* @param string $token
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTokenData($token)
|
||||
{
|
||||
return Cache::get("public_qr_link:$token");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate a token
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function invalidateToken($token)
|
||||
{
|
||||
Cache::forget("public_qr_link:$token");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
|
||||
trait ExcelExportTrait
|
||||
{
|
||||
/**
|
||||
* Export data ke file Excel dengan styling standar
|
||||
*
|
||||
* @param array $headers - Array key:label untuk headers ['name'=>'Nama', 'email'=>'Email']
|
||||
* @param array $data - Array data yang akan diekspor
|
||||
* @param string $filename - Nama file (tanpa .xlsx)
|
||||
* @param array $options - Option: ['sheetName', 'headerColor', 'alternateRowColor', 'title', 'subtitle']
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
*/
|
||||
public function exportToExcel($headers, $data, $filename = 'export', $options = [])
|
||||
{
|
||||
// Extract options
|
||||
$sheetName = $options['sheetName'] ?? 'Data';
|
||||
$headerColor = $options['headerColor'] ?? '365CF5';
|
||||
$alternateRowColor = $options['alternateRowColor'] ?? true;
|
||||
$title = $options['title'] ?? null;
|
||||
$subtitle = $options['subtitle'] ?? null;
|
||||
|
||||
// Create spreadsheet
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle($sheetName);
|
||||
|
||||
$currentRow = 1;
|
||||
|
||||
// Add title if provided
|
||||
if ($title) {
|
||||
$sheet->setCellValue('A' . $currentRow, $title);
|
||||
$sheet->mergeCells('A' . $currentRow . ':' . $this->getColumnLetter(count($headers)) . $currentRow);
|
||||
$sheet->getRowDimension($currentRow)->setRowHeight(24);
|
||||
$sheet->getStyle('A' . $currentRow)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 14, 'color' => ['rgb' => '1F2937']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$currentRow++;
|
||||
}
|
||||
|
||||
// Add subtitle if provided
|
||||
if ($subtitle) {
|
||||
$sheet->setCellValue('A' . $currentRow, $subtitle);
|
||||
$sheet->mergeCells('A' . $currentRow . ':' . $this->getColumnLetter(count($headers)) . $currentRow);
|
||||
$sheet->getRowDimension($currentRow)->setRowHeight(18);
|
||||
$sheet->getStyle('A' . $currentRow)->applyFromArray([
|
||||
'font' => ['size' => 10, 'color' => ['rgb' => '6B7280']],
|
||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||
]);
|
||||
$currentRow++;
|
||||
}
|
||||
|
||||
// Add headers
|
||||
$headerRow = $currentRow;
|
||||
foreach ($headers as $key => $header) {
|
||||
$sheet->setCellValue($this->getColumnLetter(count(array_slice($headers, 0, array_search($key, array_keys($headers)))) + 1) . $headerRow, $header);
|
||||
}
|
||||
|
||||
// Better way to add headers
|
||||
foreach (array_values($headers) as $colIndex => $header) {
|
||||
$sheet->setCellValue($this->getColumnLetter($colIndex + 1) . $headerRow, $header);
|
||||
}
|
||||
|
||||
// Style headers
|
||||
$headerRange = 'A' . $headerRow . ':' . $this->getColumnLetter(count($headers)) . $headerRow;
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => $headerColor]],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
'wrapText' => true,
|
||||
],
|
||||
]);
|
||||
$sheet->getRowDimension($headerRow)->setRowHeight(20);
|
||||
|
||||
// Add data rows
|
||||
$dataStartRow = $headerRow + 1;
|
||||
foreach ($data as $index => $row) {
|
||||
$rowNum = $dataStartRow + $index;
|
||||
foreach (array_keys($headers) as $colIndex => $key) {
|
||||
$value = $this->getNestedValue($row, $key);
|
||||
$sheet->setCellValue($this->getColumnLetter($colIndex + 1) . $rowNum, $value);
|
||||
}
|
||||
|
||||
// Alternate row color
|
||||
if ($alternateRowColor && (($index % 2) === 1)) {
|
||||
$sheet->getStyle('A' . $rowNum . ':' . $this->getColumnLetter(count($headers)) . $rowNum)->applyFromArray([
|
||||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F4F7FF']],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get highest row
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
|
||||
// Apply borders
|
||||
$tableRange = 'A' . $headerRow . ':' . $this->getColumnLetter(count($headers)) . $highestRow;
|
||||
$sheet->getStyle($tableRange)->applyFromArray([
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => 'D1D5DB'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Auto-size columns
|
||||
foreach (range(1, count($headers)) as $col) {
|
||||
$sheet->getColumnDimension($this->getColumnLetter($col))->setAutoSize(true);
|
||||
}
|
||||
|
||||
// Freeze header
|
||||
if ($title && $subtitle) {
|
||||
$sheet->freezePane('A' . ($headerRow + 1));
|
||||
} elseif ($title || $subtitle) {
|
||||
$sheet->freezePane('A' . $headerRow);
|
||||
}
|
||||
|
||||
return $this->downloadExcel($spreadsheet, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export dengan PhpSpreadsheet Advanced (untuk custom styling)
|
||||
*/
|
||||
public function exportToExcelAdvanced($headers, $data, $filename = 'export', $options = [])
|
||||
{
|
||||
return $this->exportToExcel($headers, $data, $filename, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert column number to letter (1->A, 2->B, etc)
|
||||
*/
|
||||
private function getColumnLetter($col)
|
||||
{
|
||||
$letter = '';
|
||||
while ($col > 0) {
|
||||
$col--;
|
||||
$letter = chr(65 + ($col % 26)) . $letter;
|
||||
$col = intdiv($col, 26);
|
||||
}
|
||||
return $letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested value from array or object (support dot notation)
|
||||
* Example: getNestedValue($item, 'user.name')
|
||||
*/
|
||||
private function getNestedValue($data, $key)
|
||||
{
|
||||
// Simple key
|
||||
if (is_array($data) && isset($data[$key])) {
|
||||
return $data[$key];
|
||||
}
|
||||
|
||||
if (is_object($data) && property_exists($data, $key)) {
|
||||
return $data->$key;
|
||||
}
|
||||
|
||||
// Dot notation
|
||||
$keys = explode('.', $key);
|
||||
$value = $data;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (is_array($value) && isset($value[$k])) {
|
||||
$value = $value[$k];
|
||||
} elseif (is_object($value) && property_exists($value, $k)) {
|
||||
$value = $value->$k;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download Excel file
|
||||
*/
|
||||
private function downloadExcel($spreadsheet, $filename)
|
||||
{
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
|
||||
// Save to temp file
|
||||
$tempPath = storage_path('app/temp/' . $filename . '.xlsx');
|
||||
@mkdir(dirname($tempPath), 0755, true);
|
||||
$writer->save($tempPath);
|
||||
|
||||
// Download and delete
|
||||
return response()->download($tempPath)->deleteFileAfterSend(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Middleware\RoleMiddleware;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->alias([
|
||||
'role' => RoleMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->render(function (AuthenticationException $e, Request $request) {
|
||||
if ($request->is('api/*') || $request->expectsJson()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthenticated',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
})->create();
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"intervention/image": "3.7",
|
||||
"kreait/laravel-firebase": "^6.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"phpoffice/phpspreadsheet": "^5.6",
|
||||
"predis/predis": "^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"pestphp/pest": "^3.8",
|
||||
"pestphp/pest-plugin-laravel": "^3.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
},
|
||||
"files": [
|
||||
"app/Helpers/LocationHelper.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"platform-check": false,
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'Asia/Jakarta',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => 'id',
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'id'),
|
||||
|
||||
'faker_locale' => 'id_ID',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'coordinate' => '-7.604032330848524, 112.10176449791652',
|
||||
'max_distance_tolerance' => 1000, // in meters
|
||||
];
|
||||
|
||||
?>
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Default Firebase project
|
||||
* ------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'default' => env('FIREBASE_PROJECT', 'app'),
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Firebase project configurations
|
||||
* ------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'projects' => [
|
||||
'app' => [
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Credentials / Service Account
|
||||
* ------------------------------------------------------------------------
|
||||
*
|
||||
* In order to access a Firebase project and its related services using a
|
||||
* server SDK, requests must be authenticated. For server-to-server
|
||||
* communication this is done with a Service Account.
|
||||
*
|
||||
* If you don't already have generated a Service Account, you can do so by
|
||||
* following the instructions from the official documentation pages at
|
||||
*
|
||||
* https://firebase.google.com/docs/admin/setup#initialize_the_sdk
|
||||
*
|
||||
* Once you have downloaded the Service Account JSON file, you can use it
|
||||
* to configure the package.
|
||||
*
|
||||
* If you don't provide credentials, the Firebase Admin SDK will try to
|
||||
* auto-discover them
|
||||
*
|
||||
* - by checking the environment variable FIREBASE_CREDENTIALS
|
||||
* - by checking the environment variable GOOGLE_APPLICATION_CREDENTIALS
|
||||
* - by trying to find Google's well known file
|
||||
* - by checking if the application is running on GCE/GCP
|
||||
*
|
||||
* If no credentials file can be found, an exception will be thrown the
|
||||
* first time you try to access a component of the Firebase Admin SDK.
|
||||
*
|
||||
*/
|
||||
|
||||
'credentials' => [
|
||||
'file' => env('FIREBASE_CREDENTIALS'),
|
||||
],
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Firebase Auth Component
|
||||
* ------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'auth' => [
|
||||
'tenant_id' => env('FIREBASE_AUTH_TENANT_ID'),
|
||||
],
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Firestore Component
|
||||
* ------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'firestore' => [
|
||||
|
||||
/*
|
||||
* If you want to access a Firestore database other than the default database,
|
||||
* enter its name here.
|
||||
*
|
||||
* By default, the Firestore client will connect to the `(default)` database.
|
||||
*
|
||||
* https://firebase.google.com/docs/firestore/manage-databases
|
||||
*/
|
||||
|
||||
// 'database' => env('FIREBASE_FIRESTORE_DATABASE'),
|
||||
],
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Firebase Realtime Database
|
||||
* ------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'database' => [
|
||||
|
||||
/*
|
||||
* In most of the cases the project ID defined in the credentials file
|
||||
* determines the URL of your project's Realtime Database. If the
|
||||
* connection to the Realtime Database fails, you can override
|
||||
* its URL with the value you see at
|
||||
*
|
||||
* https://console.firebase.google.com/u/1/project/_/database
|
||||
*
|
||||
* Please make sure that you use a full URL like, for example,
|
||||
* https://my-project-id.firebaseio.com
|
||||
*/
|
||||
|
||||
'url' => env('FIREBASE_DATABASE_URL'),
|
||||
|
||||
/*
|
||||
* As a best practice, a service should have access to only the resources it needs.
|
||||
* To get more fine-grained control over the resources a Firebase app instance can access,
|
||||
* use a unique identifier in your Security Rules to represent your service.
|
||||
*
|
||||
* https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges
|
||||
*/
|
||||
|
||||
// 'auth_variable_override' => [
|
||||
// 'uid' => 'my-service-worker'
|
||||
// ],
|
||||
|
||||
],
|
||||
|
||||
'dynamic_links' => [
|
||||
|
||||
/*
|
||||
* Dynamic links can be built with any URL prefix registered on
|
||||
*
|
||||
* https://console.firebase.google.com/u/1/project/_/durablelinks/links/
|
||||
*
|
||||
* You can define one of those domains as the default for new Dynamic
|
||||
* Links created within your project.
|
||||
*
|
||||
* The value must be a valid domain, for example,
|
||||
* https://example.page.link
|
||||
*/
|
||||
|
||||
'default_domain' => env('FIREBASE_DYNAMIC_LINKS_DEFAULT_DOMAIN'),
|
||||
],
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Firebase Cloud Storage
|
||||
* ------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'storage' => [
|
||||
|
||||
/*
|
||||
* Your project's default storage bucket usually uses the project ID
|
||||
* as its name. If you have multiple storage buckets and want to
|
||||
* use another one as the default for your application, you can
|
||||
* override it here.
|
||||
*/
|
||||
|
||||
'default_bucket' => env('FIREBASE_STORAGE_DEFAULT_BUCKET'),
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Caching
|
||||
* ------------------------------------------------------------------------
|
||||
*
|
||||
* The Firebase Admin SDK can cache some data returned from the Firebase
|
||||
* API, for example Google's public keys used to verify ID tokens.
|
||||
*
|
||||
*/
|
||||
|
||||
'cache_store' => env('FIREBASE_CACHE_STORE', 'file'),
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* Logging
|
||||
* ------------------------------------------------------------------------
|
||||
*
|
||||
* Enable logging of HTTP interaction for insights and/or debugging.
|
||||
*
|
||||
* Log channels are defined in config/logging.php
|
||||
*
|
||||
* Successful HTTP messages are logged with the log level 'info'.
|
||||
* Failed HTTP messages are logged with the log level 'notice'.
|
||||
*
|
||||
* Note: Using the same channel for simple and debug logs will result in
|
||||
* two entries per request and response.
|
||||
*/
|
||||
|
||||
'logging' => [
|
||||
'http_log_channel' => env('FIREBASE_HTTP_LOG_CHANNEL'),
|
||||
'http_debug_log_channel' => env('FIREBASE_HTTP_DEBUG_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------------
|
||||
* HTTP Client Options
|
||||
* ------------------------------------------------------------------------
|
||||
*
|
||||
* Behavior of the HTTP Client performing the API requests
|
||||
*/
|
||||
|
||||
'http_client_options' => [
|
||||
|
||||
/*
|
||||
* Use a proxy that all API requests should be passed through.
|
||||
* (default: none)
|
||||
*/
|
||||
|
||||
'proxy' => env('FIREBASE_HTTP_CLIENT_PROXY'),
|
||||
|
||||
/*
|
||||
* Set the maximum amount of seconds (float) that can pass before
|
||||
* a request is considered timed out
|
||||
*
|
||||
* The default time out can be reviewed at
|
||||
* https://github.com/kreait/firebase-php/blob/6.x/src/Firebase/Http/HttpClientOptions.php
|
||||
*/
|
||||
|
||||
'timeout' => env('FIREBASE_HTTP_CLIENT_TIMEOUT'),
|
||||
|
||||
'guzzle_middlewares' => [
|
||||
// MyInvokableMiddleware::class,
|
||||
// [MyMiddleware::class, 'static_method'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'periods_old' => [
|
||||
1 => ['start' => '07:00', 'end' => '07:45'],
|
||||
2 => ['start' => '07:45', 'end' => '08:30'],
|
||||
3 => ['start' => '08:30', 'end' => '09:15'],
|
||||
'break1' => ['start' => '09:15', 'end' => '09:30'],
|
||||
4 => ['start' => '09:30', 'end' => '10:15'],
|
||||
5 => ['start' => '10:15', 'end' => '11:00'],
|
||||
6 => ['start' => '11:00', 'end' => '11:45'],
|
||||
'break2' => ['start' => '11:45', 'end' => '12:30'],
|
||||
7 => ['start' => '12:30', 'end' => '13:15'],
|
||||
8 => ['start' => '13:15', 'end' => '14:00'],
|
||||
],
|
||||
|
||||
'periods' => [
|
||||
1 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 1,
|
||||
'label' => 'Jam 1',
|
||||
'start' => '07:00',
|
||||
'end' => '07:45',
|
||||
],
|
||||
2 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 2,
|
||||
'label' => 'Jam 2',
|
||||
'start' => '07:45',
|
||||
'end' => '08:30',
|
||||
],
|
||||
3 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 3,
|
||||
'label' => 'Jam 3',
|
||||
'start' => '08:30',
|
||||
'end' => '09:15',
|
||||
],
|
||||
4 => [
|
||||
'type' => 'break',
|
||||
'label' => 'Istirahat 1',
|
||||
'start' => '09:15',
|
||||
'end' => '09:30',
|
||||
],
|
||||
5 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 4,
|
||||
'label' => 'Jam 4',
|
||||
'start' => '09:30',
|
||||
'end' => '10:15',
|
||||
],
|
||||
6 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 5,
|
||||
'label' => 'Jam 5',
|
||||
'start' => '10:15',
|
||||
'end' => '11:00',
|
||||
],
|
||||
7 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 6,
|
||||
'label' => 'Jam 6',
|
||||
'start' => '11:00',
|
||||
'end' => '11:45',
|
||||
],
|
||||
8 => [
|
||||
'type' => 'break',
|
||||
'label' => 'Istirahat 2',
|
||||
'start' => '11:45',
|
||||
'end' => '12:30',
|
||||
],
|
||||
9 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 7,
|
||||
'label' => 'Jam 7',
|
||||
'start' => '12:30',
|
||||
'end' => '13:15',
|
||||
],
|
||||
10 => [
|
||||
'type' => 'class',
|
||||
'period_no' => 8,
|
||||
'label' => 'Jam 8',
|
||||
'start' => '13:15',
|
||||
'end' => '14:00',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
];
|
||||
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort(),
|
||||
// Sanctum::currentRequestHost(),
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
'telegram' => [
|
||||
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
|
||||
'chat_id' => env('TELEGRAM_CHAT_ID'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::snake((string) env('APP_NAME', 'laravel')).'_session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'qrcode' => ['hadir', 'izin', 'sakit', 'alpha', 'dispen', 'invalid'],
|
||||
'face_recognition' => ['tepat_waktu', 'terlambat'],
|
||||
];
|
||||
|
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->enum('role', ['student', 'teacher', 'admin']); // siswa, guru, admin.
|
||||
$table->string('password');
|
||||
$table->string('device_token')->nullable(); // for push notification FCM token
|
||||
$table->string('device_id')->nullable(); // for device identification IMEI or so on
|
||||
$table->string('topic_subscribe')->nullable(); // for FCM topic subscribe (like class topic)
|
||||
$table->string('profile_picture')->nullable();
|
||||
$table->boolean('is_banned')->default(false);
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
|
||||
//relisionship
|
||||
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('teachers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('nip');
|
||||
$table->string('subject'); // Subject will replace by pivot table
|
||||
$table->timestamps();
|
||||
|
||||
// Relationship
|
||||
$table->foreignId('id_user')->constrained('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('teachers');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('clases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('major');
|
||||
$table->integer('grade');
|
||||
$table->string('code');
|
||||
$table->string('fcm_topic')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('clases');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('students', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('nisn');
|
||||
$table->integer('entry_year');
|
||||
$table->string('pictures')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Relationship
|
||||
$table->foreignId('id_user')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('id_class')->constrained('clases')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('students');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subjects', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->enum('type', ['jurusan', 'umum']);
|
||||
$table->string('code');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subjects');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('schedules', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('day_of_week');
|
||||
$table->integer('period_start');
|
||||
$table->integer('period_end');
|
||||
$table->time('start_time');
|
||||
$table->time('end_time');
|
||||
$table->string('code');
|
||||
$table->timestamps();
|
||||
|
||||
// Relationship
|
||||
$table->foreignId('id_class')->constrained('clases')->onDelete('cascade');
|
||||
$table->foreignId('id_subject')->constrained('subjects')->onDelete('cascade');
|
||||
$table->foreignId('id_teacher')->constrained('teachers')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('schedules');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('attendance_histories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->integer('period_number');
|
||||
$table->enum('status', ['hadir', 'izin', 'sakit', 'alpha', 'dispen', 'invalid']);
|
||||
$table->string('coordinate')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Relationship
|
||||
$table->foreignId('id_student')->constrained('students')->onDelete('cascade');
|
||||
$table->foreignId('id_schedule')->constrained('schedules')->onDelete('cascade');
|
||||
$table->foreignId('id_class')->constrained('clases')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('attendance_histories');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('attendance_history_dailys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('status', ['tepat_waktu', 'terlambat','izin']);
|
||||
$table->string('picture');
|
||||
$table->timestamps();
|
||||
// Relationship
|
||||
$table->foreignId('id_student')->constrained('students')->onDelete('cascade');
|
||||
$table->foreignId('id_class')->constrained('clases')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('attendance_history_dailys');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subject_teacher', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('id_teacher')->constrained('teachers')->onDelete('cascade');
|
||||
$table->foreignId('id_subject')->constrained('subjects')->onDelete('cascade');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subject_teacher');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('academic_periods', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->dateTime('start_date');
|
||||
$table->dateTime('end_date');
|
||||
$table->boolean('is_active')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('academic_periods');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
// relationship foreign key
|
||||
$table->foreignId('id_academic_periods')->constrained('academic_periods')->onDelete('cascade')->onUpdate('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->dropForeign(['id_academic_periods']);
|
||||
$table->dropColumn('id_academic_periods');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->text('body');
|
||||
$table->string('type')->nullable(); // info, warning, etc
|
||||
$table->string('send_to')->nullable(); // just description, it will send it to class student or specific user
|
||||
$table->foreignId('sender_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('receiver_id')->nullable()->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('class_id')->nullable()->constrained('clases')->onDelete('cascade')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('attendance_histories', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('attendance_histories', 'attendance_date')) {
|
||||
$table->date('attendance_date')->after('coordinate')->nullable()->index();
|
||||
}
|
||||
});
|
||||
|
||||
// Backfill existing rows with their created_at date to maintain uniqueness
|
||||
DB::table('attendance_histories')
|
||||
->whereNull('attendance_date')
|
||||
->update(['attendance_date' => DB::raw('DATE(created_at)')]);
|
||||
|
||||
Schema::table('attendance_histories', function (Blueprint $table) {
|
||||
$table->unique(['id_student', 'id_schedule', 'attendance_date'], 'attendance_unique_per_day');
|
||||
});
|
||||
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->index(['code', 'id_class'], 'schedules_code_class_idx');
|
||||
});
|
||||
|
||||
Schema::table('academic_periods', function (Blueprint $table) {
|
||||
$table->index('is_active', 'academic_periods_is_active_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('attendance_histories', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('attendance_histories', 'attendance_date')) {
|
||||
$table->dropUnique('attendance_unique_per_day');
|
||||
$table->dropIndex('attendance_histories_attendance_date_index');
|
||||
$table->dropColumn('attendance_date');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->dropIndex('schedules_code_class_idx');
|
||||
});
|
||||
|
||||
Schema::table('academic_periods', function (Blueprint $table) {
|
||||
$table->dropIndex('academic_periods_is_active_idx');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('table_report_disrepancy', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('id_student')->constrained('students')->onDelete('cascade');
|
||||
$table->string('student_name');
|
||||
$table->foreignId('reported_by')->constrained('users')->onDelete('cascade'); // admin or teacher who report the discrepancy
|
||||
$table->foreignId('id_attendance_history')->constrained('attendance_histories')->onDelete('cascade'); // for specific attendance record that has discrepancy
|
||||
// $table->foreignId('id_schedule')->constrained('schedules')->onDelete('cascade');
|
||||
$table->foreignId('id_class')->constrained('clases')->onDelete('cascade');
|
||||
$table->enum('disrepancy_type', ['terlambat', 'hp_tidak_tersedia', 'izin', 'pulang_awal', 'alasan_lain'])->nullable(); // jenis ketidaksesuaian
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('marked_as_resolved')->default(false);
|
||||
$table->date('attendance_date');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('table_report_disrepancy');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('reason', ['sakit', 'izin', 'dispen', 'lainnya']);
|
||||
$table->text('information')->nullable(true);
|
||||
$table->string('evidence');
|
||||
$table->enum('status', ['proses', 'diterima', 'ditolak']);
|
||||
$table->string('feedback')->nullable(true); // feedback message from admin
|
||||
$table->date('date_permission'); // tanggal izin
|
||||
$table->integer('time_period'); // jumlah jam izin, misal 2 jam untuk 2 periode
|
||||
|
||||
// relationship foreign key
|
||||
$table->foreignId('id_student')->constrained('students')->onDelete('cascade');
|
||||
|
||||
// relation for approval by admin or teacher.
|
||||
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
// timestamp
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permissions');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AcademicPeriodsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
DB::table('academic_periods')->insert([
|
||||
[
|
||||
'name' => 'Ganjil 2025/2026',
|
||||
'start_date' => Carbon::create(2025, 7, 14, 0, 0, 0),
|
||||
'end_date' => Carbon::create(2025, 12, 20, 23, 59, 59),
|
||||
'is_active' => false,
|
||||
'created_at' => Carbon::create(2025, 7, 1, 0, 0, 0),
|
||||
'updated_at' => Carbon::create(2025, 7, 1, 0, 0, 0),
|
||||
],
|
||||
[
|
||||
'name' => 'Genap 2025/2026',
|
||||
'start_date' => Carbon::create(2026, 1, 6, 0, 0, 0),
|
||||
'end_date' => Carbon::create(2026, 6, 27, 23, 59, 59),
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::create(2025, 12, 1, 0, 0, 0),
|
||||
'updated_at' => Carbon::create(2025, 12, 1, 0, 0, 0),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\AttendanceHistoryDaily;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class AttendanceHistoryDailySeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
AttendanceHistoryDaily::create([
|
||||
'id_student' => 1,
|
||||
'id_class' => 1,
|
||||
'status' => 'tepat_waktu',
|
||||
'picture' => 'https://ui-avatars.com/api/?name=Student+One',
|
||||
'created_at' => Carbon::now()->setTime(7, 10, 0),
|
||||
'updated_at' => Carbon::now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClasesSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
DB::table('clases')->insert([
|
||||
[
|
||||
'name' => '11 TKJ 1',
|
||||
'major' => 'Teknik Komputer dan Jaringan',
|
||||
'grade' => 11,
|
||||
'code' => 'TKJ',
|
||||
'fcm_topic' => 'kelas_11_tkj_1',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'name' => '11 TKJ 2',
|
||||
'major' => 'Teknik Komputer dan Jaringan',
|
||||
'grade' => 11,
|
||||
'code' => 'TKJ',
|
||||
'fcm_topic' => 'kelas_11_tkj_2',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'name' => '11 TKJ 3',
|
||||
'major' => 'Teknik Komputer dan Jaringan',
|
||||
'grade' => 11,
|
||||
'code' => 'TKJ',
|
||||
'fcm_topic' => 'kelas_11_tkj_3',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
||||
$this->call([
|
||||
UserSeeder::class,
|
||||
ClasesSeeder::class,
|
||||
SubjectSeeder::class,
|
||||
TeacherSeeder::class,
|
||||
AcademicPeriodsSeeder::class,
|
||||
ScheduleSeeder::class,
|
||||
// AttendanceHistoryDailySeeder::class,
|
||||
]);
|
||||
|
||||
|
||||
// User::factory()->create([
|
||||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// ]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class ScheduleSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$class = DB::table('clases')
|
||||
->where('name', '11 TKJ 1')
|
||||
->first();
|
||||
|
||||
if (!$class) {
|
||||
throw new \RuntimeException('Kelas 11 TKJ 1 tidak ditemukan. Jalankan ClasesSeeder terlebih dahulu.');
|
||||
}
|
||||
|
||||
$teacherId = DB::table('teachers')
|
||||
->orderBy('id')
|
||||
->value('id');
|
||||
|
||||
if (!$teacherId) {
|
||||
throw new \RuntimeException('Tidak ada data guru untuk membuat jadwal. Jalankan TeacherSeeder terlebih dahulu.');
|
||||
}
|
||||
|
||||
$academicPeriodId = DB::table('academic_periods')
|
||||
->where('is_active', 1)
|
||||
->value('id');
|
||||
|
||||
if (!$academicPeriodId) {
|
||||
throw new \RuntimeException('Tidak ada academic period aktif. Jalankan AcademicPeriodsSeeder terlebih dahulu.');
|
||||
}
|
||||
|
||||
$subjects = DB::table('subjects')
|
||||
->orderBy('id')
|
||||
->get(['id', 'code', 'name']);
|
||||
|
||||
if ($subjects->count() < 4) {
|
||||
throw new \RuntimeException('Data mapel belum cukup untuk membuat jadwal. Jalankan SubjectSeeder terlebih dahulu.');
|
||||
}
|
||||
|
||||
$periods = config('periods.periods');
|
||||
$scheduleWindows = [
|
||||
3 => [[1, 2], [3, 5], [6, 10]],
|
||||
4 => [[1, 1], [2, 3], [5, 7], [9, 10]],
|
||||
];
|
||||
|
||||
$days = ['senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu'];
|
||||
$daySubjectMap = [
|
||||
'senin' => [0, 1, 2],
|
||||
'selasa' => [2, 3, 4],
|
||||
'rabu' => [1, 3, 0, 4],
|
||||
'kamis' => [4, 0, 2],
|
||||
'jumat' => [3, 1, 2, 0],
|
||||
'sabtu' => [0, 4, 1],
|
||||
];
|
||||
|
||||
foreach ($days as $day) {
|
||||
$subjectIndexes = $daySubjectMap[$day];
|
||||
$count = count($subjectIndexes);
|
||||
$windows = $scheduleWindows[$count];
|
||||
|
||||
foreach ($subjectIndexes as $slotIndex => $subjectIndex) {
|
||||
$subject = $subjects->get($subjectIndex);
|
||||
|
||||
if (!$subject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$periodStart, $periodEnd] = $windows[$slotIndex];
|
||||
|
||||
if (!isset($periods[$periodStart], $periods[$periodEnd])) {
|
||||
throw new \RuntimeException("Konfigurasi periode tidak ditemukan untuk rentang {$periodStart}-{$periodEnd}.");
|
||||
}
|
||||
|
||||
$startTime = $periods[$periodStart]['start'];
|
||||
$endTime = $periods[$periodEnd]['end'];
|
||||
$token = (string) random_int(1000, 9999);
|
||||
$rawCode = "{$class->name}-" . strtoupper(substr($day, 0, 3)) . "-{$periodStart}-{$periodEnd}-{$subject->code}-{$academicPeriodId}-{$token}";
|
||||
|
||||
DB::table('schedules')->insert([
|
||||
'id_class' => $class->id,
|
||||
'id_teacher' => $teacherId,
|
||||
'id_subject' => $subject->id,
|
||||
'day_of_week' => $day,
|
||||
'period_start' => $periodStart,
|
||||
'period_end' => $periodEnd,
|
||||
'start_time' => $startTime,
|
||||
'end_time' => $endTime,
|
||||
'id_academic_periods' => $academicPeriodId,
|
||||
'code' => Crypt::encryptString($rawCode),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SubjectSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$subjects = [
|
||||
['code' => 'MTK', 'name' => 'Matematika', 'type' => 'umum'],
|
||||
['code' => 'SENBUD', 'name' => 'Seni Kebudayaan', 'type' => 'umum'],
|
||||
['code' => 'SEJ', 'name' => 'Sejarah', 'type' => 'umum'],
|
||||
['code' => 'BD', 'name' => 'Bahasa Daerah', 'type' => 'umum'],
|
||||
['code' => 'FIS', 'name' => 'Fisika', 'type' => 'umum'],
|
||||
['code' => 'KIM', 'name' => 'Kimia', 'type' => 'jurusan'],
|
||||
['code' => 'SDG', 'name' => 'Sistem Digital', 'type' => 'jurusan'],
|
||||
['code' => 'PRODAS', 'name' => 'Pemrograman Dasar ', 'type' => 'jurusan'],
|
||||
['code' => 'JARKOM', 'name' => 'Jaringan Komputer', 'type' => 'jurusan'],
|
||||
];
|
||||
|
||||
foreach ($subjects as $subject) {
|
||||
DB::table('subjects')->insert([
|
||||
'code' => $subject['code'],
|
||||
'name' => $subject['name'],
|
||||
'type' => $subject['type'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Teacher;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class TeacherSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Cari user guru berdasarkan email
|
||||
|
||||
Teacher::create([
|
||||
'name' => 'Zainal Abidin',
|
||||
'nip' => '198001012005011001',
|
||||
'subject' => 'SENBUD, SEJ',
|
||||
'id_user' => 2,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
User::create([
|
||||
'email' => 'admin@example.com',
|
||||
'role' => 'admin',
|
||||
'password' => bcrypt('admin123'),
|
||||
]);
|
||||
|
||||
User::create([
|
||||
'email' => 'zainal@guru.com',
|
||||
'role' => 'teacher', // pastikan ini sesuai enum di migration
|
||||
'password' => bcrypt('user123'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Baris bahasa berikut digunakan oleh class validator. Beberapa baris
|
||||
| memiliki banyak versi seperti rule size. Ubah sesuai kebutuhan aplikasi.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => ':attribute harus diterima.',
|
||||
'accepted_if' => ':attribute harus diterima ketika :other adalah :value.',
|
||||
'active_url' => ':attribute bukan URL yang valid.',
|
||||
'after' => ':attribute harus berisi tanggal setelah :date.',
|
||||
'after_or_equal' => ':attribute harus berisi tanggal setelah atau sama dengan :date.',
|
||||
'alpha' => ':attribute hanya boleh berisi huruf.',
|
||||
'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, strip, dan garis bawah.',
|
||||
'alpha_num' => ':attribute hanya boleh berisi huruf dan angka.',
|
||||
'array' => ':attribute harus berupa array.',
|
||||
'ascii' => ':attribute hanya boleh berisi karakter dan simbol alfanumerik single-byte.',
|
||||
'before' => ':attribute harus berisi tanggal sebelum :date.',
|
||||
'before_or_equal' => ':attribute harus berisi tanggal sebelum atau sama dengan :date.',
|
||||
'between' => [
|
||||
'array' => ':attribute harus berisi antara :min sampai :max item.',
|
||||
'file' => ':attribute harus berukuran antara :min sampai :max kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai antara :min sampai :max.',
|
||||
'string' => ':attribute harus berisi antara :min sampai :max karakter.',
|
||||
],
|
||||
'boolean' => ':attribute harus bernilai true atau false.',
|
||||
'can' => ':attribute berisi nilai yang tidak diizinkan.',
|
||||
'confirmed' => 'Konfirmasi :attribute tidak cocok.',
|
||||
'contains' => ':attribute kehilangan nilai yang dibutuhkan.',
|
||||
'current_password' => 'Password salah.',
|
||||
'date' => ':attribute bukan tanggal yang valid.',
|
||||
'date_equals' => ':attribute harus berisi tanggal yang sama dengan :date.',
|
||||
'date_format' => ':attribute tidak sesuai dengan format :format.',
|
||||
'decimal' => ':attribute harus memiliki :decimal angka desimal.',
|
||||
'declined' => ':attribute harus ditolak.',
|
||||
'declined_if' => ':attribute harus ditolak ketika :other bernilai :value.',
|
||||
'different' => ':attribute dan :other harus berbeda.',
|
||||
'digits' => ':attribute harus terdiri :digits angka.',
|
||||
'digits_between' => ':attribute harus terdiri dari :min sampai :max angka.',
|
||||
'dimensions' => ':attribute memiliki dimensi gambar yang tidak valid.',
|
||||
'distinct' => ':attribute memiliki nilai yang duplikat.',
|
||||
'doesnt_end_with' => ':attribute tidak boleh diakhiri dengan salah satu berikut: :values.',
|
||||
'doesnt_start_with' => ':attribute tidak boleh diawali dengan salah satu berikut: :values.',
|
||||
'email' => ':attribute harus berupa alamat email yang valid.',
|
||||
'ends_with' => ':attribute harus diakhiri salah satu dari berikut: :values.',
|
||||
'enum' => ':attribute yang dipilih tidak valid.',
|
||||
'exists' => ':attribute yang dipilih tidak valid.',
|
||||
'extensions' => ':attribute harus memiliki salah satu ekstensi berikut: :values.',
|
||||
'file' => ':attribute harus berupa berkas.',
|
||||
'filled' => ':attribute wajib diisi.',
|
||||
'gt' => [
|
||||
'array' => ':attribute harus berisi lebih dari :value item.',
|
||||
'file' => ':attribute harus berukuran lebih dari :value kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai lebih dari :value.',
|
||||
'string' => ':attribute harus berisi lebih dari :value karakter.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => ':attribute harus berisi :value item atau lebih.',
|
||||
'file' => ':attribute harus berukuran lebih besar atau sama dengan :value kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai lebih besar atau sama dengan :value.',
|
||||
'string' => ':attribute harus berisi lebih besar atau sama dengan :value karakter.',
|
||||
],
|
||||
'hex_color' => ':attribute harus berupa warna heksadesimal yang valid.',
|
||||
'image' => ':attribute harus berupa gambar.',
|
||||
'in' => ':attribute yang dipilih tidak valid.',
|
||||
'in_array' => ':attribute tidak terdapat dalam :other.',
|
||||
'integer' => ':attribute harus berupa bilangan bulat.',
|
||||
'ip' => ':attribute harus berupa alamat IP yang valid.',
|
||||
'ipv4' => ':attribute harus berupa alamat IPv4 yang valid.',
|
||||
'ipv6' => ':attribute harus berupa alamat IPv6 yang valid.',
|
||||
'json' => ':attribute harus berupa JSON string yang valid.',
|
||||
'list' => ':attribute harus berupa list.',
|
||||
'lowercase' => ':attribute harus berupa huruf kecil.',
|
||||
'lt' => [
|
||||
'array' => ':attribute harus berisi kurang dari :value item.',
|
||||
'file' => ':attribute harus berukuran kurang dari :value kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai kurang dari :value.',
|
||||
'string' => ':attribute harus berisi kurang dari :value karakter.',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => ':attribute harus berisi tidak lebih dari :value item.',
|
||||
'file' => ':attribute harus berukuran kurang dari atau sama dengan :value kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai kurang dari atau sama dengan :value.',
|
||||
'string' => ':attribute harus berisi kurang dari atau sama dengan :value karakter.',
|
||||
],
|
||||
'mac_address' => ':attribute harus berupa MAC address yang valid.',
|
||||
'max' => [
|
||||
'array' => ':attribute tidak boleh berisi lebih dari :max item.',
|
||||
'file' => ':attribute tidak boleh berukuran lebih dari :max kilobyte.',
|
||||
'numeric' => ':attribute tidak boleh bernilai lebih dari :max.',
|
||||
'string' => ':attribute tidak boleh berisi lebih dari :max karakter.',
|
||||
],
|
||||
'max_digits' => ':attribute tidak boleh memiliki lebih dari :max angka.',
|
||||
'mimes' => ':attribute harus berupa berkas dengan format: :values.',
|
||||
'mimetypes' => ':attribute harus berupa berkas dengan format: :values.',
|
||||
'min' => [
|
||||
'array' => ':attribute harus berisi minimal :min item.',
|
||||
'file' => ':attribute harus berukuran minimal :min kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai minimal :min.',
|
||||
'string' => ':attribute harus berisi minimal :min karakter.',
|
||||
],
|
||||
'min_digits' => ':attribute harus memiliki minimal :min angka.',
|
||||
'missing' => ':attribute tidak boleh ada.',
|
||||
'missing_if' => ':attribute tidak boleh ada ketika :other bernilai :value.',
|
||||
'missing_unless' => ':attribute tidak boleh ada kecuali :other bernilai :value.',
|
||||
'missing_with' => ':attribute tidak boleh ada ketika :values ada.',
|
||||
'missing_with_all' => ':attribute tidak boleh ada ketika :values ada.',
|
||||
'multiple_of' => ':attribute harus kelipatan dari :value.',
|
||||
'not_in' => ':attribute yang dipilih tidak valid.',
|
||||
'not_regex' => 'Format :attribute tidak valid.',
|
||||
'numeric' => ':attribute harus berupa angka.',
|
||||
'password' => [
|
||||
'letters' => ':attribute harus berisi setidaknya satu huruf.',
|
||||
'mixed' => ':attribute harus berisi setidaknya satu huruf besar dan satu huruf kecil.',
|
||||
'numbers' => ':attribute harus berisi setidaknya satu angka.',
|
||||
'symbols' => ':attribute harus berisi setidaknya satu simbol.',
|
||||
'uncompromised' => ':attribute yang dimasukkan telah muncul dalam kebocoran data. Mohon pilih :attribute lain.',
|
||||
],
|
||||
'present' => ':attribute wajib ada.',
|
||||
'present_if' => ':attribute wajib ada ketika :other bernilai :value.',
|
||||
'present_unless' => ':attribute wajib ada kecuali :other bernilai :value.',
|
||||
'present_with' => ':attribute wajib ada ketika :values ada.',
|
||||
'present_with_all' => ':attribute wajib ada ketika semua :values ada.',
|
||||
'prohibited' => ':attribute tidak diperbolehkan.',
|
||||
'prohibited_if' => ':attribute tidak diperbolehkan ketika :other bernilai :value.',
|
||||
'prohibited_unless' => ':attribute tidak diperbolehkan kecuali :other terdapat dalam :values.',
|
||||
'prohibits' => ':attribute melarang :other untuk ada.',
|
||||
'regex' => 'Format :attribute tidak valid.',
|
||||
'required' => ':attribute wajib diisi.',
|
||||
'required_array_keys' => 'Bidang :attribute harus berisi entri untuk: :values.',
|
||||
'required_if' => ':attribute wajib diisi ketika :other adalah :value.',
|
||||
'required_if_accepted' => ':attribute wajib diisi ketika :other diterima.',
|
||||
'required_if_declined' => ':attribute wajib diisi ketika :other ditolak.',
|
||||
'required_unless' => ':attribute wajib diisi kecuali :other terdapat dalam :values.',
|
||||
'required_with' => ':attribute wajib diisi ketika :values ada.',
|
||||
'required_with_all' => ':attribute wajib diisi ketika :values ada.',
|
||||
'required_without' => ':attribute wajib diisi ketika :values tidak ada.',
|
||||
'required_without_all' => ':attribute wajib diisi ketika tidak ada :values yang tersedia.',
|
||||
'same' => ':attribute dan :other harus sama.',
|
||||
'size' => [
|
||||
'array' => ':attribute harus berisi :size item.',
|
||||
'file' => ':attribute harus berukuran :size kilobyte.',
|
||||
'numeric' => ':attribute harus bernilai :size.',
|
||||
'string' => ':attribute harus berisi :size karakter.',
|
||||
],
|
||||
'starts_with' => ':attribute harus diawali salah satu dari berikut: :values.',
|
||||
'string' => ':attribute harus berupa string.',
|
||||
'timezone' => ':attribute harus berupa zona waktu yang valid.',
|
||||
'unique' => ':attribute sudah digunakan.',
|
||||
'uploaded' => ':attribute gagal diunggah.',
|
||||
'uppercase' => ':attribute harus berupa huruf besar.',
|
||||
'url' => ':attribute harus berupa URL yang valid.',
|
||||
'ulid' => ':attribute harus berupa ULID yang valid.',
|
||||
'uuid' => ':attribute harus berupa UUID yang valid.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Di sini Anda dapat menentukan pesan validasi khusus untuk atribut dengan
|
||||
| menggunakan konvensi "attribute.rule" untuk penamaan baris. Hal ini
|
||||
| mempermudah penentuan baris bahasa khusus untuk aturan atribut tertentu.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Baris bahasa berikut digunakan untuk menukar placeholder atribut dengan
|
||||
| sesuatu yang lebih ramah pembaca seperti "Alamat E-Mail" alih-alih
|
||||
| "email". Hal ini membantu kita membuat pesan menjadi lebih ekspresif.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [
|
||||
'name' => 'Nama',
|
||||
'email' => 'Email',
|
||||
'password' => 'Password',
|
||||
'password_confirmation' => 'Konfirmasi Password',
|
||||
'current_password' => 'Password Saat Ini',
|
||||
'new_password' => 'Password Baru',
|
||||
'new_password_confirmation' => 'Konfirmasi Password Baru',
|
||||
'nip' => 'NIP',
|
||||
'nisn' => 'NISN',
|
||||
'id_class' => 'Kelas',
|
||||
'entry_year' => 'Tahun Masuk',
|
||||
'subjects' => 'Mata Pelajaran',
|
||||
'subjects.*' => 'Mata Pelajaran',
|
||||
'profile_picture' => 'Foto Profil',
|
||||
'pictures' => 'Foto Webcam',
|
||||
'pictures_edit' => 'Foto Webcam',
|
||||
'token' => 'Token',
|
||||
'form_type' => 'Tipe Form',
|
||||
'delete_webcam_photo' => 'Hapus Foto Webcam',
|
||||
],
|
||||
|
||||
];
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue