diff --git a/docs/user-stories/00-Generic/US15-utilizador-username.md b/docs/user-stories/00-Generic/US15-utilizador-username.md
new file mode 100644
index 0000000..39972a9
--- /dev/null
+++ b/docs/user-stories/00-Generic/US15-utilizador-username.md
@@ -0,0 +1,27 @@
+# US15 - Identificação de Utilizador por Username
+
+**Como** utilizador do RefoodOne
+**Quero** que a minha conta de acesso seja caracterizada por um username (para login) e um nome completo (para visualização no sistema)
+**Para** simplificar o login no dia-a-dia e manter o meu nome legível na lista de utilizadores.
+
+## Descrição do Fluxo
+1. Ao aceder à página de login, o voluntário insere o seu **Nome de Utilizador** (e.g. `joao`) em vez do e-mail ou nome completo.
+2. Na lista de utilizadores na área de administração, o Administrador consegue visualizar em colunas separadas o **Nome de Utilizador** e o **Nome Completo**.
+3. Ao criar ou editar um utilizador, o Administrador pode definir ambos os campos (**Nome Completo** e **Nome de Utilizador**).
+
+## Critérios de Aceitação
+
+### 1. Interface Gráfica (UI)
+- **Ecrã de Login**: Campo de texto rotulado como "Nome de Utilizador" (ex: `refoodpdn`) em vez de "Utilizador / E-mail".
+- **Ecrã de Listagem**: Coluna para o Nome Completo (Nome) e coluna para o Nome de Utilizador (Username).
+- **Ecrã de Detalhe/Criação**: Dois campos de texto:
+ - **Nome Completo** (e.g. "João Silva")
+ - **Nome de Utilizador** (e.g. "joao")
+
+### 2. Comportamento e Regras de Negócio
+- O `username` deve ser único na base de dados.
+- Ao atualizar a base de dados, as contas já existentes devem ter o seu `username` atualizado de forma automática para o seu primeiro nome (a primeira palavra do campo nome em minúsculas).
+- O login é efetuado através do `username`.
+
+### 3. Integração de Dados
+- Alteração da tabela `users` para conter ambos os campos `name` e `username`.
diff --git a/src/app.d.ts b/src/app.d.ts
index 80abb84..cc1f3ae 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -6,6 +6,7 @@ declare global {
interface Locals {
user: {
id: string;
+ name: string;
username: string;
role: string;
} | null;
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index ccdc718..918f8f6 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -26,6 +26,7 @@ export const handle: Handle = async ({ event, resolve }) => {
if (session.expiresAt > Date.now()) {
event.locals.user = {
id: user.id,
+ name: user.name,
username: user.username,
role: user.role
};
diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts
index a0e999b..3f95b3d 100644
--- a/src/lib/server/db/index.ts
+++ b/src/lib/server/db/index.ts
@@ -8,6 +8,41 @@ if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
const client = new Database(env.DATABASE_URL);
+// Run schema migration if 'name' column is missing
+try {
+ const tableInfo = client.prepare("PRAGMA table_info(users)").all() as any[];
+ const hasNameColumn = tableInfo.some((col) => col.name === 'name');
+ if (!hasNameColumn) {
+ console.log("Migrating users table: adding 'name' column...");
+
+ // 1. Add nullable 'name' column
+ client.prepare("ALTER TABLE users ADD COLUMN name TEXT").run();
+
+ // 2. Set 'name' to the current 'username' (which represented the name before)
+ client.prepare("UPDATE users SET name = username").run();
+
+ // 3. Update 'username' to the lowercase first name
+ const allUsers = client.prepare("SELECT id, name FROM users").all() as any[];
+ for (const u of allUsers) {
+ const fullName = u.name || '';
+ const firstName = fullName.trim().split(/\s+/)[0].toLowerCase();
+
+ let uniqueUsername = firstName;
+ let counter = 1;
+ while (true) {
+ const exists = client.prepare("SELECT id FROM users WHERE username = ? AND id != ?").get(uniqueUsername, u.id);
+ if (!exists) break;
+ uniqueUsername = `${firstName}${counter}`;
+ counter++;
+ }
+ client.prepare("UPDATE users SET username = ? WHERE id = ?").run(uniqueUsername, u.id);
+ }
+ console.log("Database migration for users table completed.");
+ }
+} catch (err) {
+ console.error("Migration failed:", err);
+}
+
export const db = drizzle(client, { schema });
// Seed admin user if the users table is empty (US00)
@@ -17,6 +52,7 @@ try {
console.log('Seeding default admin user (refoodpdn)...');
const passwordHash = bcrypt.hashSync('rpdn!2512', 10);
db.insert(schema.users).values({
+ name: 'Administrador Refood',
username: 'refoodpdn',
passwordHash: passwordHash,
role: 'admin'
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index ff81c55..f149157 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -2,6 +2,7 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
+ name: text('name').notNull(),
username: text('username').notNull().unique(),
passwordHash: text('password_hash').notNull(),
role: text('role').notNull(), // 'admin' | 'shift_manager' | 'volunteer'
diff --git a/src/routes/admin/utilizadores/+page.server.ts b/src/routes/admin/utilizadores/+page.server.ts
index df2cd50..54dd5f4 100644
--- a/src/routes/admin/utilizadores/+page.server.ts
+++ b/src/routes/admin/utilizadores/+page.server.ts
@@ -8,12 +8,13 @@ export const load: PageServerLoad = async () => {
const list = db
.select({
id: schema.users.id,
+ name: schema.users.name,
username: schema.users.username,
role: schema.users.role,
createdAt: schema.users.createdAt
})
.from(schema.users)
- .orderBy(asc(schema.users.username))
+ .orderBy(asc(schema.users.name))
.all();
return {
diff --git a/src/routes/admin/utilizadores/+page.svelte b/src/routes/admin/utilizadores/+page.svelte
index 126ca6c..32766b1 100644
--- a/src/routes/admin/utilizadores/+page.svelte
+++ b/src/routes/admin/utilizadores/+page.svelte
@@ -60,6 +60,7 @@
@@ -67,7 +68,7 @@
Nome
+ Username
Role
Ações
Nenhum utilizador encontrado.