First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
SysCMS Commercial License
Copyright (c) 2026 Marco Costa. All rights reserved.
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
SysCMS Commercial License
Copyright (c) 2026 Marco Costa. All rights reserved.
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+22
View File
@@ -0,0 +1,22 @@
# Notes Module for SysCMS
Notes and annotations management module.
## Features
- Create and manage notes
- Categorization and tagging
- Search functionality
- Rich text editing
- Sharing capabilities
## Installation
1. Upload the `notes.zip` file through the module manager or extract it to the `modules/` directory.
2. Run the migrations to register the module.
3. Configure note settings in the admin panel.
## Routes
- Web routes: `routes/web.php`
## License
Copyright (c) 2026 Marco Costa. All rights reserved.
Licensed under the SysCMS Commercial License.
+432
View File
@@ -0,0 +1,432 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace Modules\Notes\Controllers;
use App\Controllers\BaseAdminController;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Modules\Notes\Models\Note;
use Modules\Notes\Models\Subject;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Capsule\Manager as Capsule;
/**
* Class NotesController
*
* Main controller for Notes management.
*
* @package Modules\Notes\Controllers
*/
class NotesController extends BaseAdminController
{
/**
* NotesController constructor.
*
* @param View $view
*/
public function __construct(View $view)
{
parent::__construct($view);
}
/**
* Check if user has permission.
*
* @param string $permission
* @return void
*/
protected function checkPermission(string $permission)
{
$user = $this->getCurrentUser();
if (!$user || !$user->hasPermission($permission)) {
$this->addFlash('error', 'Você não tem permissão para acessar esta área.');
$this->redirect('admin/dashboard')->send();
exit;
}
}
/**
* Display a listing of the notes.
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function index(Request $request)
{
$this->checkPermission('notes.view');
$query = Note::currentUser()->with('subject');
// Search system
if ($search = $request->query->get('search')) {
$query->where(function ($q) use ($search) {
$q->where('notes_title', 'like', "%{$search}%")
->orWhere('notes_content', 'like', "%{$search}%");
});
}
// Subject filter
if ($subject_id = $request->query->get('subject_id')) {
$query->where('notes_subject_id', $subject_id);
}
$notes = $query->orderBy('notes_is_pinned', 'desc')
->orderBy('created_at', 'desc')
->get();
// Get subjects for filter and select
$subjects = Subject::currentUser()->orderBy('subjects_name')->get();
return $this->renderAdminView('@notes/admin/index.twig', [
'title' => __('notes.title'),
'notes' => $notes,
'subjects' => $subjects,
'filters' => [
'search' => $search,
'subject_id' => $subject_id
]
]);
}
/**
* Show the form for creating a new note.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function create()
{
$this->checkPermission('notes.create');
$subjects = Subject::currentUser()->orderBy('subjects_name')->get();
return $this->renderAdminView('@notes/admin/create.twig', [
'title' => __('notes.add_note'),
'subjects' => $subjects
]);
}
/**
* Store a newly created note.
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function store(Request $request)
{
$this->checkPermission('notes.create');
$data = $request->request->all();
try {
// Ensure session is started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$busId = $_SESSION['user_idbus'] ?? null;
if (empty($busId)) {
$busId = 1;
}
$noteData = [
'notes_idbus' => $busId,
'notes_iduser' => $_SESSION['user_id'] ?? null,
'notes_title' => $data['notes_title'],
'notes_content' => $data['notes_content'] ?? null,
'notes_subject_id' => $data['notes_subject_id'] ?: null,
'notes_color' => $data['notes_color'] ?? '#ffffff',
'notes_is_pinned' => isset($data['notes_is_pinned']) ? 1 : 0
];
// Handle image upload
if ($request->files->has('notes_image') && $request->files->get('notes_image')) {
$file = $request->files->get('notes_image');
if ($file->isValid()) {
$noteData['notes_image'] = $this->handleUpload($file);
}
}
// Handle file upload
if ($request->files->has('notes_file') && $request->files->get('notes_file')) {
$file = $request->files->get('notes_file');
if ($file->isValid()) {
$noteData['notes_file'] = $this->handleUpload($file);
}
}
Note::create($noteData);
$this->addFlash('success', __('notes.flash_note_created'));
} catch (\Exception $e) {
// Check if error is missing column (SQLSTATE[42S22])
if (strpos($e->getMessage(), '42S22') !== false) {
\App\Helpers\Log::warning('Missing columns in notes table. Migration might be pending.', ['error' => $e->getMessage()]);
// Try saving without the new columns
unset($noteData['notes_image'], $noteData['notes_file']);
try {
Note::create($noteData);
$this->addFlash('success', __('notes.flash_note_created') . ' (Aviso: Anexos não suportados até que a migração seja executada)');
} catch (\Exception $e2) {
\App\Helpers\Log::error('Error storing note fallback', ['error' => $e2->getMessage()]);
$this->addFlash('error', 'Erro ao criar a nota: ' . $e2->getMessage());
}
} else {
\App\Helpers\Log::error('Error storing note', ['error' => $e->getMessage()]);
$this->addFlash('error', 'Erro ao criar a nota: ' . $e->getMessage());
}
}
return $this->redirect('/admin/notes');
}
/**
* Show the form for editing the specified note.
*
* @param Request $request
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
*/
public function edit(Request $request, $id)
{
$this->checkPermission('notes.edit');
$note = Note::currentUser()->findOrFail($id);
$subjects = Subject::currentUser()->orderBy('subjects_name')->get();
return $this->renderAdminView('@notes/admin/edit.twig', [
'title' => __('notes.edit_note'),
'note' => $note,
'subjects' => $subjects
]);
}
/**
* Update the specified note.
*
* @param Request $request
* @param int $id
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function update(Request $request, $id)
{
$this->checkPermission('notes.edit');
$data = $request->request->all();
try {
$note = Note::currentUser()->findOrFail($id);
$noteData = [
'notes_title' => $data['notes_title'],
'notes_content' => $data['notes_content'] ?? null,
'notes_subject_id' => $data['notes_subject_id'] ?: null,
'notes_color' => $data['notes_color'] ?? '#ffffff',
'notes_is_pinned' => isset($data['notes_is_pinned']) ? 1 : 0
];
// Handle image upload
if ($request->files->has('notes_image') && $request->files->get('notes_image')) {
$file = $request->files->get('notes_image');
if ($file->isValid()) {
// Delete old image
if ($note->notes_image) {
$this->deletePhysicalFile($note->notes_image);
}
$noteData['notes_image'] = $this->handleUpload($file);
}
}
// Handle file upload
if ($request->files->has('notes_file') && $request->files->get('notes_file')) {
$file = $request->files->get('notes_file');
if ($file->isValid()) {
// Delete old file
if ($note->notes_file) {
$this->deletePhysicalFile($note->notes_file);
}
$noteData['notes_file'] = $this->handleUpload($file);
}
}
$note->update($noteData);
$this->addFlash('success', __('notes.flash_note_updated'));
} catch (\Exception $e) {
// Check if error is missing column (SQLSTATE[42S22])
if (strpos($e->getMessage(), '42S22') !== false) {
\App\Helpers\Log::warning('Missing columns in notes table. Migration might be pending.', ['id' => $id, 'error' => $e->getMessage()]);
// Try updating without the new columns
unset($noteData['notes_image'], $noteData['notes_file']);
try {
$note->update($noteData);
$this->addFlash('success', __('notes.flash_note_updated') . ' (Aviso: Anexos não suportados até que a migração seja executada)');
} catch (\Exception $e2) {
\App\Helpers\Log::error('Error updating note fallback', ['id' => $id, 'error' => $e2->getMessage()]);
$this->addFlash('error', 'Erro ao atualizar a nota: ' . $e2->getMessage());
}
} else {
\App\Helpers\Log::error('Error updating note', ['id' => $id, 'error' => $e->getMessage()]);
$this->addFlash('error', 'Erro ao atualizar a nota: ' . $e->getMessage());
}
}
return $this->redirect('/admin/notes');
}
/**
* Remove the specified note.
*
* @param Request $request
* @param int $id
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function delete(Request $request, $id)
{
$this->checkPermission('notes.delete');
$note = Note::currentUser()->findOrFail($id);
// Delete attachments
if ($note->notes_image) {
$this->deletePhysicalFile($note->notes_image);
}
if ($note->notes_file) {
$this->deletePhysicalFile($note->notes_file);
}
$note->delete();
$this->addFlash('success', __('notes.flash_note_deleted'));
return $this->redirect('/admin/notes');
}
/**
* Handle file upload.
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
* @return string|null
*/
protected function handleUpload($file): ?string
{
if (!$file) {
return null;
}
$destination = dirname(__DIR__, 4) . '/storage/uploads/notes';
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$filename = uniqid('img_') . '.' . $file->guessExtension();
$file->move($destination, $filename);
return $filename;
}
/**
* Delete physical file.
*
* @param string $filename
* @return void
*/
protected function deletePhysicalFile(string $filename)
{
$filePath = dirname(__DIR__, 4) . '/storage/uploads/notes/' . $filename;
if (file_exists($filePath) && is_file($filePath)) {
unlink($filePath);
}
}
/**
* Toggle the pinned status of a note.
*
* @param Request $request
* @param int $id
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function pin(Request $request, $id)
{
$this->checkPermission('notes.edit');
$note = Note::currentUser()->findOrFail($id);
$note->notes_is_pinned = !$note->notes_is_pinned;
$note->save();
$this->addFlash('success', $note->notes_is_pinned ? __('notes.flash_note_pinned') : __('notes.flash_note_unpinned'));
return $this->redirect('/admin/notes');
}
/**
* Store a newly created subject.
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function storeSubject(Request $request)
{
$this->checkPermission('notes.create');
$data = $request->request->all();
// Ensure session is started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$busId = $_SESSION['user_idbus'] ?? null;
if (empty($busId)) {
$busId = 1;
}
Subject::create([
'subjects_idbus' => $busId,
'subjects_iduser' => $_SESSION['user_id'] ?? null,
'subjects_name' => $data['subjects_name'],
'subjects_color' => $data['subjects_color'] ?? '#6c757d'
]);
$this->addFlash('success', __('notes.flash_subject_created'));
return $this->redirect('/admin/notes');
}
/**
* Delete a subject.
*
* @param Request $request
* @param int $id
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function deleteSubject(Request $request, $id)
{
$this->checkPermission('notes.delete');
$subject = Subject::currentUser()->findOrFail($id);
// Remove reference from notes
Note::currentUser()->where('notes_subject_id', $id)->update(['notes_subject_id' => null]);
$subject->delete();
$this->addFlash('success', __('notes.flash_subject_deleted'));
return $this->redirect('/admin/notes');
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace Modules\Notes\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Class Note
*
* Model for notes.
*
* @package Modules\Notes\Models
*/
class Note extends Model
{
/**
* @var string The table associated with the model.
*/
protected $table = 'notes';
/**
* @var string The primary key associated with the table.
*/
protected $primaryKey = 'notes_id';
/**
* @var array The attributes that are mass assignable.
*/
protected $fillable = [
'notes_idbus',
'notes_iduser',
'notes_title',
'notes_content',
'notes_subject_id',
'notes_is_pinned',
'notes_color',
'notes_image',
'notes_file'
];
/**
* The "booted" method of the model.
*
* @return void
*/
protected static function booted()
{
static::creating(function ($note) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$note->notes_idbus = $_SESSION['user_idbus'] ?? null;
if (empty($note->notes_idbus)) {
$note->notes_idbus = 1;
}
$note->notes_iduser = $_SESSION['user_id'] ?? null;
});
}
/**
* Scope a query to only include notes for the current business and user.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeCurrentUser($query)
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$busId = $_SESSION['user_idbus'] ?? null;
if (empty($busId)) {
$busId = 1;
}
$userId = $_SESSION['user_id'] ?? null;
return $query->where('notes_idbus', $busId)
->where('notes_iduser', $userId);
}
/**
* Get the subject that owns the note.
*/
public function subject()
{
return $this->belongsTo(Subject::class, 'notes_subject_id', 'subjects_id');
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace Modules\Notes\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Class Subject
*
* Model for note subjects/categories.
*
* @package Modules\Notes\Models
*/
class Subject extends Model
{
/**
* @var string The table associated with the model.
*/
protected $table = 'notes_subjects';
/**
* @var string The primary key associated with the table.
*/
protected $primaryKey = 'subjects_id';
/**
* @var array The attributes that are mass assignable.
*/
protected $fillable = [
'subjects_idbus',
'subjects_iduser',
'subjects_name',
'subjects_color'
];
/**
* The "booted" method of the model.
*
* @return void
*/
protected static function booted()
{
static::creating(function ($subject) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($subject->subjects_idbus)) {
$subject->subjects_idbus = $_SESSION['user_idbus'] ?? null;
}
if (empty($subject->subjects_idbus)) {
$subject->subjects_idbus = 1;
}
if (empty($subject->subjects_iduser)) {
$subject->subjects_iduser = $_SESSION['user_id'] ?? null;
}
});
}
/**
* Scope a query to only include subjects for the current business and user.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeCurrentUser($query)
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$busId = $_SESSION['user_idbus'] ?? null;
if (empty($busId)) {
$busId = 1;
}
$userId = $_SESSION['user_id'] ?? null;
return $query->where('subjects_idbus', $busId)
->where('subjects_iduser', $userId);
}
/**
* Get the notes for the subject.
*/
public function notes()
{
return $this->hasMany(Note::class, 'notes_subject_id', 'subjects_id');
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace Modules\Notes\Providers;
use Kernel\Application;
/**
* Class NotesServiceProvider
*
* Service provider for the Notes module.
*
* @package Modules\Notes\Providers
*/
class NotesServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
// Registration logic if needed
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$app = Application::getInstance();
// You can register global variables for Twig here if needed
// $app->getView()->addGlobal('notes_count', Note::currentUser()->count());
}
}
@@ -0,0 +1,112 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Illuminate\Database\Capsule\Manager as Capsule;
/**
* Migration: Insert Notes Module
*/
class InsertNotesModule
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
$moduleName = 'notes';
$moduleData = [
'modules_name' => $moduleName,
'modules_display_name' => 'Notes',
'modules_description' => 'Módulo de notas e links para o usuário.',
'modules_version' => '1.0.0',
'modules_author' => 'SysCMS Team',
'modules_ative' => 1,
'modules_public' => 0,
'modules_blocked' => 0,
'modules_dependencies' => json_encode(['users']),
'modules_permissions' => json_encode([
"notes.view",
"notes.create",
"notes.edit",
"notes.delete"
]),
'modules_settings' => json_encode([
"menu" => [
"dashboard" => [
"icon" => "bi bi-sticky",
"label" => "Notes",
"route" => "admin/notes",
"order" => 60,
"permission" => "notes.view"
]
]
]),
'modules_path' => 'modules/notes',
'modules_namespace' => 'Modules\\Notes',
'modules_icon' => 'bi bi-sticky',
'menu_side_bar' => 1,
'menu_nav_bar' => 0,
'modules_order' => 60,
'modules_install_date' => date('Y-m-d H:i:s'),
'modules_update_date' => date('Y-m-d H:i:s')
];
$existingModule = Capsule::table('modules')->where('modules_name', $moduleName)->first();
if (!$existingModule) {
$moduleId = Capsule::table('modules')->insertGetId($moduleData);
// Associate with all existing companies
if (Capsule::schema()->hasTable('bus')) {
$companies = Capsule::table('bus')->get();
foreach ($companies as $company) {
Capsule::table('bus_modules')->insert([
'bus_modules_bus_id' => $company->bus_id,
'bus_modules_module_id' => $moduleId,
'bus_modules_is_enabled' => 1,
'bus_modules_show_public_nav' => 0,
'bus_modules_show_dashboard_nav' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
]);
}
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
$module = Capsule::table('modules')->where('modules_name', 'notes')->first();
if ($module) {
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
Capsule::table('modules')->where('modules_id', $module->modules_id)->delete();
}
}
}
@@ -0,0 +1,68 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
/**
* Migration: Create Notes Table
*/
class CreateNotesTable
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (!Capsule::schema()->hasTable('notes')) {
Capsule::schema()->create('notes', function (Blueprint $table) {
$table->id('notes_id');
$table->unsignedBigInteger('notes_idbus')->nullable();
$table->unsignedBigInteger('notes_iduser')->nullable();
$table->string('notes_title');
$table->text('notes_content')->nullable();
$table->enum('notes_type', ['note', 'link'])->default('note');
$table->string('notes_url')->nullable();
$table->boolean('notes_is_pinned')->default(false);
$table->string('notes_color')->default('#ffffff');
$table->timestamps();
$table->index('notes_idbus');
$table->index('notes_iduser');
$table->index('notes_type');
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Capsule::schema()->dropIfExists('notes');
}
}
@@ -0,0 +1,64 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
/**
* Migration: Add Category to Notes Table
*/
class AddCategoryToNotesTable
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (Capsule::schema()->hasTable('notes')) {
Capsule::schema()->table('notes', function (Blueprint $table) {
if (!Capsule::schema()->hasColumn('notes', 'notes_category')) {
$table->string('notes_category')->nullable()->after('notes_type');
$table->index('notes_category');
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
if (Capsule::schema()->hasTable('notes')) {
Capsule::schema()->table('notes', function (Blueprint $table) {
if (Capsule::schema()->hasColumn('notes', 'notes_category')) {
$table->dropColumn('notes_category');
}
});
}
}
}
@@ -0,0 +1,82 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
/**
* Migration: Create Notes Subjects Table and Update Notes Table
*/
class CreateNotesSubjectsTable
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
// Create subjects table
if (!Capsule::schema()->hasTable('notes_subjects')) {
Capsule::schema()->create('notes_subjects', function (Blueprint $table) {
$table->id('subjects_id');
$table->unsignedBigInteger('subjects_idbus')->nullable();
$table->unsignedBigInteger('subjects_iduser')->nullable();
$table->string('subjects_name');
$table->string('subjects_color')->default('#6c757d');
$table->timestamps();
$table->index('subjects_idbus');
$table->index('subjects_iduser');
});
}
// Update notes table to use subject_id
if (Capsule::schema()->hasTable('notes')) {
Capsule::schema()->table('notes', function (Blueprint $table) {
if (!Capsule::schema()->hasColumn('notes', 'notes_subject_id')) {
$table->unsignedBigInteger('notes_subject_id')->nullable()->after('notes_type');
$table->index('notes_subject_id');
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
if (Capsule::schema()->hasTable('notes')) {
Capsule::schema()->table('notes', function (Blueprint $table) {
if (Capsule::schema()->hasColumn('notes', 'notes_subject_id')) {
$table->dropColumn('notes_subject_id');
}
});
}
Capsule::schema()->dropIfExists('notes_subjects');
}
}
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
/**
* Migration: Remove Link columns from Notes
*/
class RemoveLinkColumnsFromNotes
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (Capsule::schema()->hasColumn('notes', 'notes_type')) {
Capsule::schema()->table('notes', function (Blueprint $table) {
$table->dropColumn('notes_type');
});
}
if (Capsule::schema()->hasColumn('notes', 'notes_url')) {
Capsule::schema()->table('notes', function (Blueprint $table) {
$table->dropColumn('notes_url');
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Capsule::schema()->table('notes', function (Blueprint $table) {
$table->enum('notes_type', ['note', 'link'])->default('note')->after('notes_content');
$table->string('notes_url')->nullable()->after('notes_type');
});
}
}
@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
/**
* Migration 006: Add attachments to notes table.
*
* This migration adds columns for images and files to the notes table.
*/
class AddAttachmentsToNotes
{
/**
* Run the migration.
*
* @return void
*/
public function up()
{
if (Capsule::schema()->hasTable('notes')) {
Capsule::schema()->table('notes', function ($table) {
if (!Capsule::schema()->hasColumn('notes', 'notes_image')) {
$table->string('notes_image')->nullable()->after('notes_color');
}
if (!Capsule::schema()->hasColumn('notes', 'notes_file')) {
$table->string('notes_file')->nullable()->after('notes_image');
}
});
}
}
/**
* Reverse the migration.
*
* @return void
*/
public function down()
{
if (Capsule::schema()->hasTable('notes')) {
Capsule::schema()->table('notes', function ($table) {
if (Capsule::schema()->hasColumn('notes', 'notes_image')) {
$table->dropColumn('notes_image');
}
if (Capsule::schema()->hasColumn('notes', 'notes_file')) {
$table->dropColumn('notes_file');
}
});
}
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"title": "Meine Notizen",
"add_note": "Notiz hinzufügen",
"edit_note": "Notiz bearbeiten",
"note_title": "Titel",
"note_content": "Inhalt",
"note_subject": "Kategorie",
"all_subjects": "Alle Kategorien",
"manage_subjects": "Kategorien verwalten",
"add_subject": "Kategorie hinzufügen",
"subject_name": "Kategoriename",
"subject_color": "Kategoriefarbe",
"select_subject": "Wählen Sie eine Kategorie",
"search_placeholder": "Notizen suchen...",
"note_color": "Farbe",
"is_pinned": "Oben anheften",
"save": "Speichern",
"cancel": "Abbrechen",
"delete_confirm": "Sind Sie sicher, dass Sie diese Notiz löschen möchten?",
"delete_subject_confirm": "Sind Sie sicher, dass Sie diese Kategorie löschen möchten? Alle verknüpften Notizen bleiben ohne Kategorie.",
"no_notes": "Keine Notizen gefunden.",
"created_at": "Erstellt am",
"updated_at": "Aktualisiert am",
"pin": "Anheften",
"unpin": "Lösen",
"delete": "Löschen",
"flash_note_created": "Notiz erfolgreich erstellt!",
"flash_note_updated": "Notiz erfolgreich aktualisiert!",
"flash_note_deleted": "Notiz erfolgreich gelöscht!",
"flash_note_pinned": "Notiz angeheftet!",
"flash_note_unpinned": "Notiz gelöst!",
"flash_subject_created": "Kategorie erfolgreich erstellt!",
"flash_subject_deleted": "Kategorie erfolgreich gelöscht!",
"add": "Hinzufügen",
"note_image": "Bild",
"note_file": "Datei",
"view_file": "Datei ansehen",
"has_attachments": "Hat Anhänge"
}
+39
View File
@@ -0,0 +1,39 @@
{
"title": "My Notes",
"add_note": "Add Note",
"edit_note": "Edit Note",
"note_title": "Title",
"note_content": "Content",
"note_subject": "Category",
"all_subjects": "All Categories",
"manage_subjects": "Manage Categories",
"add_subject": "Add Category",
"subject_name": "Category Name",
"subject_color": "Category Color",
"select_subject": "Select a Category",
"search_placeholder": "Search notes...",
"note_color": "Color",
"is_pinned": "Pin to top",
"save": "Save",
"cancel": "Cancel",
"delete_confirm": "Are you sure you want to delete this note?",
"delete_subject_confirm": "Are you sure you want to delete this category? All linked notes will be left without a category.",
"no_notes": "No notes found.",
"created_at": "Created at",
"updated_at": "Updated at",
"pin": "Pin",
"unpin": "Unpin",
"delete": "Delete",
"flash_note_created": "Note created successfully!",
"flash_note_updated": "Note updated successfully!",
"flash_note_deleted": "Note deleted successfully!",
"flash_note_pinned": "Note pinned!",
"flash_note_unpinned": "Note unpinned!",
"flash_subject_created": "Category created successfully!",
"flash_subject_deleted": "Category deleted successfully!",
"add": "Add",
"note_image": "Image",
"note_file": "File",
"view_file": "View file",
"has_attachments": "Has attachments"
}
+39
View File
@@ -0,0 +1,39 @@
{
"title": "Mis Notas",
"add_note": "Añadir Nota",
"edit_note": "Editar Nota",
"note_title": "Título",
"note_content": "Contenido",
"note_subject": "Categoría",
"all_subjects": "Todas las Categorías",
"manage_subjects": "Gestionar Categorías",
"add_subject": "Añadir Categoría",
"subject_name": "Nombre de la Categoría",
"subject_color": "Color de la Categoría",
"select_subject": "Selecciona una Categoría",
"search_placeholder": "Buscar notas...",
"note_color": "Color",
"is_pinned": "Fijar arriba",
"save": "Guardar",
"cancel": "Cancelar",
"delete_confirm": "¿Estás seguro de que quieres eliminar esta nota?",
"delete_subject_confirm": "¿Estás seguro de que quieres eliminar esta categoría? Todas las notas vinculadas se quedarán sin categoría.",
"no_notes": "No se encontraron notas.",
"created_at": "Creado en",
"updated_at": "Actualizado en",
"pin": "Fijar",
"unpin": "Desfijar",
"delete": "Eliminar",
"flash_note_created": "¡Nota creada con éxito!",
"flash_note_updated": "¡Nota actualizada con éxito!",
"flash_note_deleted": "¡Nota eliminada con éxito!",
"flash_note_pinned": "¡Nota fijada!",
"flash_note_unpinned": "¡Nota desfijada!",
"flash_subject_created": "¡Categoría creada con éxito!",
"flash_subject_deleted": "¡Categoría eliminada con éxito!",
"add": "Añadir",
"note_image": "Imagen",
"note_file": "Archivo",
"view_file": "Ver archivo",
"has_attachments": "Tiene adjuntos"
}
+39
View File
@@ -0,0 +1,39 @@
{
"title": "Minhas Notas",
"add_note": "Adicionar Nota",
"edit_note": "Editar Nota",
"note_title": "Título",
"note_content": "Conteúdo",
"note_subject": "Categoria",
"all_subjects": "Todas as Categorias",
"manage_subjects": "Gerenciar Categorias",
"add_subject": "Adicionar Categoria",
"subject_name": "Nome da Categoria",
"subject_color": "Cor da Categoria",
"select_subject": "Selecione uma Categoria",
"search_placeholder": "Buscar notas...",
"note_color": "Cor",
"is_pinned": "Fixar no topo",
"save": "Salvar",
"cancel": "Cancelar",
"delete_confirm": "Tem certeza que deseja excluir esta nota?",
"delete_subject_confirm": "Tem certeza que deseja excluir esta categoria? Todas as notas vinculadas ficarão sem categoria.",
"no_notes": "Nenhuma nota encontrada.",
"created_at": "Criado em",
"updated_at": "Atualizado em",
"pin": "Fixar",
"unpin": "Desafixar",
"delete": "Excluir",
"flash_note_created": "Nota criada com sucesso!",
"flash_note_updated": "Nota atualizada com sucesso!",
"flash_note_deleted": "Nota excluída com sucesso!",
"flash_note_pinned": "Nota fixada!",
"flash_note_unpinned": "Nota desafixada!",
"flash_subject_created": "Categoria criada com sucesso!",
"flash_subject_deleted": "Categoria excluída com sucesso!",
"add": "Adicionar",
"note_image": "Imagem",
"note_file": "Arquivo",
"view_file": "Ver arquivo",
"has_attachments": "Possui anexos"
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "notes",
"display_name": "Notes",
"description": "Store personal notes for the user.",
"version": "1.0.0",
"author": "SysCMS Team",
"dependencies": [
"users"
],
"permissions": [
"notes.view",
"notes.create",
"notes.edit",
"notes.delete"
],
"settings": [],
"providers": [
"Modules\\Notes\\Providers\\NotesServiceProvider"
],
"routes": {
"web": "routes/web.php"
},
"views": "views",
"locales": "locales",
"menu": {
"dashboard": {
"icon": "bi bi-sticky",
"label": "notes.title",
"route": "/admin/notes",
"order": 60
}
},
"ready": true,
"tables": [
"notes",
"notes_subjects"
],
"module_ative": true
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2026 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Kernel\Application;
use Modules\Notes\Controllers\NotesController;
/**
* Register Notes module routes
*
* @param Application $app
* @return void
*/
return function (Application $app) {
// Admin Routes
$app->addRoute('GET', '/admin/notes', [NotesController::class, 'index']);
$app->addRoute('GET', '/admin/notes/create', [NotesController::class, 'create']);
$app->addRoute('POST', '/admin/notes/store', [NotesController::class, 'store']);
$app->addRoute('GET', '/admin/notes/edit/{id}', [NotesController::class, 'edit']);
$app->addRoute('POST', '/admin/notes/update/{id}', [NotesController::class, 'update']);
$app->addRoute('POST', '/admin/notes/delete/{id}', [NotesController::class, 'delete']);
$app->addRoute('POST', '/admin/notes/pin/{id}', [NotesController::class, 'pin']);
// Subjects management
$app->addRoute('POST', '/admin/notes/subjects/store', [NotesController::class, 'storeSubject']);
$app->addRoute('POST', '/admin/notes/subjects/delete/{id}', [NotesController::class, 'deleteSubject']);
};
+111
View File
@@ -0,0 +1,111 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
{% block head %}
<style>
#quill-editor {
height: 300px;
background: #fff;
}
.color-picker-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #ddd;
display: inline-block;
cursor: pointer;
margin-right: 5px;
}
.color-picker-btn.active {
border-color: #000;
transform: scale(1.1);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">{{ title }}</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="{{ url('/admin/dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ url('/admin/notes') }}">{{ __('notes.title') }}</a></li>
<li class="breadcrumb-item active">{{ title }}</li>
</ol>
<div class="card mb-4">
<div class="card-body">
<form id="noteForm" action="{{ url('/admin/notes/store') }}" method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-8 mb-3">
<label for="notes_title" class="form-label fw-bold">{{ __('notes.note_title') }}</label>
<input type="text" class="form-control" id="notes_title" name="notes_title" required>
</div>
<div class="col-md-4 mb-3">
<label for="notes_subject_id" class="form-label fw-bold">{{ __('notes.note_subject') }}</label>
<select class="form-select" id="notes_subject_id" name="notes_subject_id">
<option value="">{{ __('notes.select_subject') }}</option>
{% for subject in subjects %}
<option value="{{ subject.subjects_id }}">{{ subject.subjects_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">{{ __('notes.note_content') }}</label>
<div id="quill-editor" data-target="notes_content"></div>
<textarea name="notes_content" id="notes_content" style="display:none"></textarea>
</div>
<div class="row align-items-center mb-4">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold d-block">{{ __('notes.note_color') }}</label>
<div class="d-flex flex-wrap gap-2">
<input type="hidden" name="notes_color" id="notes_color" value="#ffffff">
<div class="color-picker-btn active" style="background-color: #ffffff;" onclick="setColor('#ffffff', this)"></div>
<div class="color-picker-btn" style="background-color: #fff9c4;" onclick="setColor('#fff9c4', this)"></div>
<div class="color-picker-btn" style="background-color: #c8e6c9;" onclick="setColor('#c8e6c9', this)"></div>
<div class="color-picker-btn" style="background-color: #bbdefb;" onclick="setColor('#bbdefb', this)"></div>
<div class="color-picker-btn" style="background-color: #f8bbd0;" onclick="setColor('#f8bbd0', this)"></div>
<div class="color-picker-btn" style="background-color: #e1bee7;" onclick="setColor('#e1bee7', this)"></div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch mt-md-4">
<input class="form-check-input" type="checkbox" id="notes_is_pinned" name="notes_is_pinned" value="1">
<label class="form-check-label" for="notes_is_pinned">{{ __('notes.is_pinned') }}</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6 mb-3">
<label for="notes_image" class="form-label fw-bold">{{ __('notes.note_image')|default('Imagem') }}</label>
<input type="file" class="form-control" id="notes_image" name="notes_image" accept="image/*">
</div>
<div class="col-md-6 mb-3">
<label for="notes_file" class="form-label fw-bold">{{ __('notes.note_file')|default('Arquivo') }}</label>
<input type="file" class="form-control" id="notes_file" name="notes_file">
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ url('/admin/notes') }}" class="btn btn-secondary">{{ __('notes.cancel') }}</a>
<button type="submit" class="btn btn-primary">{{ __('notes.save') }}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function setColor(color, element) {
document.getElementById('notes_color').value = color;
document.querySelectorAll('.color-picker-btn').forEach(btn => btn.classList.remove('active'));
element.classList.add('active');
}
</script>
{% endblock %}
+123
View File
@@ -0,0 +1,123 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
{% block head %}
<style>
#quill-editor {
height: 300px;
background: #fff;
}
.color-picker-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #ddd;
display: inline-block;
cursor: pointer;
margin-right: 5px;
}
.color-picker-btn.active {
border-color: #000;
transform: scale(1.1);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">{{ title }}</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="{{ url('/admin/dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ url('/admin/notes') }}">{{ __('notes.title') }}</a></li>
<li class="breadcrumb-item active">{{ title }}</li>
</ol>
<div class="card mb-4">
<div class="card-body">
<form id="noteForm" action="{{ url('/admin/notes/update/' ~ note.notes_id) }}" method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-8 mb-3">
<label for="notes_title" class="form-label fw-bold">{{ __('notes.note_title') }}</label>
<input type="text" class="form-control" id="notes_title" name="notes_title" value="{{ note.notes_title }}" required>
</div>
<div class="col-md-4 mb-3">
<label for="notes_subject_id" class="form-label fw-bold">{{ __('notes.note_subject') }}</label>
<select class="form-select" id="notes_subject_id" name="notes_subject_id">
<option value="">{{ __('notes.select_subject') }}</option>
{% for subject in subjects %}
<option value="{{ subject.subjects_id }}" {{ note.notes_subject_id == subject.subjects_id ? 'selected' : '' }}>{{ subject.subjects_name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">{{ __('notes.note_content') }}</label>
<div id="quill-editor" data-target="notes_content">{{ note.notes_content|raw }}</div>
<textarea name="notes_content" id="notes_content" style="display:none">{{ note.notes_content }}</textarea>
</div>
<div class="row align-items-center mb-4">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold d-block">{{ __('notes.note_color') }}</label>
<div class="d-flex flex-wrap gap-2">
<input type="hidden" name="notes_color" id="notes_color" value="{{ note.notes_color }}">
{% set colors = ['#ffffff', '#fff9c4', '#c8e6c9', '#bbdefb', '#f8bbd0', '#e1bee7'] %}
{% for color in colors %}
<div class="color-picker-btn {{ note.notes_color == color ? 'active' : '' }}"
style="background-color: {{ color }};"
onclick="setColor('{{ color }}', this)"></div>
{% endfor %}
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check form-switch mt-md-4">
<input class="form-check-input" type="checkbox" id="notes_is_pinned" name="notes_is_pinned" value="1" {{ note.notes_is_pinned ? 'checked' : '' }}>
<label class="form-check-label" for="notes_is_pinned">{{ __('notes.is_pinned') }}</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6 mb-3">
<label for="notes_image" class="form-label fw-bold">{{ __('notes.note_image')|default('Imagem') }}</label>
{% if note.notes_image %}
<div class="mb-2">
<img src="/storage/uploads/notes/{{ note.notes_image }}" class="img-thumbnail" style="max-height: 100px;">
</div>
{% endif %}
<input type="file" class="form-control" id="notes_image" name="notes_image" accept="image/*">
</div>
<div class="col-md-6 mb-3">
<label for="notes_file" class="form-label fw-bold">{{ __('notes.note_file')|default('Arquivo') }}</label>
{% if note.notes_file %}
<div class="mb-2">
<a href="/storage/uploads/notes/{{ note.notes_file }}" target="_blank" class="btn btn-sm btn-outline-info">
<i class="fas fa-paperclip"></i> {{ __('notes.view_file')|default('Ver arquivo') }}
</a>
</div>
{% endif %}
<input type="file" class="form-control" id="notes_file" name="notes_file">
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ url('/admin/notes') }}" class="btn btn-secondary">{{ __('notes.cancel') }}</a>
<button type="submit" class="btn btn-primary">{{ __('notes.save') }}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function setColor(color, element) {
document.getElementById('notes_color').value = color;
document.querySelectorAll('.color-picker-btn').forEach(btn => btn.classList.remove('active'));
element.classList.add('active');
}
</script>
{% endblock %}
+194
View File
@@ -0,0 +1,194 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
{% block head %}
<style>
.note-card {
transition: transform 0.2s, box-shadow 0.2s;
border-radius: 10px;
overflow: hidden;
height: 100%;
}
.note-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important;
}
.note-content {
max-height: 200px;
overflow: hidden;
position: relative;
word-break: break-word;
overflow-wrap: break-word;
}
.note-content p {
margin-bottom: 0.5rem;
}
.note-content *:last-child {
margin-bottom: 0;
}
.pinned-badge {
position: absolute;
top: 10px;
right: 10px;
color: #ffc107;
font-size: 1.2rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mt-4">{{ title }}</h1>
<div class="d-flex gap-2">
<form action="{{ url('/admin/notes') }}" method="GET" class="d-flex gap-2 me-3">
<div class="input-group" style="min-width: 250px;">
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control" placeholder="{{ __('notes.search_placeholder') }}" value="{{ filters.search }}">
</div>
<select name="subject_id" class="form-select" style="min-width: 180px;" onchange="this.form.submit()">
<option value="">{{ __('notes.all_subjects') }}</option>
{% for subject in subjects %}
<option value="{{ subject.subjects_id }}" {{ filters.subject_id == subject.subjects_id ? 'selected' : '' }}>{{ subject.subjects_name }}</option>
{% endfor %}
</select>
{% if filters.search or filters.subject_id %}
<a href="{{ url('/admin/notes') }}" class="btn btn-outline-secondary" title="{{ __('notes.cancel') }}"><i class="bi bi-x-lg"></i></a>
{% endif %}
</form>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#subjectsModal">
<i class="bi bi-tags me-1"></i> {{ __('notes.manage_subjects') }}
</button>
<a href="{{ url('/admin/notes/create') }}" class="btn btn-primary">
<i class="bi bi-file-earmark-plus me-1"></i> {{ __('notes.add_note') }}
</a>
</div>
</div>
{% if flash_messages.success %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ flash_messages.success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
{% for note in notes %}
<div class="col">
<div class="card note-card shadow-sm h-100" style="background-color: {{ note.notes_color }};">
{% if note.notes_is_pinned %}
<div class="pinned-badge"><i class="bi bi-pin-angle-fill"></i></div>
{% endif %}
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title text-truncate pe-4">
{{ note.notes_title }}
{% if note.notes_image or note.notes_file %}
<span class="ms-1 fs-6 text-muted" title="{{ __('notes.has_attachments')|default('Possui anexos') }}">
<i class="bi bi-paperclip"></i>
</span>
{% endif %}
</h5>
<div class="dropdown">
<button class="btn btn-link btn-sm text-dark p-0" type="button" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="{{ url('/admin/notes/edit/' ~ note.notes_id) }}" class="dropdown-item">
<i class="bi bi-pencil me-2"></i> {{ __('notes.edit_note') }}
</a>
</li>
<li>
<form action="{{ url('/admin/notes/pin/' ~ note.notes_id) }}" method="POST" class="d-inline">
<button type="submit" class="dropdown-item">
<i class="bi bi-pin me-2"></i> {{ note.notes_is_pinned ? __('notes.unpin') : __('notes.pin') }}
</button>
</form>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<form action="{{ url('/admin/notes/delete/' ~ note.notes_id) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ __('notes.delete_confirm') }}')">
<button type="submit" class="dropdown-item text-danger">
<i class="bi bi-trash me-2"></i> {{ __('notes.delete') }}
</button>
</form>
</li>
</ul>
</div>
</div>
<div class="note-content small text-muted">
{{ note.notes_content|raw }}
</div>
</div>
<div class="card-footer bg-transparent border-0 d-flex justify-content-between align-items-center">
<div class="d-flex gap-1">
{% if note.subject %}
<span class="badge text-dark" style="background-color: {{ note.subject.subjects_color }};">
<i class="bi bi-tag me-1"></i>
{{ note.subject.subjects_name }}
</span>
{% endif %}
</div>
<small class="text-muted" title="{{ __('notes.created_at') }}: {{ note.created_at|date('d/m/Y H:i') }}">
{{ note.created_at|date('d/m/Y') }}
</small>
</div>
</div>
</div>
{% else %}
<div class="col-12 text-center py-5">
<div class="text-muted">
<i class="bi bi-journal-x display-1 mb-3"></i>
<p class="lead">{{ __('notes.no_notes') }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Subjects Management Modal -->
<div class="modal fade" id="subjectsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ __('notes.manage_subjects') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ __('notes.cancel') }}"></button>
</div>
<div class="modal-body">
<form action="{{ url('/admin/notes/subjects/store') }}" method="POST" class="mb-4">
<div class="row g-2">
<div class="col-7">
<input type="text" name="subjects_name" class="form-control" placeholder="{{ __('notes.subject_name') }}" required>
</div>
<div class="col-3">
<input type="color" name="subjects_color" class="form-control form-control-color w-100" value="#6c757d" title="{{ __('notes.subject_color') }}">
</div>
<div class="col-2">
<button type="submit" class="btn btn-primary w-100" title="{{ __('notes.add') }}"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
</form>
<div class="list-group">
{% for subject in subjects %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<span class="badge rounded-circle p-2 me-2" style="background-color: {{ subject.subjects_color }};"> </span>
{{ subject.subjects_name }}
</div>
<form action="{{ url('/admin/notes/subjects/delete/' ~ subject.subjects_id) }}" method="POST" onsubmit="return confirm('{{ __('notes.delete_subject_confirm') }}')">
<button type="submit" class="btn btn-link btn-sm text-danger p-0" title="{{ __('notes.delete') }}"><i class="bi bi-trash"></i></button>
</form>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}