Added Service

This commit is contained in:
Marc Rejohn Castillano 2026-02-21 21:57:31 +08:00
parent 6238c701c0
commit 46a84b4d95
8 changed files with 171 additions and 41 deletions

View File

@ -1,10 +1,15 @@
class Office { class Office {
Office({required this.id, required this.name}); Office({required this.id, required this.name, this.serviceId});
final String id; final String id;
final String name; final String name;
final String? serviceId;
factory Office.fromMap(Map<String, dynamic> map) { factory Office.fromMap(Map<String, dynamic> map) {
return Office(id: map['id'] as String, name: map['name'] as String? ?? ''); return Office(
id: map['id'] as String,
name: map['name'] as String? ?? '',
serviceId: map['service_id'] as String?,
);
} }
} }

10
lib/models/service.dart Normal file
View File

@ -0,0 +1,10 @@
class Service {
Service({required this.id, required this.name});
final String id;
final String name;
factory Service.fromMap(Map<String, dynamic> map) {
return Service(id: map['id'] as String, name: map['name'] as String? ?? '');
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/service.dart';
import 'supabase_provider.dart';
final servicesProvider = StreamProvider<List<Service>>((ref) {
final client = ref.watch(supabaseClientProvider);
return client
.from('services')
.stream(primaryKey: ['id'])
.order('name')
.map((rows) => rows.map((r) => Service.fromMap(r)).toList());
});
final servicesOnceProvider = FutureProvider<List<Service>>((ref) async {
final client = ref.watch(supabaseClientProvider);
final rows = await client.from('services').select().order('name');
return (rows as List<dynamic>)
.map((r) => Service.fromMap(r as Map<String, dynamic>))
.toList();
});

View File

@ -377,12 +377,20 @@ class OfficesController {
final SupabaseClient _client; final SupabaseClient _client;
Future<void> createOffice({required String name}) async { Future<void> createOffice({required String name, String? serviceId}) async {
await _client.from('offices').insert({'name': name}); final payload = {'name': name};
if (serviceId != null) payload['service_id'] = serviceId;
await _client.from('offices').insert(payload);
} }
Future<void> updateOffice({required String id, required String name}) async { Future<void> updateOffice({
await _client.from('offices').update({'name': name}).eq('id', id); required String id,
required String name,
String? serviceId,
}) async {
final payload = {'name': name};
if (serviceId != null) payload['service_id'] = serviceId;
await _client.from('offices').update(payload).eq('id', id);
} }
Future<void> deleteOffice({required String id}) async { Future<void> deleteOffice({required String id}) async {

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/office.dart'; import '../../models/office.dart';
import '../../providers/profile_provider.dart'; import '../../providers/profile_provider.dart';
import '../../providers/tickets_provider.dart'; import '../../providers/tickets_provider.dart';
import '../../providers/services_provider.dart';
import '../../widgets/mono_text.dart'; import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart'; import '../../theme/app_surfaces.dart';
@ -163,16 +164,59 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
Office? office, Office? office,
}) async { }) async {
final nameController = TextEditingController(text: office?.name ?? ''); final nameController = TextEditingController(text: office?.name ?? '');
String? selectedServiceId = office?.serviceId;
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
final servicesAsync = ref.watch(servicesOnceProvider);
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog( return AlertDialog(
shape: AppSurfaces.of(context).dialogShape, shape: AppSurfaces.of(context).dialogShape,
title: Text(office == null ? 'Create Office' : 'Edit Office'), title: Text(office == null ? 'Create Office' : 'Edit Office'),
content: TextField( content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController, controller: nameController,
decoration: const InputDecoration(labelText: 'Office name'), decoration: const InputDecoration(
labelText: 'Office name',
),
),
const SizedBox(height: 12),
servicesAsync.when(
data: (services) {
return DropdownButtonFormField<String?>(
initialValue: selectedServiceId,
decoration: const InputDecoration(
labelText: 'Service',
),
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('None'),
),
...services.map(
(s) => DropdownMenuItem<String?>(
value: s.id,
child: Text(s.name),
),
),
],
onChanged: (v) =>
setState(() => selectedServiceId = v),
);
},
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: LinearProgressIndicator(),
),
error: (e, _) => Text('Failed to load services: $e'),
),
],
),
), ),
actions: [ actions: [
TextButton( TextButton(
@ -190,9 +234,16 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
} }
final controller = ref.read(officesControllerProvider); final controller = ref.read(officesControllerProvider);
if (office == null) { if (office == null) {
await controller.createOffice(name: name); await controller.createOffice(
name: name,
serviceId: selectedServiceId,
);
} else { } else {
await controller.updateOffice(id: office.id, name: name); await controller.updateOffice(
id: office.id,
name: name,
serviceId: selectedServiceId,
);
} }
ref.invalidate(officesProvider); ref.invalidate(officesProvider);
if (context.mounted) { if (context.mounted) {
@ -205,6 +256,8 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
); );
}, },
); );
},
);
} }
Future<void> _confirmDelete( Future<void> _confirmDelete(

View File

@ -0,0 +1,30 @@
-- Add services table and link offices to services
-- 1) Create services table
CREATE TABLE IF NOT EXISTS services (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE
);
-- 2) Add service_id to offices
ALTER TABLE IF EXISTS offices
ADD COLUMN IF NOT EXISTS service_id uuid REFERENCES services(id) ON DELETE SET NULL;
-- 3) Insert default services
INSERT INTO services (name) VALUES
('Medical Center Chief'),
('Medical Service'),
('Nursing Service'),
('Hospital Operations and Patient Support Service'),
('Finance Service'),
('Allied/Ancillary')
ON CONFLICT (name) DO NOTHING;
-- 4) Make existing offices default to Medical Center Chief (for now)
UPDATE offices
SET service_id = s.id
FROM services s
WHERE s.name = 'Medical Center Chief';
-- 5) (Optional) Add index for faster lookups
CREATE INDEX IF NOT EXISTS idx_offices_service_id ON offices(service_id);

View File

@ -68,6 +68,7 @@ void main() {
final task = Task( final task = Task(
id: 'TSK-1', id: 'TSK-1',
ticketId: 'TCK-1', ticketId: 'TCK-1',
taskNumber: '2026-02-00001',
title: 'Reboot printer', title: 'Reboot printer',
description: 'Clear queue and reboot', description: 'Clear queue and reboot',
officeId: 'office-1', officeId: 'office-1',

View File

@ -106,6 +106,7 @@ void main() {
final emptyTask = Task( final emptyTask = Task(
id: 'tsk-1', id: 'tsk-1',
ticketId: null, ticketId: null,
taskNumber: '2026-02-00002',
title: 'No metadata', title: 'No metadata',
description: '', description: '',
officeId: 'office-1', officeId: 'office-1',
@ -159,6 +160,7 @@ void main() {
final task = Task( final task = Task(
id: 'tsk-1', id: 'tsk-1',
ticketId: null, ticketId: null,
taskNumber: '2026-02-00002',
title: 'Has metadata', title: 'Has metadata',
description: '', description: '',
officeId: 'office-1', officeId: 'office-1',